code_ownership 2.0.0.pre.1-arm64-darwin → 2.0.0.pre.3-arm64-darwin

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: eaf0d23097ced4e1034524765fda4bf2b67a76217b8cf7d90929147cef6f52d1
4
- data.tar.gz: 70fc585836c0b596a2a6a9f75159eb03ffe7430dc226be65fc122fffed871237
3
+ metadata.gz: b2f959f149fbbace4157a6bcd12ac2d15f8a43d517fce8ea4d1898273059c4bc
4
+ data.tar.gz: 5c4d451d92e34a5f64fa4207ce9fe6099a512a80f3037e8c739be89fc6dca18b
5
5
  SHA512:
6
- metadata.gz: 5652011eef699489ad876bee325fd66547e26d4cdfd14db17ba1b8f938c50c8e1e312df748bdbfd56b4d65c37b755a1977ad84f437c80047c6e7a52b84152f7f
7
- data.tar.gz: b06751757a624cc8b1fbc6e8ddfa6a6fa9f56993cdfb223e5d1ddda6ba1cc94c03b8094302b9627f97390a7064dfd3d529d51447d47edce6bc8498065e26edb8
6
+ metadata.gz: 920fecf5b66a01c71c00f43dda6b613e215fc810e54fb1b6fd8c18eaf247266dc3f860676b2f7fce2da9ab2a0c4b2c5a416c83b96890a7cfba087d2308d50290
7
+ data.tar.gz: 14f964a70e0c750e826bffbd9cc84db851e0172abb2d062898b6923d17f0743e8ce44276d1f668e7dde81dce5a46098363c03c912924b6987be25e4255c53e4f
@@ -95,6 +95,10 @@ module CodeOwnership
95
95
  options[:json] = true
96
96
  end
97
97
 
98
+ opts.on('--verbose', 'Output verbose information') do
99
+ options[:verbose] = true
100
+ end
101
+
98
102
  opts.on('--help', 'Shows this prompt') do
99
103
  puts opts
100
104
  exit
@@ -107,24 +111,7 @@ module CodeOwnership
107
111
  raise "Please pass in one file. Use `#{EXECUTABLE} for_file --help` for more info"
108
112
  end
109
113
 
110
- team = CodeOwnership.for_file(files.first)
111
-
112
- team_name = team&.name || 'Unowned'
113
- team_yml = team&.config_yml || 'Unowned'
114
-
115
- if options[:json]
116
- json = {
117
- team_name: team_name,
118
- team_yml: team_yml
119
- }
120
-
121
- puts json.to_json
122
- else
123
- puts <<~MSG
124
- Team: #{team_name}
125
- Team YML: #{team_yml}
126
- MSG
127
- end
114
+ puts CodeOwnership::Private::ForFileOutputBuilder.build(file_path: files.first, json: !!options[:json], verbose: !!options[:verbose])
128
115
  end
129
116
 
130
117
  def self.for_team(argv)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+
5
+ module CodeOwnership
6
+ module Private
7
+ class ForFileOutputBuilder
8
+ extend T::Sig
9
+ private_class_method :new
10
+
11
+ sig { params(file_path: String, json: T::Boolean, verbose: T::Boolean).void }
12
+ def initialize(file_path:, json:, verbose:)
13
+ @file_path = file_path
14
+ @json = json
15
+ @verbose = verbose
16
+ end
17
+
18
+ sig { params(file_path: String, json: T::Boolean, verbose: T::Boolean).returns(String) }
19
+ def self.build(file_path:, json:, verbose:)
20
+ new(file_path: file_path, json: json, verbose: verbose).build
21
+ end
22
+
23
+ UNOWNED_OUTPUT = T.let(
24
+ {
25
+ team_name: 'Unowned',
26
+ team_yml: 'Unowned'
27
+ },
28
+ T::Hash[Symbol, T.untyped]
29
+ )
30
+
31
+ sig { returns(String) }
32
+ def build
33
+ result_hash = @verbose ? build_verbose : build_terse
34
+
35
+ return result_hash.to_json if @json
36
+
37
+ build_message_for(result_hash)
38
+ end
39
+
40
+ private
41
+
42
+ sig { returns(T::Hash[Symbol, T.untyped]) }
43
+ def build_verbose
44
+ result = CodeOwnership.for_file_verbose(@file_path)
45
+ return UNOWNED_OUTPUT if result.nil?
46
+
47
+ {
48
+ team_name: result[:team_name],
49
+ team_yml: result[:team_config_yml],
50
+ description: result[:reasons]
51
+ }
52
+ end
53
+
54
+ sig { returns(T::Hash[Symbol, T.untyped]) }
55
+ def build_terse
56
+ team = CodeOwnership.for_file(@file_path, from_codeowners: false, allow_raise: true)
57
+
58
+ if team.nil?
59
+ UNOWNED_OUTPUT
60
+ else
61
+ {
62
+ team_name: team.name,
63
+ team_yml: team.config_yml
64
+ }
65
+ end
66
+ end
67
+
68
+ sig { params(result_hash: T::Hash[Symbol, T.untyped]).returns(String) }
69
+ def build_message_for(result_hash)
70
+ messages = ["Team: #{result_hash[:team_name]}", "Team YML: #{result_hash[:team_yml]}"]
71
+ description_list = T.let(Array(result_hash[:description]), T::Array[String])
72
+ messages << build_description_message(description_list) unless description_list.empty?
73
+ messages.last << "\n"
74
+ messages.join("\n")
75
+ end
76
+
77
+ sig { params(reasons: T::Array[String]).returns(String) }
78
+ def build_description_message(reasons)
79
+ "Description:\n- #{reasons.join("\n-")}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -12,8 +12,8 @@ module CodeOwnership
12
12
 
13
13
  requires_ancestor { Kernel }
14
14
 
15
- sig { params(file_path: String).returns(T.nilable(CodeTeams::Team)) }
16
- def for_file(file_path)
15
+ sig { params(file_path: String, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
16
+ def for_file(file_path, allow_raise: false)
17
17
  return nil if file_path.start_with?('./')
18
18
 
19
19
  return FilePathTeamCache.get(file_path) if FilePathTeamCache.cached?(file_path)
@@ -24,12 +24,22 @@ module CodeOwnership
24
24
  if result[:team_name].nil?
25
25
  FilePathTeamCache.set(file_path, nil)
26
26
  else
27
- FilePathTeamCache.set(file_path, T.let(find_team!(T.must(result[:team_name])), T.nilable(CodeTeams::Team)))
27
+ FilePathTeamCache.set(file_path, T.let(find_team!(T.must(result[:team_name]), allow_raise: allow_raise), T.nilable(CodeTeams::Team)))
28
28
  end
29
29
 
30
30
  FilePathTeamCache.get(file_path)
31
31
  end
32
32
 
33
+ 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)
35
+ ::RustCodeOwners.teams_for_files(files).each_with_object({}) do |path_team, hash|
36
+ file_path, team = path_team
37
+ found_team = team ? find_team!(team[:team_name], allow_raise: allow_raise) : nil
38
+ FilePathTeamCache.set(file_path, found_team)
39
+ hash[file_path] = found_team
40
+ end
41
+ end
42
+
33
43
  sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
34
44
  def for_class(klass)
35
45
  file_path = FilePathFinder.path_from_klass(klass)
@@ -43,7 +53,7 @@ module CodeOwnership
43
53
  owner_name = package.raw_hash['owner'] || package.metadata['owner']
44
54
  return nil if owner_name.nil?
45
55
 
46
- find_team!(owner_name)
56
+ find_team!(owner_name, allow_raise: true)
47
57
  end
48
58
 
49
59
  sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
@@ -63,10 +73,14 @@ module CodeOwnership
63
73
  nil
64
74
  end
65
75
 
66
- sig { params(team_name: String).returns(CodeTeams::Team) }
67
- def find_team!(team_name)
68
- CodeTeams.find(team_name) ||
76
+ sig { params(team_name: String, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
77
+ def find_team!(team_name, allow_raise: false)
78
+ team = CodeTeams.find(team_name)
79
+ if team.nil? && allow_raise
69
80
  raise(StandardError, "Could not find team with name: `#{team_name}`. Make sure the team is one of `#{CodeTeams.all.map(&:name).sort}`")
81
+ end
82
+
83
+ team
70
84
  end
71
85
 
72
86
  private_class_method(:find_team!)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CodeOwnership
4
- VERSION = '2.0.0-1'
4
+ VERSION = '2.0.0-3'
5
5
  end
@@ -11,6 +11,7 @@ require 'code_ownership/version'
11
11
  require 'code_ownership/private/file_path_finder'
12
12
  require 'code_ownership/private/file_path_team_cache'
13
13
  require 'code_ownership/private/team_finder'
14
+ require 'code_ownership/private/for_file_output_builder'
14
15
  require 'code_ownership/cli'
15
16
 
16
17
  begin
@@ -33,15 +34,135 @@ module CodeOwnership
33
34
  requires_ancestor { Kernel }
34
35
  GlobsToOwningTeamMap = T.type_alias { T::Hash[String, CodeTeams::Team] }
35
36
 
37
+ # Returns the version of the code_ownership gem and the codeowners-rs gem.
36
38
  sig { returns(T::Array[String]) }
37
39
  def version
38
40
  ["code_ownership version: #{VERSION}",
39
41
  "codeowners-rs version: #{::RustCodeOwners.version}"]
40
42
  end
41
43
 
42
- sig { params(file: String).returns(T.nilable(CodeTeams::Team)) }
43
- def for_file(file)
44
- Private::TeamFinder.for_file(file)
44
+ # Returns the owning team for a given file path.
45
+ #
46
+ # @param file [String] The path to the file to find ownership for. Can be relative or absolute.
47
+ # @param from_codeowners [Boolean] (default: true) When true, uses CODEOWNERS file to determine ownership.
48
+ # When false, uses alternative team finding strategies (e.g., package ownership).
49
+ # from_codeowners true is faster because it simply matches the provided file to the generate CODEOWNERS file. This is a safe option when you can trust the CODEOWNERS file to be up to date.
50
+ # @param allow_raise [Boolean] (default: false) When true, raises an exception if ownership cannot be determined.
51
+ # When false, returns nil for files without ownership.
52
+ #
53
+ # @return [CodeTeams::Team, nil] The team that owns the file, or nil if no owner is found
54
+ # (unless allow_raise is true, in which case an exception is raised).
55
+ #
56
+ # @example Find owner for a file using CODEOWNERS
57
+ # team = CodeOwnership.for_file('app/models/user.rb')
58
+ # # => #<CodeTeams::Team:0x... @name="platform">
59
+ #
60
+ # @example Find owner without using CODEOWNERS
61
+ # team = CodeOwnership.for_file('app/models/user.rb', from_codeowners: false)
62
+ # # => #<CodeTeams::Team:0x... @name="platform">
63
+ #
64
+ # @example Raise if no owner is found
65
+ # team = CodeOwnership.for_file('unknown_file.rb', allow_raise: true)
66
+ # # => raises exception if no owner found
67
+ #
68
+ 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)
70
+ if from_codeowners
71
+ teams_for_files_from_codeowners([file], allow_raise: allow_raise).values.first
72
+ else
73
+ Private::TeamFinder.for_file(file, allow_raise: allow_raise)
74
+ end
75
+ end
76
+
77
+ # Returns the owning teams for multiple file paths using the CODEOWNERS file.
78
+ #
79
+ # This method efficiently determines ownership for multiple files in a single operation
80
+ # by leveraging the generated CODEOWNERS file. It's more performant than calling
81
+ # `for_file` multiple times when you need to check ownership for many files.
82
+ #
83
+ # @param files [Array<String>] An array of file paths to find ownership for.
84
+ # Paths can be relative to the project root or absolute.
85
+ # @param allow_raise [Boolean] (default: false) When true, raises an exception if a team
86
+ # name in CODEOWNERS cannot be resolved to an actual team.
87
+ # When false, returns nil for files with unresolvable teams.
88
+ #
89
+ # @return [T::Hash[String, T.nilable(CodeTeams::Team)]] A hash mapping each file path to its
90
+ # owning team. Files without ownership
91
+ # or with unresolvable teams will map to nil.
92
+ #
93
+ # @example Get owners for multiple files
94
+ # files = ['app/models/user.rb', 'app/controllers/users_controller.rb', 'config/routes.rb']
95
+ # owners = CodeOwnership.teams_for_files_from_codeowners(files)
96
+ # # => {
97
+ # # 'app/models/user.rb' => #<CodeTeams::Team:0x... @name="platform">,
98
+ # # 'app/controllers/users_controller.rb' => #<CodeTeams::Team:0x... @name="platform">,
99
+ # # 'config/routes.rb' => #<CodeTeams::Team:0x... @name="infrastructure">
100
+ # # }
101
+ #
102
+ # @example Handle files without owners
103
+ # files = ['owned_file.rb', 'unowned_file.txt']
104
+ # owners = CodeOwnership.teams_for_files_from_codeowners(files)
105
+ # # => {
106
+ # # 'owned_file.rb' => #<CodeTeams::Team:0x... @name="backend">,
107
+ # # 'unowned_file.txt' => nil
108
+ # # }
109
+ #
110
+ # @note This method uses caching internally for performance. The cache is populated
111
+ # as files are processed and reused for subsequent lookups.
112
+ #
113
+ # @note This method relies on the CODEOWNERS file being up-to-date. Run
114
+ # `CodeOwnership.validate!` to ensure the CODEOWNERS file is current.
115
+ #
116
+ # @see #for_file for single file ownership lookup
117
+ # @see #validate! for ensuring CODEOWNERS file is up-to-date
118
+ #
119
+ 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)
121
+ Private::TeamFinder.teams_for_files(files, allow_raise: allow_raise)
122
+ end
123
+
124
+ # Returns detailed ownership information for a given file path.
125
+ #
126
+ # This method provides verbose ownership details including the team name,
127
+ # team configuration file path, and the reasons/sources for ownership assignment.
128
+ # It's particularly useful for debugging ownership assignments and understanding
129
+ # why a file is owned by a specific team.
130
+ #
131
+ # @param file [String] The path to the file to find ownership for. Can be relative or absolute.
132
+ #
133
+ # @return [T::Hash[Symbol, String], nil] A hash containing detailed ownership information,
134
+ # or nil if no owner is found.
135
+ #
136
+ # The returned hash contains the following keys when an owner is found:
137
+ # - :team_name [String] - The name of the owning team
138
+ # - :team_config_yml [String] - Path to the team's configuration YAML file
139
+ # - :reasons [Array<String>] - List of reasons/sources explaining why this team owns the file
140
+ # (e.g., "CODEOWNERS pattern: /app/models/**", "Package ownership")
141
+ #
142
+ # @example Get verbose ownership details
143
+ # details = CodeOwnership.for_file_verbose('app/models/user.rb')
144
+ # # => {
145
+ # # team_name: "platform",
146
+ # # team_config_yml: "config/teams/platform.yml",
147
+ # # reasons: ["Matched pattern '/app/models/**' in CODEOWNERS"]
148
+ # # }
149
+ #
150
+ # @example Handle unowned files
151
+ # details = CodeOwnership.for_file_verbose('unowned_file.txt')
152
+ # # => nil
153
+ #
154
+ # @note This method is primarily used by the CLI tool when the --verbose flag is provided,
155
+ # allowing users to understand the ownership assignment logic.
156
+ #
157
+ # @note Unlike `for_file`, this method always uses the CODEOWNERS file and other ownership
158
+ # sources to determine ownership, providing complete context about the ownership decision.
159
+ #
160
+ # @see #for_file for a simpler ownership lookup that returns just the team
161
+ # @see CLI#for_file for the command-line interface that uses this method
162
+ #
163
+ sig { params(file: String).returns(T.nilable(T::Hash[Symbol, String])) }
164
+ def for_file_verbose(file)
165
+ ::RustCodeOwners.for_file(file)
45
166
  end
46
167
 
47
168
  sig { params(team: T.any(CodeTeams::Team, String)).returns(T::Array[String]) }
@@ -50,9 +171,55 @@ module CodeOwnership
50
171
  ::RustCodeOwners.for_team(team.name)
51
172
  end
52
173
 
53
- class InvalidCodeOwnershipConfigurationError < StandardError
54
- end
55
-
174
+ # Validates code ownership configuration and optionally corrects issues.
175
+ #
176
+ # This method performs comprehensive validation of the code ownership setup, ensuring:
177
+ # 1. Only one ownership mechanism is defined per file (no conflicts between annotations, packages, or globs)
178
+ # 2. All referenced teams are valid (exist in CodeTeams configuration)
179
+ # 3. All files have ownership (unless explicitly listed in unowned_globs)
180
+ # 4. The .github/CODEOWNERS file is up-to-date and properly formatted
181
+ #
182
+ # When autocorrect is enabled, the method will automatically:
183
+ # - Generate or update the CODEOWNERS file based on current ownership rules
184
+ # - Fix any formatting issues in the CODEOWNERS file
185
+ # - Stage the corrected CODEOWNERS file (unless stage_changes is false)
186
+ #
187
+ # @param autocorrect [Boolean] Whether to automatically fix correctable issues (default: true)
188
+ # When true, regenerates and updates the CODEOWNERS file
189
+ # When false, only validates without making changes
190
+ #
191
+ # @param stage_changes [Boolean] Whether to stage the CODEOWNERS file after autocorrection (default: true)
192
+ # Only applies when autocorrect is true
193
+ # When false, changes are written but not staged with git
194
+ #
195
+ # @param files [Array<String>, nil] Ignored. This is a legacy parameter that is no longer used.
196
+ #
197
+ # @return [void]
198
+ #
199
+ # @raise [RuntimeError] Raises an error if validation fails with details about:
200
+ # - Files with conflicting ownership definitions
201
+ # - References to non-existent teams
202
+ # - Files without ownership (not in unowned_globs)
203
+ # - CODEOWNERS file inconsistencies
204
+ #
205
+ # @example Basic validation with autocorrection
206
+ # CodeOwnership.validate!
207
+ # # Validates all files and auto-corrects/stages CODEOWNERS if needed
208
+ #
209
+ # @example Validation without making changes
210
+ # CodeOwnership.validate!(autocorrect: false)
211
+ # # Only checks for issues without updating CODEOWNERS
212
+ #
213
+ # @example Validate and fix but don't stage changes
214
+ # CodeOwnership.validate!(autocorrect: true, stage_changes: false)
215
+ # # Fixes CODEOWNERS but doesn't stage it with git
216
+ #
217
+ # @note This method is called by the CLI command: bin/codeownership validate
218
+ # @note The validation can be disabled for CODEOWNERS by setting skip_codeowners_validation: true in config/code_ownership.yml
219
+ #
220
+ # @see CLI.validate! for the command-line interface
221
+ # @see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners for CODEOWNERS format
222
+ #
56
223
  sig do
57
224
  params(
58
225
  autocorrect: T::Boolean,
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.1
4
+ version: 2.0.0.pre.3
5
5
  platform: arm64-darwin
6
6
  authors:
7
7
  - Gusto Engineers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-14 00:00:00.000000000 Z
11
+ date: 2025-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: code_teams
@@ -181,6 +181,7 @@ files:
181
181
  - lib/code_ownership/cli.rb
182
182
  - lib/code_ownership/private/file_path_finder.rb
183
183
  - lib/code_ownership/private/file_path_team_cache.rb
184
+ - lib/code_ownership/private/for_file_output_builder.rb
184
185
  - lib/code_ownership/private/permit_pack_owner_top_level_key.rb
185
186
  - lib/code_ownership/private/team_finder.rb
186
187
  - lib/code_ownership/version.rb