code_ownership 2.0.0.pre.5-aarch64-linux → 2.1.1-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4328f66e3db5f10c84c7d3e386dd215cabd5330f45468a636f0614756e84b000
4
- data.tar.gz: 4bf797d7470e50b4e4a7fafc3ec4762bff0de2dfebf090bfaf51dcb5427a593c
3
+ metadata.gz: 9e9175ef61c6c43ee0d46a598b3b4a4964e48ea003c36ea55389d3f26ceafe09
4
+ data.tar.gz: 645682d314c7193f1cd66775a2cc9b291d277410f1ed52faab56170bca62d16c
5
5
  SHA512:
6
- metadata.gz: 3c27b636afa5c3a821355aa128f1e09142f4c7116a7f5f732ed783edadaf766574b2af44d67b034d4a272c87041868e1f91950836051795a75361c261997b20f
7
- data.tar.gz: f0f5bcb16165cebe23a4130f278cb3070976e36e580c6fd98a732229af7c2bbc1fb93c2a3730c371aa88e9c174666129a7f930a992ceea9497d99e50032312a5
6
+ metadata.gz: 70d0082a39a4c73e0ff15eff891c551edb7e13541555172442ccba003f7d619e6ac1912d80629c7c6ef8b90a39fa6366f6c8fe002a3767315cbeee05aa241e8a
7
+ data.tar.gz: d113bc64fd257d8bc9e5a2eb11c501d4dbee8e6d58ba4ab0d23f2cc68baf50aafce2570d00cee3690a9ed784ee89b28fe66ec41a1ad49117a74e1a9dd695af8d
data/README.md CHANGED
@@ -6,13 +6,14 @@ Check out [`lib/code_ownership.rb`](https://github.com/rubyatscale/code_ownershi
6
6
 
7
7
  Check out [`code_ownership_spec.rb`](https://github.com/rubyatscale/code_ownership/blob/main/spec/lib/code_ownership_spec.rb) to see examples of how code ownership is used.
8
8
 
9
- There is also a [companion VSCode Extension]([url](https://github.com/rubyatscale/code-ownership-vscode)) for this gem. Just search `Gusto.code-ownership-vscode` in the VSCode Extension Marketplace.
9
+ There is also a [companion VSCode Extension](https://github.com/rubyatscale/code-ownership-vscode) for this gem. Just search `Gusto.code-ownership-vscode` in the VSCode Extension Marketplace.
10
10
 
11
11
  ## Getting started
12
12
 
13
13
  To get started there's a few things you should do.
14
14
 
15
- 1) Create a `config/code_ownership.yml` file and declare where your files live. Here's a sample to start with:
15
+ 1. Create a `config/code_ownership.yml` file and declare where your files live. Here's a sample to start with:
16
+
16
17
  ```yml
17
18
  owned_globs:
18
19
  - '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
@@ -23,33 +24,42 @@ unowned_globs:
23
24
  - app/services/some_file2.rb
24
25
  - frontend/javascripts/**/__generated__/**/*
25
26
  ```
26
- 2) Declare some teams. Here's an example, that would live at `config/teams/operations.yml`:
27
+
28
+ 2. Declare some teams. Here's an example, that would live at `config/teams/operations.yml`:
29
+
27
30
  ```yml
28
31
  name: Operations
29
32
  github:
30
33
  team: '@my-org/operations-team'
31
34
  ```
32
- 3) Declare ownership. You can do this at a directory level or at a file level. All of the files within the `owned_globs` you declared in step 1 will need to have an owner assigned (or be opted out via `unowned_globs`). See the next section for more detail.
33
- 4) Run validations when you commit, and/or in CI. If you run validations in CI, ensure that if your `.github/CODEOWNERS` file gets changed, that gets pushed to the PR.
35
+
36
+ 3. Declare ownership. You can do this at a directory level or at a file level. All of the files within the `owned_globs` you declared in step 1 will need to have an owner assigned (or be opted out via `unowned_globs`). See the next section for more detail.
37
+ 4. Run validations when you commit, and/or in CI. If you run validations in CI, ensure that if your `.github/CODEOWNERS` file gets changed, that gets pushed to the PR.
34
38
 
35
39
  ## Usage: Declaring Ownership
36
40
 
37
41
  There are five ways to declare code ownership using this gem:
38
42
 
39
43
  ### Directory-Based Ownership
44
+
40
45
  Directory based ownership allows for all files in that directory and all its sub-directories to be owned by one team. To define this, add a `.codeowner` file inside that directory with the name of the team as the contents of that file.
46
+
41
47
  ```
42
48
  Team
43
49
  ```
44
50
 
45
51
  ### File-Annotation Based Ownership
52
+
46
53
  File annotations are a last resort if there is no clear home for your code. File annotations go at the top of your file, and look like this:
54
+
47
55
  ```ruby
48
56
  # @team MyTeam
49
57
  ```
50
58
 
51
59
  ### Package-Based Ownership
60
+
52
61
  Package based ownership integrates [`packwerk`](https://github.com/Shopify/packwerk) and has ownership defined per package. To define that all files within a package are owned by one team, configure your `package.yml` like this:
62
+
53
63
  ```yml
54
64
  enforce_dependency: true
55
65
  enforce_privacy: true
@@ -58,16 +68,19 @@ metadata:
58
68
  ```
59
69
 
60
70
  You can also define `owner` as a top-level key, e.g.
71
+
61
72
  ```yml
62
73
  enforce_dependency: true
63
74
  enforce_privacy: true
64
75
  owner: Team
65
76
  ```
66
77
 
67
- To do this, add `code_ownership` to the `require` key of your `packwerk.yml`. See https://github.com/Shopify/packwerk/blob/main/USAGE.md#loading-extensions for more information.
78
+ To do this, add `code_ownership` to the `require` key of your `packwerk.yml`. See <https://github.com/Shopify/packwerk/blob/main/USAGE.md#loading-extensions> for more information.
68
79
 
69
80
  ### Glob-Based Ownership
81
+
70
82
  In your team's configured YML (see [`code_teams`](https://github.com/rubyatscale/code_teams)), you can set `owned_globs` to be a glob of files your team owns. For example, in `my_team.yml`:
83
+
71
84
  ```yml
72
85
  name: My Team
73
86
  owned_globs:
@@ -78,6 +91,7 @@ unowned_globs:
78
91
  ```
79
92
 
80
93
  ### Javascript Package Ownership
94
+
81
95
  Javascript package based ownership allows you to specify an ownership key in a `package.json`. To use this, configure your `package.json` like this:
82
96
 
83
97
  ```json
@@ -91,6 +105,7 @@ Javascript package based ownership allows you to specify an ownership key in a `
91
105
  ```
92
106
 
93
107
  You can also tell `code_ownership` where to find JS packages in the configuration, like this:
108
+
94
109
  ```yml
95
110
  js_package_paths:
96
111
  - frontend/javascripts/packages/*
@@ -101,12 +116,15 @@ This defaults `**/`, which makes it look for `package.json` files across your ap
101
116
 
102
117
  > [!NOTE]
103
118
  > Javscript package ownership does not respect `unowned_globs`. If you wish to disable usage of this feature you can set `js_package_paths` to an empty list.
119
+
104
120
  ```yml
105
121
  js_package_paths: []
106
122
  ```
107
123
 
108
124
  ## Usage: Reading CodeOwnership
125
+
109
126
  ### `for_file`
127
+
110
128
  `CodeOwnership.for_file`, given a relative path to a file returns a `CodeTeams::Team` if there is a team that owns the file, `nil` otherwise.
111
129
 
112
130
  ```ruby
@@ -118,6 +136,7 @@ Contributor note: If you are making updates to this method or the methods gettin
118
136
  See `code_ownership_spec.rb` for examples.
119
137
 
120
138
  ### `for_backtrace`
139
+
121
140
  `CodeOwnership.for_backtrace` can be given a backtrace and will either return `nil`, or a `CodeTeams::Team`.
122
141
 
123
142
  ```ruby
@@ -141,19 +160,22 @@ Under the hood, this finds the file where the class is defined and returns the o
141
160
  See `code_ownership_spec.rb` for an example.
142
161
 
143
162
  ### `for_team`
163
+
144
164
  `CodeOwnership.for_team` can be used to generate an ownership report for a team.
165
+
145
166
  ```ruby
146
167
  CodeOwnership.for_team('My Team')
147
168
  ```
148
169
 
149
170
  You can shovel this into a markdown file for easy viewing using the CLI:
171
+
150
172
  ```
151
- bin/codeownership for_team 'My Team' > tmp/ownership_report.md
173
+ codeownership for_team 'My Team' > tmp/ownership_report.md
152
174
  ```
153
175
 
154
176
  ## Usage: Generating a `CODEOWNERS` file
155
177
 
156
- A `CODEOWNERS` file defines who owns specific files or paths in a repository. When you run `bin/codeownership validate`, a `.github/CODEOWNERS` file will automatically be generated and updated.
178
+ A `CODEOWNERS` file defines who owns specific files or paths in a repository. When you run `codeownership validate`, a `.github/CODEOWNERS` file will automatically be generated and updated.
157
179
 
158
180
  If `codeowners_path` is set in `code_ownership.yml` codeowners will use that path to generate the `CODEOWNERS` file. For example, `codeowners_path: docs` will generate `docs/CODEOWNERS`.
159
181
 
@@ -161,14 +183,15 @@ If `codeowners_path` is set in `code_ownership.yml` codeowners will use that pat
161
183
 
162
184
  CodeOwnership comes with a validation function to ensure the following things are true:
163
185
 
164
- 1) Only one mechanism is defining file ownership. That is -- you can't have a file annotation on a file owned via package-based or glob-based ownership. This helps make ownership behavior more clear by avoiding concerns about precedence.
165
- 2) All teams referenced as an owner for any file or package is a valid team (i.e. it's in the list of `CodeTeams.all`).
166
- 3) All files have ownership. You can specify in `unowned_globs` to represent a TODO list of files to add ownership to.
167
- 3) The `.github/CODEOWNERS` file is up to date. This is automatically corrected and staged unless specified otherwise with `bin/codeownership validate --skip-autocorrect --skip-stage`. You can turn this validation off by setting `skip_codeowners_validation: true` in `config/code_ownership.yml`.
186
+ 1. Only one mechanism is defining file ownership. That is -- you can't have a file annotation on a file owned via package-based or glob-based ownership. This helps make ownership behavior more clear by avoiding concerns about precedence.
187
+ 2. All teams referenced as an owner for any file or package is a valid team (i.e. it's in the list of `CodeTeams.all`).
188
+ 3. All files have ownership. You can specify in `unowned_globs` to represent a TODO list of files to add ownership to.
189
+ 4. The `.github/CODEOWNERS` file is up to date. This is automatically corrected and staged unless specified otherwise with `bin/codeownership validate --skip-autocorrect --skip-stage`. You can turn this validation off by setting `skip_codeowners_validation: true` in `config/code_ownership.yml`.
168
190
 
169
191
  CodeOwnership also allows you to specify which globs and file extensions should be considered ownable.
170
192
 
171
193
  Here is an example `config/code_ownership.yml`.
194
+
172
195
  ```yml
173
196
  owned_globs:
174
197
  - '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
@@ -178,13 +201,24 @@ unowned_globs:
178
201
  - app/services/some_file2.rb
179
202
  - frontend/javascripts/**/__generated__/**/*
180
203
  ```
204
+
181
205
  You can call the validation function with the Ruby API
206
+
182
207
  ```ruby
183
208
  CodeOwnership.validate!
184
209
  ```
210
+
185
211
  or the CLI
186
- ```
187
- bin/codeownership validate
212
+
213
+ ```bash
214
+ # Validate all files
215
+ codeownership validate
216
+
217
+ # Validate specific files
218
+ codeownership validate path/to/file1.rb path/to/file2.rb
219
+
220
+ # Validate only staged files
221
+ codeownership validate --diff
188
222
  ```
189
223
 
190
224
  ## Development
@@ -192,10 +226,12 @@ bin/codeownership validate
192
226
  Please add to `CHANGELOG.md` and this `README.md` when you make make changes.
193
227
 
194
228
  ## Running specs
229
+
195
230
  ```sh
196
231
  bundle install
197
232
  bundle exec rake
198
233
  ```
199
234
 
200
235
  ## Creating a new release
236
+
201
237
  Simply [create a new release](https://github.com/rubyatscale/code_ownership/releases/new) with github. The release tag must match the gem version
@@ -1,4 +1,5 @@
1
1
  # typed: true
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'optparse'
4
5
  require 'pathname'
@@ -37,7 +38,7 @@ module CodeOwnership
37
38
  options = {}
38
39
 
39
40
  parser = OptionParser.new do |opts|
40
- opts.banner = "Usage: #{EXECUTABLE} validate [options]"
41
+ opts.banner = "Usage: #{EXECUTABLE} validate [options] [files...]"
41
42
 
42
43
  opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .github/CODEOWNERS file') do
43
44
  options[:skip_autocorrect] = true
@@ -59,11 +60,22 @@ module CodeOwnership
59
60
  args = parser.order!(argv)
60
61
  parser.parse!(args)
61
62
 
62
- files = if options[:diff]
63
+ # Collect any remaining arguments as file paths
64
+ specified_files = argv.reject { |arg| arg.start_with?('--') }
65
+
66
+ files = if !specified_files.empty?
67
+ # Files explicitly provided on command line
68
+ if options[:diff]
69
+ warn 'Warning: Ignoring --diff flag because explicit files were provided'
70
+ end
71
+ specified_files.select { |file| File.exist?(file) }
72
+ elsif options[:diff]
73
+ # Staged files from git
63
74
  ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
64
75
  File.exist?(file)
65
76
  end
66
77
  else
78
+ # No files specified, validate all
67
79
  nil
68
80
  end
69
81
 
@@ -1,19 +1,15 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  # typed: strict
4
3
 
5
4
  module CodeOwnership
6
5
  module Private
7
6
  module FilePathFinder
8
- module_function
9
-
10
7
  extend T::Sig
11
- extend T::Helpers
12
8
 
13
9
  # Returns a string version of the relative path to a Rails constant,
14
10
  # or nil if it can't find anything
15
- sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(String)) }
16
- def path_from_klass(klass)
11
+ sig { params(klass: T.nilable(T.any(T::Class[T.anything], T::Module[T.anything]))).returns(T.nilable(String)) }
12
+ def self.path_from_klass(klass)
17
13
  if klass
18
14
  path = Object.const_source_location(klass.to_s)&.first
19
15
  (path && Pathname.new(path).relative_path_from(Pathname.pwd).to_s) || nil
@@ -23,7 +19,7 @@ module CodeOwnership
23
19
  end
24
20
 
25
21
  sig { params(backtrace: T.nilable(T::Array[String])).returns(T::Enumerable[String]) }
26
- def from_backtrace(backtrace)
22
+ def self.from_backtrace(backtrace)
27
23
  return [] unless backtrace
28
24
 
29
25
  # The pattern for a backtrace hasn't changed in forever and is considered
@@ -1,37 +1,33 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  # typed: strict
4
3
 
5
4
  module CodeOwnership
6
5
  module Private
7
6
  module FilePathTeamCache
8
- module_function
9
-
10
7
  extend T::Sig
11
- extend T::Helpers
12
8
 
13
9
  sig { params(file_path: String).returns(T.nilable(CodeTeams::Team)) }
14
- def get(file_path)
10
+ def self.get(file_path)
15
11
  cache[file_path]
16
12
  end
17
13
 
18
14
  sig { params(file_path: String, team: T.nilable(CodeTeams::Team)).void }
19
- def set(file_path, team)
15
+ def self.set(file_path, team)
20
16
  cache[file_path] = team
21
17
  end
22
18
 
23
19
  sig { params(file_path: String).returns(T::Boolean) }
24
- def cached?(file_path)
20
+ def self.cached?(file_path)
25
21
  cache.key?(file_path)
26
22
  end
27
23
 
28
24
  sig { void }
29
- def bust_cache!
25
+ def self.bust_cache!
30
26
  @cache = nil
31
27
  end
32
28
 
33
29
  sig { returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
34
- def cache
30
+ def self.cache
35
31
  @cache ||= T.let(@cache,
36
32
  T.nilable(T::Hash[String, T.nilable(CodeTeams::Team)]))
37
33
  @cache ||= {}
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  # typed: strict
4
3
 
5
4
  module CodeOwnership
6
5
  module Private
7
6
  class ForFileOutputBuilder
8
7
  extend T::Sig
8
+
9
9
  private_class_method :new
10
10
 
11
11
  sig { params(file_path: String, json: T::Boolean, verbose: T::Boolean).void }
@@ -1,19 +1,13 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  # typed: strict
4
3
 
5
4
  module CodeOwnership
6
5
  module Private
7
6
  module TeamFinder
8
- module_function
9
-
10
7
  extend T::Sig
11
- extend T::Helpers
12
-
13
- requires_ancestor { Kernel }
14
8
 
15
9
  sig { params(file_path: String, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
16
- def for_file(file_path, allow_raise: false)
10
+ def self.for_file(file_path, allow_raise: false)
17
11
  return nil if file_path.start_with?('./')
18
12
 
19
13
  return FilePathTeamCache.get(file_path) if FilePathTeamCache.cached?(file_path)
@@ -31,7 +25,7 @@ module CodeOwnership
31
25
  end
32
26
 
33
27
  sig { params(files: T::Array[String], allow_raise: T::Boolean).returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
34
- def teams_for_files(files, allow_raise: false)
28
+ def self.teams_for_files(files, allow_raise: false)
35
29
  result = {}
36
30
 
37
31
  # Collect cached results and identify non-cached files
@@ -57,8 +51,8 @@ module CodeOwnership
57
51
  result
58
52
  end
59
53
 
60
- sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
61
- def for_class(klass)
54
+ sig { params(klass: T.nilable(T.any(T::Class[T.anything], T::Module[T.anything]))).returns(T.nilable(::CodeTeams::Team)) }
55
+ def self.for_class(klass)
62
56
  file_path = FilePathFinder.path_from_klass(klass)
63
57
  return nil if file_path.nil?
64
58
 
@@ -66,7 +60,7 @@ module CodeOwnership
66
60
  end
67
61
 
68
62
  sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
69
- def for_package(package)
63
+ def self.for_package(package)
70
64
  owner_name = package.raw_hash['owner'] || package.metadata['owner']
71
65
  return nil if owner_name.nil?
72
66
 
@@ -74,12 +68,12 @@ module CodeOwnership
74
68
  end
75
69
 
76
70
  sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
77
- def for_backtrace(backtrace, excluded_teams: [])
71
+ def self.for_backtrace(backtrace, excluded_teams: [])
78
72
  first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)&.first
79
73
  end
80
74
 
81
75
  sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
82
- def first_owned_file_for_backtrace(backtrace, excluded_teams: [])
76
+ def self.first_owned_file_for_backtrace(backtrace, excluded_teams: [])
83
77
  FilePathFinder.from_backtrace(backtrace).each do |file|
84
78
  team = for_file(file)
85
79
  if team && !excluded_teams.include?(team)
@@ -91,7 +85,7 @@ module CodeOwnership
91
85
  end
92
86
 
93
87
  sig { params(team_name: String, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
94
- def find_team!(team_name, allow_raise: false)
88
+ def self.find_team!(team_name, allow_raise: false)
95
89
  team = CodeTeams.find(team_name)
96
90
  if team.nil? && allow_raise
97
91
  raise(StandardError, "Could not find team with name: `#{team_name}`. Make sure the team is one of `#{CodeTeams.all.map(&:name).sort}`")
@@ -1,6 +1,6 @@
1
- # typed: false
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module CodeOwnership
5
- VERSION = '2.0.0-5'
5
+ VERSION = '2.1.1'
6
6
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  # typed: strict
4
3
 
5
- require 'set'
6
4
  require 'code_teams'
7
5
  require 'sorbet-runtime'
8
6
  require 'json'
@@ -26,17 +24,13 @@ if defined?(Packwerk)
26
24
  end
27
25
 
28
26
  module CodeOwnership
29
- module_function
30
-
31
27
  extend T::Sig
32
- extend T::Helpers
33
28
 
34
- requires_ancestor { Kernel }
35
29
  GlobsToOwningTeamMap = T.type_alias { T::Hash[String, CodeTeams::Team] }
36
30
 
37
31
  # Returns the version of the code_ownership gem and the codeowners-rs gem.
38
32
  sig { returns(T::Array[String]) }
39
- def version
33
+ def self.version
40
34
  ["code_ownership version: #{VERSION}",
41
35
  "codeowners-rs version: #{::RustCodeOwners.version}"]
42
36
  end
@@ -66,7 +60,7 @@ module CodeOwnership
66
60
  # # => raises exception if no owner found
67
61
  #
68
62
  sig { params(file: String, from_codeowners: T::Boolean, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
69
- def for_file(file, from_codeowners: true, allow_raise: false)
63
+ def self.for_file(file, from_codeowners: true, allow_raise: false)
70
64
  if from_codeowners
71
65
  teams_for_files_from_codeowners([file], allow_raise: allow_raise).values.first
72
66
  else
@@ -117,7 +111,7 @@ module CodeOwnership
117
111
  # @see #validate! for ensuring CODEOWNERS file is up-to-date
118
112
  #
119
113
  sig { params(files: T::Array[String], allow_raise: T::Boolean).returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
120
- def teams_for_files_from_codeowners(files, allow_raise: false)
114
+ def self.teams_for_files_from_codeowners(files, allow_raise: false)
121
115
  Private::TeamFinder.teams_for_files(files, allow_raise: allow_raise)
122
116
  end
123
117
 
@@ -161,12 +155,12 @@ module CodeOwnership
161
155
  # @see CLI#for_file for the command-line interface that uses this method
162
156
  #
163
157
  sig { params(file: String).returns(T.nilable(T::Hash[Symbol, String])) }
164
- def for_file_verbose(file)
158
+ def self.for_file_verbose(file)
165
159
  ::RustCodeOwners.for_file(file)
166
160
  end
167
161
 
168
162
  sig { params(team: T.any(CodeTeams::Team, String)).returns(T::Array[String]) }
169
- def for_team(team)
163
+ def self.for_team(team)
170
164
  team = T.must(CodeTeams.find(team)) if team.is_a?(String)
171
165
  ::RustCodeOwners.for_team(team.name)
172
166
  end
@@ -227,7 +221,7 @@ module CodeOwnership
227
221
  files: T.nilable(T::Array[String])
228
222
  ).void
229
223
  end
230
- def validate!(
224
+ def self.validate!(
231
225
  autocorrect: true,
232
226
  stage_changes: true,
233
227
  files: nil
@@ -239,27 +233,78 @@ module CodeOwnership
239
233
  end
240
234
  end
241
235
 
236
+ # Removes the file annotation (e.g., "# @team TeamName") from a file.
237
+ #
238
+ # This method removes the ownership annotation from the first line of a file,
239
+ # which is typically used to declare team ownership at the file level.
240
+ # The annotation can be in the form of:
241
+ # - Ruby comments: # @team TeamName
242
+ # - JavaScript/TypeScript comments: // @team TeamName
243
+ # - YAML comments: -# @team TeamName
244
+ #
245
+ # If the file does not have an annotation or the annotation doesn't match a valid team,
246
+ # this method does nothing.
247
+ #
248
+ # @param filename [String] The path to the file from which to remove the annotation.
249
+ # Can be relative or absolute.
250
+ #
251
+ # @return [void]
252
+ #
253
+ # @example Remove annotation from a Ruby file
254
+ # # Before: File contains "# @team Platform\nclass User; end"
255
+ # CodeOwnership.remove_file_annotation!('app/models/user.rb')
256
+ # # After: File contains "class User; end"
257
+ #
258
+ # @example Remove annotation from a JavaScript file
259
+ # # Before: File contains "// @team Frontend\nexport default function() {}"
260
+ # CodeOwnership.remove_file_annotation!('app/javascript/component.js')
261
+ # # After: File contains "export default function() {}"
262
+ #
263
+ # @note This method modifies the file in place.
264
+ # @note Leading newlines after the annotation are also removed to maintain clean formatting.
265
+ #
266
+ sig { params(filename: String).void }
267
+ def self.remove_file_annotation!(filename)
268
+ filepath = Pathname.new(filename)
269
+
270
+ begin
271
+ content = filepath.read
272
+ rescue Errno::EISDIR, Errno::ENOENT
273
+ # Ignore files that fail to read (directories, missing files, etc.)
274
+ return
275
+ end
276
+
277
+ # Remove the team annotation and any trailing newlines after it
278
+ team_pattern = %r{\A(?:#|//|-#) @team .*\n+}
279
+ new_content = content.sub(team_pattern, '')
280
+
281
+ filepath.write(new_content) if new_content != content
282
+ rescue ArgumentError => e
283
+ # Handle invalid byte sequences gracefully
284
+ raise unless e.message.include?('invalid byte sequence')
285
+ end
286
+
242
287
  # Given a backtrace from either `Exception#backtrace` or `caller`, find the
243
288
  # first line that corresponds to a file with assigned ownership
244
289
  sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
245
- def for_backtrace(backtrace, excluded_teams: [])
290
+ def self.for_backtrace(backtrace, excluded_teams: [])
246
291
  Private::TeamFinder.for_backtrace(backtrace, excluded_teams: excluded_teams)
247
292
  end
248
293
 
249
294
  # Given a backtrace from either `Exception#backtrace` or `caller`, find the
250
295
  # first owned file in it, useful for figuring out which file is being blamed.
251
296
  sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
252
- def first_owned_file_for_backtrace(backtrace, excluded_teams: [])
297
+ def self.first_owned_file_for_backtrace(backtrace, excluded_teams: [])
253
298
  Private::TeamFinder.first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)
254
299
  end
255
300
 
256
- sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
257
- def for_class(klass)
301
+ sig { params(klass: T.nilable(T.any(T::Class[T.anything], T::Module[T.anything]))).returns(T.nilable(::CodeTeams::Team)) }
302
+ def self.for_class(klass)
258
303
  Private::TeamFinder.for_class(klass)
259
304
  end
260
305
 
261
306
  sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
262
- def for_package(package)
307
+ def self.for_package(package)
263
308
  Private::TeamFinder.for_package(package)
264
309
  end
265
310
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: code_ownership
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.pre.5
4
+ version: 2.1.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Gusto Engineers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-15 00:00:00.000000000 Z
11
+ date: 2026-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: code_teams
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 0.5.11249
47
+ version: 0.6.12763
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 0.5.11249
54
+ version: 0.6.12763
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: debug
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -177,7 +177,9 @@ files:
177
177
  - bin/codeownership
178
178
  - lib/code_ownership.rb
179
179
  - lib/code_ownership/3.2/code_ownership.so
180
+ - lib/code_ownership/3.3/code_ownership.so
180
181
  - lib/code_ownership/3.4/code_ownership.so
182
+ - lib/code_ownership/4.0/code_ownership.so
181
183
  - lib/code_ownership/cli.rb
182
184
  - lib/code_ownership/private/file_path_finder.rb
183
185
  - lib/code_ownership/private/file_path_team_cache.rb
@@ -205,7 +207,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
205
207
  version: '3.2'
206
208
  - - "<"
207
209
  - !ruby/object:Gem::Version
208
- version: 3.5.dev
210
+ version: 4.1.dev
209
211
  required_rubygems_version: !ruby/object:Gem::Requirement
210
212
  requirements:
211
213
  - - ">="