From 2504cf3f2d44cfed9a25a31a6e0d4ed14085baed Mon Sep 17 00:00:00 2001 From: Martin Emde Date: Thu, 13 Nov 2025 11:08:18 -0800 Subject: [PATCH] Restore support for CodeOwnership.remove_file_annotations! This method is called by rubyatscale/packs gem when moving packs. Co-authored-by: Sweta Sanghavi Co-authored-by: Ashley Willard Co-authored-by: Denis Kisselev Co-authored-by: Ivy Evans --- lib/code_ownership.rb | 51 ++++++++++ spec/lib/code_ownership_spec.rb | 170 ++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) diff --git a/lib/code_ownership.rb b/lib/code_ownership.rb index 3116fad..62c1c41 100644 --- a/lib/code_ownership.rb +++ b/lib/code_ownership.rb @@ -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)) } diff --git a/spec/lib/code_ownership_spec.rb b/spec/lib/code_ownership_spec.rb index 85e1c19..6abb7d2 100644 --- a/spec/lib/code_ownership_spec.rb +++ b/spec/lib/code_ownership_spec.rb @@ -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