Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions lib/code_ownership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,57 @@ def validate!(
end
end

# Removes the file annotation (e.g., "# @team TeamName") from a file.
#
# This method removes the ownership annotation from the first line of a file,
# which is typically used to declare team ownership at the file level.
# The annotation can be in the form of:
# - Ruby comments: # @team TeamName
# - JavaScript/TypeScript comments: // @team TeamName
# - YAML comments: -# @team TeamName
#
# If the file does not have an annotation or the annotation doesn't match a valid team,
# this method does nothing.
#
# @param filename [String] The path to the file from which to remove the annotation.
# Can be relative or absolute.
#
# @return [void]
#
# @example Remove annotation from a Ruby file
# # Before: File contains "# @team Platform\nclass User; end"
# CodeOwnership.remove_file_annotation!('app/models/user.rb')
# # After: File contains "class User; end"
#
# @example Remove annotation from a JavaScript file
# # Before: File contains "// @team Frontend\nexport default function() {}"
# CodeOwnership.remove_file_annotation!('app/javascript/component.js')
# # After: File contains "export default function() {}"
#
# @note This method modifies the file in place.
# @note Leading newlines after the annotation are also removed to maintain clean formatting.
#
sig { params(filename: String).void }
def remove_file_annotation!(filename)
filepath = Pathname.new(filename)

begin
content = filepath.read
rescue Errno::EISDIR, Errno::ENOENT
# Ignore files that fail to read (directories, missing files, etc.)
return
end

# Remove the team annotation and any trailing newlines after it
team_pattern = %r{\A(?:#|//|-#) @team .*\n+}
new_content = content.sub(team_pattern, '')

filepath.write(new_content) if new_content != content
rescue ArgumentError => e
# Handle invalid byte sequences gracefully
raise unless e.message.include?('invalid byte sequence')
end

# Given a backtrace from either `Exception#backtrace` or `caller`, find the
# first line that corresponds to a file with assigned ownership
sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
Expand Down
170 changes: 170 additions & 0 deletions spec/lib/code_ownership_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,174 @@
expect(described_class.version).to eq ["code_ownership version: #{CodeOwnership::VERSION}", "codeowners-rs version: #{RustCodeOwners.version}"]
end
end

describe '.remove_file_annotation!' do
subject(:remove_file_annotation) do
CodeOwnership.remove_file_annotation!(filename)
# Getting the owner gets stored in the cache, so after we remove the file annotation we want to bust the cache
CodeOwnership.bust_caches!
end

before do
write_file('config/teams/foo.yml', <<~CONTENTS)
name: Foo
github:
team: '@MyOrg/foo-team'
CONTENTS
write_configuration
end

context 'ruby file has no annotation' do
let(:filename) { 'app/my_file.rb' }

before do
write_file(filename, <<~CONTENTS)
# Empty file
CONTENTS
end

it 'has no effect' do
expect(File.read(filename)).to eq "# Empty file\n"

remove_file_annotation

expect(File.read(filename)).to eq "# Empty file\n"
end
end

context 'ruby file has annotation' do
let(:filename) { 'app/my_file.rb' }

before do
write_file(filename, <<~CONTENTS)
# @team Foo

# Some content
CONTENTS

RustCodeOwners.generate_and_validate(nil, false)
end

it 'removes the annotation' do
current_ownership = CodeOwnership.for_file(filename, from_codeowners: false)
expect(current_ownership&.name).to eq 'Foo'
expect(File.read(filename)).to eq <<~RUBY
# @team Foo

# Some content
RUBY

remove_file_annotation

new_ownership = CodeOwnership.for_file(filename, from_codeowners: false)
expect(new_ownership).to eq nil
expected_output = <<~RUBY
# Some content
RUBY

expect(File.read(filename)).to eq expected_output
end
end

context 'javascript file has annotation' do
let(:filename) { 'app/my_file.jsx' }

before do
write_file(filename, <<~CONTENTS)
// @team Foo

// Some content
CONTENTS

RustCodeOwners.generate_and_validate(nil, false)
end

it 'removes the annotation' do
current_ownership = CodeOwnership.for_file(filename, from_codeowners: false)
expect(current_ownership&.name).to eq 'Foo'
expect(File.read(filename)).to eq <<~JAVASCRIPT
// @team Foo

// Some content
JAVASCRIPT

remove_file_annotation

new_ownership = CodeOwnership.for_file(filename, from_codeowners: false)
expect(new_ownership).to eq nil
expected_output = <<~JAVASCRIPT
// Some content
JAVASCRIPT

expect(File.read(filename)).to eq expected_output
end
end

context "haml has annotation (only verifies file is changed, the curren implementation doesn't verify haml files)" do
let(:filename) { 'app/views/my_file.html.haml' }

before do
write_file(filename, <<~CONTENTS)
-# @team Foo

-# Some content
CONTENTS
end

it 'removes the annotation' do
expect(File.read(filename)).to eq <<~HAML
-# @team Foo

-# Some content
HAML

remove_file_annotation

expected_output = <<~HAML
-# Some content
HAML

expect(File.read(filename)).to eq expected_output
end
end

context 'file has new lines after the annotation' do
let(:filename) { 'app/my_file.rb' }

before do
write_file(filename, <<~CONTENTS)
# @team Foo


# Some content


# Some other content
CONTENTS
end

it 'removes the annotation and the leading new lines' do
expect(File.read(filename)).to eq <<~RUBY
# @team Foo


# Some content


# Some other content
RUBY

remove_file_annotation

expected_output = <<~RUBY
# Some content


# Some other content
RUBY

expect(File.read(filename)).to eq expected_output
end
end
end
end