code_ownership 2.1.1-aarch64-mingw-ucrt

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 440e5064df89e8f6030e33c5b990262162d66e9395afa404836c88c1a6a74593
4
+ data.tar.gz: '08d7c9d42d3bb443d764f719910c7b7d31e1587d30c489f89ab36cde6d6d15da'
5
+ SHA512:
6
+ metadata.gz: ffa68ab47c952d5a8d8f7625a9996392d64aced0bfa00a36db1747b0ee4ec7606b09dc68272ef6ff7a1ce31f294184a87e03317a89559272676ed28a11cadfb5
7
+ data.tar.gz: 9dd957f2c966baef82e9af05ffe84f5c3118758f824c14379e2e471393de491b7f192dd1c5854fce9cdd78391e931f50eb8ab5eab094a3c541eee4e356fd0f28
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Gusto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # CodeOwnership
2
+
3
+ This gem helps engineering teams declare ownership of code. This gem works best in large, usually monolithic code bases where many teams work together.
4
+
5
+ Check out [`lib/code_ownership.rb`](https://github.com/rubyatscale/code_ownership/blob/main/lib/code_ownership.rb) to see the public API.
6
+
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
+
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
+
11
+ ## Getting started
12
+
13
+ To get started there's a few things you should do.
14
+
15
+ 1. Create a `config/code_ownership.yml` file and declare where your files live. Here's a sample to start with:
16
+
17
+ ```yml
18
+ owned_globs:
19
+ - '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
20
+ js_package_paths: []
21
+ unowned_globs:
22
+ - db/**/*
23
+ - app/services/some_file1.rb
24
+ - app/services/some_file2.rb
25
+ - frontend/javascripts/**/__generated__/**/*
26
+ ```
27
+
28
+ 2. Declare some teams. Here's an example, that would live at `config/teams/operations.yml`:
29
+
30
+ ```yml
31
+ name: Operations
32
+ github:
33
+ team: '@my-org/operations-team'
34
+ ```
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.
38
+
39
+ ## Usage: Declaring Ownership
40
+
41
+ There are five ways to declare code ownership using this gem:
42
+
43
+ ### Directory-Based Ownership
44
+
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
+
47
+ ```
48
+ Team
49
+ ```
50
+
51
+ ### File-Annotation Based Ownership
52
+
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
+
55
+ ```ruby
56
+ # @team MyTeam
57
+ ```
58
+
59
+ ### Package-Based Ownership
60
+
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
+
63
+ ```yml
64
+ enforce_dependency: true
65
+ enforce_privacy: true
66
+ metadata:
67
+ owner: Team
68
+ ```
69
+
70
+ You can also define `owner` as a top-level key, e.g.
71
+
72
+ ```yml
73
+ enforce_dependency: true
74
+ enforce_privacy: true
75
+ owner: Team
76
+ ```
77
+
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.
79
+
80
+ ### Glob-Based Ownership
81
+
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
+
84
+ ```yml
85
+ name: My Team
86
+ owned_globs:
87
+ - app/services/stuff_belonging_to_my_team/**/**
88
+ - app/controllers/other_stuff_belonging_to_my_team/**/**
89
+ unowned_globs:
90
+ - app/controllers/other_stuff_belonging_to_my_team/that_one_weird_dir_we_dont_own/*
91
+ ```
92
+
93
+ ### Javascript Package Ownership
94
+
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:
96
+
97
+ ```json
98
+ {
99
+ // other keys
100
+ "metadata": {
101
+ "owner": "My Team"
102
+ }
103
+ // other keys
104
+ }
105
+ ```
106
+
107
+ You can also tell `code_ownership` where to find JS packages in the configuration, like this:
108
+
109
+ ```yml
110
+ js_package_paths:
111
+ - frontend/javascripts/packages/*
112
+ - frontend/other_location_for_packages/*
113
+ ```
114
+
115
+ This defaults `**/`, which makes it look for `package.json` files across your application.
116
+
117
+ > [!NOTE]
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
+
120
+ ```yml
121
+ js_package_paths: []
122
+ ```
123
+
124
+ ## Usage: Reading CodeOwnership
125
+
126
+ ### `for_file`
127
+
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.
129
+
130
+ ```ruby
131
+ CodeOwnership.for_file('path/to/file/relative/to/application/root.rb')
132
+ ```
133
+
134
+ Contributor note: If you are making updates to this method or the methods getting used here, please benchmark the performance of the new implementation against the current for both `for_files` and `for_file` (with 1, 100, 1000 files).
135
+
136
+ See `code_ownership_spec.rb` for examples.
137
+
138
+ ### `for_backtrace`
139
+
140
+ `CodeOwnership.for_backtrace` can be given a backtrace and will either return `nil`, or a `CodeTeams::Team`.
141
+
142
+ ```ruby
143
+ CodeOwnership.for_backtrace(exception.backtrace)
144
+ ```
145
+
146
+ This will go through the backtrace, and return the first found owner of the files associated with frames within the backtrace.
147
+
148
+ See `code_ownership_spec.rb` for an example.
149
+
150
+ ### `for_class`
151
+
152
+ `CodeOwnership.for_class` can be given a class and will either return `nil`, or a `CodeTeams::Team`.
153
+
154
+ ```ruby
155
+ CodeOwnership.for_class(MyClass)
156
+ ```
157
+
158
+ Under the hood, this finds the file where the class is defined and returns the owner of that file.
159
+
160
+ See `code_ownership_spec.rb` for an example.
161
+
162
+ ### `for_team`
163
+
164
+ `CodeOwnership.for_team` can be used to generate an ownership report for a team.
165
+
166
+ ```ruby
167
+ CodeOwnership.for_team('My Team')
168
+ ```
169
+
170
+ You can shovel this into a markdown file for easy viewing using the CLI:
171
+
172
+ ```
173
+ codeownership for_team 'My Team' > tmp/ownership_report.md
174
+ ```
175
+
176
+ ## Usage: Generating a `CODEOWNERS` file
177
+
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.
179
+
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`.
181
+
182
+ ## Proper Configuration & Validation
183
+
184
+ CodeOwnership comes with a validation function to ensure the following things are true:
185
+
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`.
190
+
191
+ CodeOwnership also allows you to specify which globs and file extensions should be considered ownable.
192
+
193
+ Here is an example `config/code_ownership.yml`.
194
+
195
+ ```yml
196
+ owned_globs:
197
+ - '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
198
+ unowned_globs:
199
+ - db/**/*
200
+ - app/services/some_file1.rb
201
+ - app/services/some_file2.rb
202
+ - frontend/javascripts/**/__generated__/**/*
203
+ ```
204
+
205
+ You can call the validation function with the Ruby API
206
+
207
+ ```ruby
208
+ CodeOwnership.validate!
209
+ ```
210
+
211
+ or the CLI
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
222
+ ```
223
+
224
+ ## Development
225
+
226
+ Please add to `CHANGELOG.md` and this `README.md` when you make make changes.
227
+
228
+ ## Running specs
229
+
230
+ ```sh
231
+ bundle install
232
+ bundle exec rake
233
+ ```
234
+
235
+ ## Creating a new release
236
+
237
+ Simply [create a new release](https://github.com/rubyatscale/code_ownership/releases/new) with github. The release tag must match the gem version
data/bin/codeownership ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: strict
3
+
4
+ require 'code_ownership'
5
+ CodeOwnership::Cli.run!(ARGV)
@@ -0,0 +1,149 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'pathname'
6
+ require 'fileutils'
7
+
8
+ module CodeOwnership
9
+ class Cli
10
+ EXECUTABLE = 'bin/codeownership'
11
+
12
+ def self.run!(argv)
13
+ command = argv.shift
14
+ if command == 'validate'
15
+ validate!(argv)
16
+ elsif command == 'for_file'
17
+ for_file(argv)
18
+ elsif command == 'for_team'
19
+ for_team(argv)
20
+ elsif command == 'version'
21
+ version
22
+ elsif [nil, 'help'].include?(command)
23
+ puts <<~USAGE
24
+ Usage: #{EXECUTABLE} <subcommand>
25
+
26
+ Subcommands:
27
+ validate - run all validations
28
+ for_file - find code ownership for a single file
29
+ for_team - find code ownership information for a team
30
+ help - display help information about code_ownership
31
+ USAGE
32
+ else
33
+ puts "'#{command}' is not a code_ownership command. See `#{EXECUTABLE} help`."
34
+ end
35
+ end
36
+
37
+ def self.validate!(argv)
38
+ options = {}
39
+
40
+ parser = OptionParser.new do |opts|
41
+ opts.banner = "Usage: #{EXECUTABLE} validate [options] [files...]"
42
+
43
+ opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .github/CODEOWNERS file') do
44
+ options[:skip_autocorrect] = true
45
+ end
46
+
47
+ opts.on('-d', '--diff', 'Only run validations with staged files') do
48
+ options[:diff] = true
49
+ end
50
+
51
+ opts.on('-s', '--skip-stage', 'Skips staging the CODEOWNERS file') do
52
+ options[:skip_stage] = true
53
+ end
54
+
55
+ opts.on('--help', 'Shows this prompt') do
56
+ puts opts
57
+ exit
58
+ end
59
+ end
60
+ args = parser.order!(argv)
61
+ parser.parse!(args)
62
+
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
74
+ ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
75
+ File.exist?(file)
76
+ end
77
+ else
78
+ # No files specified, validate all
79
+ nil
80
+ end
81
+
82
+ CodeOwnership.validate!(
83
+ files: files,
84
+ autocorrect: !options[:skip_autocorrect],
85
+ stage_changes: !options[:skip_stage]
86
+ )
87
+ end
88
+
89
+ def self.version
90
+ puts CodeOwnership.version.join("\n")
91
+ end
92
+
93
+ # For now, this just returns team ownership
94
+ # Later, this could also return code ownership errors about that file.
95
+ def self.for_file(argv)
96
+ options = {}
97
+
98
+ # Long-term, we probably want to use something like `thor` so we don't have to implement logic
99
+ # like this. In the short-term, this is a simple way for us to use the built-in OptionParser
100
+ # while having an ergonomic CLI.
101
+ files = argv.reject { |arg| arg.start_with?('--') }
102
+
103
+ parser = OptionParser.new do |opts|
104
+ opts.banner = "Usage: #{EXECUTABLE} for_file [options]"
105
+
106
+ opts.on('--json', 'Output as JSON') do
107
+ options[:json] = true
108
+ end
109
+
110
+ opts.on('--verbose', 'Output verbose information') do
111
+ options[:verbose] = true
112
+ end
113
+
114
+ opts.on('--help', 'Shows this prompt') do
115
+ puts opts
116
+ exit
117
+ end
118
+ end
119
+ args = parser.order!(argv)
120
+ parser.parse!(args)
121
+
122
+ if files.count != 1
123
+ raise "Please pass in one file. Use `#{EXECUTABLE} for_file --help` for more info"
124
+ end
125
+
126
+ puts CodeOwnership::Private::ForFileOutputBuilder.build(file_path: files.first, json: !!options[:json], verbose: !!options[:verbose])
127
+ end
128
+
129
+ def self.for_team(argv)
130
+ parser = OptionParser.new do |opts|
131
+ opts.banner = "Usage: #{EXECUTABLE} for_team 'Team Name'"
132
+
133
+ opts.on('--help', 'Shows this prompt') do
134
+ puts opts
135
+ exit
136
+ end
137
+ end
138
+ teams = argv.reject { |arg| arg.start_with?('--') }
139
+ args = parser.order!(argv)
140
+ parser.parse!(args)
141
+
142
+ if teams.count != 1
143
+ raise "Please pass in one team. Use `#{EXECUTABLE} for_team --help` for more info"
144
+ end
145
+
146
+ puts CodeOwnership.for_team(teams.first).join("\n")
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module CodeOwnership
5
+ module Private
6
+ module FilePathFinder
7
+ extend T::Sig
8
+
9
+ # Returns a string version of the relative path to a Rails constant,
10
+ # or nil if it can't find anything
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)
13
+ if klass
14
+ path = Object.const_source_location(klass.to_s)&.first
15
+ (path && Pathname.new(path).relative_path_from(Pathname.pwd).to_s) || nil
16
+ end
17
+ rescue NameError
18
+ nil
19
+ end
20
+
21
+ sig { params(backtrace: T.nilable(T::Array[String])).returns(T::Enumerable[String]) }
22
+ def self.from_backtrace(backtrace)
23
+ return [] unless backtrace
24
+
25
+ # The pattern for a backtrace hasn't changed in forever and is considered
26
+ # stable: https://github.com/ruby/ruby/blob/trunk/vm_backtrace.c#L303-L317
27
+ #
28
+ # This pattern matches a line like the following:
29
+ #
30
+ # ./app/controllers/some_controller.rb:43:in `block (3 levels) in create'
31
+ #
32
+ backtrace_line = if RUBY_VERSION >= '3.4.0'
33
+ %r{\A(#{Pathname.pwd}/|\./)?
34
+ (?<file>.+) # Matches 'app/controllers/some_controller.rb'
35
+ :
36
+ (?<line>\d+) # Matches '43'
37
+ :in\s
38
+ '(?<function>.*)' # Matches "`block (3 levels) in create'"
39
+ \z}x
40
+ else
41
+ %r{\A(#{Pathname.pwd}/|\./)?
42
+ (?<file>.+) # Matches 'app/controllers/some_controller.rb'
43
+ :
44
+ (?<line>\d+) # Matches '43'
45
+ :in\s
46
+ `(?<function>.*)' # Matches "`block (3 levels) in create'"
47
+ \z}x
48
+ end
49
+
50
+ backtrace.lazy.filter_map do |line|
51
+ match = line.match(backtrace_line)
52
+ next unless match
53
+
54
+ T.must(match[:file])
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module CodeOwnership
5
+ module Private
6
+ module FilePathTeamCache
7
+ extend T::Sig
8
+
9
+ sig { params(file_path: String).returns(T.nilable(CodeTeams::Team)) }
10
+ def self.get(file_path)
11
+ cache[file_path]
12
+ end
13
+
14
+ sig { params(file_path: String, team: T.nilable(CodeTeams::Team)).void }
15
+ def self.set(file_path, team)
16
+ cache[file_path] = team
17
+ end
18
+
19
+ sig { params(file_path: String).returns(T::Boolean) }
20
+ def self.cached?(file_path)
21
+ cache.key?(file_path)
22
+ end
23
+
24
+ sig { void }
25
+ def self.bust_cache!
26
+ @cache = nil
27
+ end
28
+
29
+ sig { returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
30
+ def self.cache
31
+ @cache ||= T.let(@cache,
32
+ T.nilable(T::Hash[String, T.nilable(CodeTeams::Team)]))
33
+ @cache ||= {}
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module CodeOwnership
5
+ module Private
6
+ class ForFileOutputBuilder
7
+ extend T::Sig
8
+
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
@@ -0,0 +1,23 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'packwerk'
5
+
6
+ module CodeOwnership
7
+ module Private
8
+ class PackOwnershipValidator
9
+ extend T::Sig
10
+ include Packwerk::Validator
11
+
12
+ sig { override.params(package_set: Packwerk::PackageSet, configuration: Packwerk::Configuration).returns(Result) }
13
+ def call(package_set, configuration)
14
+ Result.new(ok: true)
15
+ end
16
+
17
+ sig { override.returns(T::Array[String]) }
18
+ def permitted_keys
19
+ %w[owner]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module CodeOwnership
5
+ module Private
6
+ module TeamFinder
7
+ extend T::Sig
8
+
9
+ sig { params(file_path: String, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
10
+ def self.for_file(file_path, allow_raise: false)
11
+ return nil if file_path.start_with?('./')
12
+
13
+ return FilePathTeamCache.get(file_path) if FilePathTeamCache.cached?(file_path)
14
+
15
+ result = T.let(RustCodeOwners.for_file(file_path), T.nilable(T::Hash[Symbol, String]))
16
+ return if result.nil?
17
+
18
+ if result[:team_name].nil?
19
+ FilePathTeamCache.set(file_path, nil)
20
+ else
21
+ FilePathTeamCache.set(file_path, T.let(find_team!(T.must(result[:team_name]), allow_raise: allow_raise), T.nilable(CodeTeams::Team)))
22
+ end
23
+
24
+ FilePathTeamCache.get(file_path)
25
+ end
26
+
27
+ sig { params(files: T::Array[String], allow_raise: T::Boolean).returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
28
+ def self.teams_for_files(files, allow_raise: false)
29
+ result = {}
30
+
31
+ # Collect cached results and identify non-cached files
32
+ not_cached_files = []
33
+ files.each do |file_path|
34
+ if FilePathTeamCache.cached?(file_path)
35
+ result[file_path] = FilePathTeamCache.get(file_path)
36
+ else
37
+ not_cached_files << file_path
38
+ end
39
+ end
40
+
41
+ return result if not_cached_files.empty?
42
+
43
+ # Process non-cached files
44
+ ::RustCodeOwners.teams_for_files(not_cached_files).each do |path_team|
45
+ file_path, team = path_team
46
+ found_team = team ? find_team!(team[:team_name], allow_raise: allow_raise) : nil
47
+ FilePathTeamCache.set(file_path, found_team)
48
+ result[file_path] = found_team
49
+ end
50
+
51
+ result
52
+ end
53
+
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)
56
+ file_path = FilePathFinder.path_from_klass(klass)
57
+ return nil if file_path.nil?
58
+
59
+ for_file(file_path)
60
+ end
61
+
62
+ sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
63
+ def self.for_package(package)
64
+ owner_name = package.raw_hash['owner'] || package.metadata['owner']
65
+ return nil if owner_name.nil?
66
+
67
+ find_team!(owner_name, allow_raise: true)
68
+ end
69
+
70
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
71
+ def self.for_backtrace(backtrace, excluded_teams: [])
72
+ first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)&.first
73
+ end
74
+
75
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
76
+ def self.first_owned_file_for_backtrace(backtrace, excluded_teams: [])
77
+ FilePathFinder.from_backtrace(backtrace).each do |file|
78
+ team = for_file(file)
79
+ if team && !excluded_teams.include?(team)
80
+ return [team, file]
81
+ end
82
+ end
83
+
84
+ nil
85
+ end
86
+
87
+ sig { params(team_name: String, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
88
+ def self.find_team!(team_name, allow_raise: false)
89
+ team = CodeTeams.find(team_name)
90
+ if team.nil? && allow_raise
91
+ raise(StandardError, "Could not find team with name: `#{team_name}`. Make sure the team is one of `#{CodeTeams.all.map(&:name).sort}`")
92
+ end
93
+
94
+ team
95
+ end
96
+
97
+ private_class_method(:find_team!)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CodeOwnership
5
+ VERSION = '2.1.1'
6
+ end
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ require 'code_teams'
5
+ require 'sorbet-runtime'
6
+ require 'json'
7
+ require 'packs-specification'
8
+ require 'code_ownership/version'
9
+ require 'code_ownership/private/file_path_finder'
10
+ require 'code_ownership/private/file_path_team_cache'
11
+ require 'code_ownership/private/team_finder'
12
+ require 'code_ownership/private/for_file_output_builder'
13
+ require 'code_ownership/cli'
14
+
15
+ begin
16
+ RUBY_VERSION =~ /(\d+\.\d+)/
17
+ require "code_ownership/#{Regexp.last_match(1)}/code_ownership"
18
+ rescue LoadError
19
+ require 'code_ownership/code_ownership'
20
+ end
21
+
22
+ if defined?(Packwerk)
23
+ require 'code_ownership/private/permit_pack_owner_top_level_key'
24
+ end
25
+
26
+ module CodeOwnership
27
+ extend T::Sig
28
+
29
+ GlobsToOwningTeamMap = T.type_alias { T::Hash[String, CodeTeams::Team] }
30
+
31
+ # Returns the version of the code_ownership gem and the codeowners-rs gem.
32
+ sig { returns(T::Array[String]) }
33
+ def self.version
34
+ ["code_ownership version: #{VERSION}",
35
+ "codeowners-rs version: #{::RustCodeOwners.version}"]
36
+ end
37
+
38
+ # Returns the owning team for a given file path.
39
+ #
40
+ # @param file [String] The path to the file to find ownership for. Can be relative or absolute.
41
+ # @param from_codeowners [Boolean] (default: true) When true, uses CODEOWNERS file to determine ownership.
42
+ # When false, uses alternative team finding strategies (e.g., package ownership).
43
+ # 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.
44
+ # @param allow_raise [Boolean] (default: false) When true, raises an exception if ownership cannot be determined.
45
+ # When false, returns nil for files without ownership.
46
+ #
47
+ # @return [CodeTeams::Team, nil] The team that owns the file, or nil if no owner is found
48
+ # (unless allow_raise is true, in which case an exception is raised).
49
+ #
50
+ # @example Find owner for a file using CODEOWNERS
51
+ # team = CodeOwnership.for_file('app/models/user.rb')
52
+ # # => #<CodeTeams::Team:0x... @name="platform">
53
+ #
54
+ # @example Find owner without using CODEOWNERS
55
+ # team = CodeOwnership.for_file('app/models/user.rb', from_codeowners: false)
56
+ # # => #<CodeTeams::Team:0x... @name="platform">
57
+ #
58
+ # @example Raise if no owner is found
59
+ # team = CodeOwnership.for_file('unknown_file.rb', allow_raise: true)
60
+ # # => raises exception if no owner found
61
+ #
62
+ sig { params(file: String, from_codeowners: T::Boolean, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
63
+ def self.for_file(file, from_codeowners: true, allow_raise: false)
64
+ if from_codeowners
65
+ teams_for_files_from_codeowners([file], allow_raise: allow_raise).values.first
66
+ else
67
+ Private::TeamFinder.for_file(file, allow_raise: allow_raise)
68
+ end
69
+ end
70
+
71
+ # Returns the owning teams for multiple file paths using the CODEOWNERS file.
72
+ #
73
+ # This method efficiently determines ownership for multiple files in a single operation
74
+ # by leveraging the generated CODEOWNERS file. It's more performant than calling
75
+ # `for_file` multiple times when you need to check ownership for many files.
76
+ #
77
+ # @param files [Array<String>] An array of file paths to find ownership for.
78
+ # Paths can be relative to the project root or absolute.
79
+ # @param allow_raise [Boolean] (default: false) When true, raises an exception if a team
80
+ # name in CODEOWNERS cannot be resolved to an actual team.
81
+ # When false, returns nil for files with unresolvable teams.
82
+ #
83
+ # @return [T::Hash[String, T.nilable(CodeTeams::Team)]] A hash mapping each file path to its
84
+ # owning team. Files without ownership
85
+ # or with unresolvable teams will map to nil.
86
+ #
87
+ # @example Get owners for multiple files
88
+ # files = ['app/models/user.rb', 'app/controllers/users_controller.rb', 'config/routes.rb']
89
+ # owners = CodeOwnership.teams_for_files_from_codeowners(files)
90
+ # # => {
91
+ # # 'app/models/user.rb' => #<CodeTeams::Team:0x... @name="platform">,
92
+ # # 'app/controllers/users_controller.rb' => #<CodeTeams::Team:0x... @name="platform">,
93
+ # # 'config/routes.rb' => #<CodeTeams::Team:0x... @name="infrastructure">
94
+ # # }
95
+ #
96
+ # @example Handle files without owners
97
+ # files = ['owned_file.rb', 'unowned_file.txt']
98
+ # owners = CodeOwnership.teams_for_files_from_codeowners(files)
99
+ # # => {
100
+ # # 'owned_file.rb' => #<CodeTeams::Team:0x... @name="backend">,
101
+ # # 'unowned_file.txt' => nil
102
+ # # }
103
+ #
104
+ # @note This method uses caching internally for performance. The cache is populated
105
+ # as files are processed and reused for subsequent lookups.
106
+ #
107
+ # @note This method relies on the CODEOWNERS file being up-to-date. Run
108
+ # `CodeOwnership.validate!` to ensure the CODEOWNERS file is current.
109
+ #
110
+ # @see #for_file for single file ownership lookup
111
+ # @see #validate! for ensuring CODEOWNERS file is up-to-date
112
+ #
113
+ sig { params(files: T::Array[String], allow_raise: T::Boolean).returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
114
+ def self.teams_for_files_from_codeowners(files, allow_raise: false)
115
+ Private::TeamFinder.teams_for_files(files, allow_raise: allow_raise)
116
+ end
117
+
118
+ # Returns detailed ownership information for a given file path.
119
+ #
120
+ # This method provides verbose ownership details including the team name,
121
+ # team configuration file path, and the reasons/sources for ownership assignment.
122
+ # It's particularly useful for debugging ownership assignments and understanding
123
+ # why a file is owned by a specific team.
124
+ #
125
+ # @param file [String] The path to the file to find ownership for. Can be relative or absolute.
126
+ #
127
+ # @return [T::Hash[Symbol, String], nil] A hash containing detailed ownership information,
128
+ # or nil if no owner is found.
129
+ #
130
+ # The returned hash contains the following keys when an owner is found:
131
+ # - :team_name [String] - The name of the owning team
132
+ # - :team_config_yml [String] - Path to the team's configuration YAML file
133
+ # - :reasons [Array<String>] - List of reasons/sources explaining why this team owns the file
134
+ # (e.g., "CODEOWNERS pattern: /app/models/**", "Package ownership")
135
+ #
136
+ # @example Get verbose ownership details
137
+ # details = CodeOwnership.for_file_verbose('app/models/user.rb')
138
+ # # => {
139
+ # # team_name: "platform",
140
+ # # team_config_yml: "config/teams/platform.yml",
141
+ # # reasons: ["Matched pattern '/app/models/**' in CODEOWNERS"]
142
+ # # }
143
+ #
144
+ # @example Handle unowned files
145
+ # details = CodeOwnership.for_file_verbose('unowned_file.txt')
146
+ # # => nil
147
+ #
148
+ # @note This method is primarily used by the CLI tool when the --verbose flag is provided,
149
+ # allowing users to understand the ownership assignment logic.
150
+ #
151
+ # @note Unlike `for_file`, this method always uses the CODEOWNERS file and other ownership
152
+ # sources to determine ownership, providing complete context about the ownership decision.
153
+ #
154
+ # @see #for_file for a simpler ownership lookup that returns just the team
155
+ # @see CLI#for_file for the command-line interface that uses this method
156
+ #
157
+ sig { params(file: String).returns(T.nilable(T::Hash[Symbol, String])) }
158
+ def self.for_file_verbose(file)
159
+ ::RustCodeOwners.for_file(file)
160
+ end
161
+
162
+ sig { params(team: T.any(CodeTeams::Team, String)).returns(T::Array[String]) }
163
+ def self.for_team(team)
164
+ team = T.must(CodeTeams.find(team)) if team.is_a?(String)
165
+ ::RustCodeOwners.for_team(team.name)
166
+ end
167
+
168
+ # Validates code ownership configuration and optionally corrects issues.
169
+ #
170
+ # This method performs comprehensive validation of the code ownership setup, ensuring:
171
+ # 1. Only one ownership mechanism is defined per file (no conflicts between annotations, packages, or globs)
172
+ # 2. All referenced teams are valid (exist in CodeTeams configuration)
173
+ # 3. All files have ownership (unless explicitly listed in unowned_globs)
174
+ # 4. The .github/CODEOWNERS file is up-to-date and properly formatted
175
+ #
176
+ # When autocorrect is enabled, the method will automatically:
177
+ # - Generate or update the CODEOWNERS file based on current ownership rules
178
+ # - Fix any formatting issues in the CODEOWNERS file
179
+ # - Stage the corrected CODEOWNERS file (unless stage_changes is false)
180
+ #
181
+ # @param autocorrect [Boolean] Whether to automatically fix correctable issues (default: true)
182
+ # When true, regenerates and updates the CODEOWNERS file
183
+ # When false, only validates without making changes
184
+ #
185
+ # @param stage_changes [Boolean] Whether to stage the CODEOWNERS file after autocorrection (default: true)
186
+ # Only applies when autocorrect is true
187
+ # When false, changes are written but not staged with git
188
+ #
189
+ # @param files [Array<String>, nil] Ignored. This is a legacy parameter that is no longer used.
190
+ #
191
+ # @return [void]
192
+ #
193
+ # @raise [RuntimeError] Raises an error if validation fails with details about:
194
+ # - Files with conflicting ownership definitions
195
+ # - References to non-existent teams
196
+ # - Files without ownership (not in unowned_globs)
197
+ # - CODEOWNERS file inconsistencies
198
+ #
199
+ # @example Basic validation with autocorrection
200
+ # CodeOwnership.validate!
201
+ # # Validates all files and auto-corrects/stages CODEOWNERS if needed
202
+ #
203
+ # @example Validation without making changes
204
+ # CodeOwnership.validate!(autocorrect: false)
205
+ # # Only checks for issues without updating CODEOWNERS
206
+ #
207
+ # @example Validate and fix but don't stage changes
208
+ # CodeOwnership.validate!(autocorrect: true, stage_changes: false)
209
+ # # Fixes CODEOWNERS but doesn't stage it with git
210
+ #
211
+ # @note This method is called by the CLI command: bin/codeownership validate
212
+ # @note The validation can be disabled for CODEOWNERS by setting skip_codeowners_validation: true in config/code_ownership.yml
213
+ #
214
+ # @see CLI.validate! for the command-line interface
215
+ # @see https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners for CODEOWNERS format
216
+ #
217
+ sig do
218
+ params(
219
+ autocorrect: T::Boolean,
220
+ stage_changes: T::Boolean,
221
+ files: T.nilable(T::Array[String])
222
+ ).void
223
+ end
224
+ def self.validate!(
225
+ autocorrect: true,
226
+ stage_changes: true,
227
+ files: nil
228
+ )
229
+ if autocorrect
230
+ ::RustCodeOwners.generate_and_validate(files, !stage_changes)
231
+ else
232
+ ::RustCodeOwners.validate(files)
233
+ end
234
+ end
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
+
287
+ # Given a backtrace from either `Exception#backtrace` or `caller`, find the
288
+ # first line that corresponds to a file with assigned ownership
289
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
290
+ def self.for_backtrace(backtrace, excluded_teams: [])
291
+ Private::TeamFinder.for_backtrace(backtrace, excluded_teams: excluded_teams)
292
+ end
293
+
294
+ # Given a backtrace from either `Exception#backtrace` or `caller`, find the
295
+ # first owned file in it, useful for figuring out which file is being blamed.
296
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
297
+ def self.first_owned_file_for_backtrace(backtrace, excluded_teams: [])
298
+ Private::TeamFinder.first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)
299
+ end
300
+
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)
303
+ Private::TeamFinder.for_class(klass)
304
+ end
305
+
306
+ sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
307
+ def self.for_package(package)
308
+ Private::TeamFinder.for_package(package)
309
+ end
310
+
311
+ # Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
312
+ # Namely, the set of files, packages, and directories which are tracked for ownership should not change.
313
+ # The primary reason this is helpful is for clients of CodeOwnership who want to test their code, and each test context
314
+ # has different ownership and tracked files.
315
+ sig { void }
316
+ def self.bust_caches!
317
+ Private::FilePathTeamCache.bust_cache!
318
+ end
319
+ end
metadata ADDED
@@ -0,0 +1,219 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: code_ownership
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.1.1
5
+ platform: aarch64-mingw-ucrt
6
+ authors:
7
+ - Gusto Engineers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-01-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: code_teams
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: packs-specification
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sorbet-runtime
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.6.12763
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.6.12763
55
+ - !ruby/object:Gem::Dependency
56
+ name: debug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: packwerk
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: railties
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sorbet
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: tapioca
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: A gem to help engineering teams declare ownership of code
168
+ email:
169
+ - dev@gusto.com
170
+ executables:
171
+ - codeownership
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - LICENSE
176
+ - README.md
177
+ - bin/codeownership
178
+ - lib/code_ownership.rb
179
+ - lib/code_ownership/3.4/code_ownership.so
180
+ - lib/code_ownership/4.0/code_ownership.so
181
+ - lib/code_ownership/cli.rb
182
+ - lib/code_ownership/private/file_path_finder.rb
183
+ - lib/code_ownership/private/file_path_team_cache.rb
184
+ - lib/code_ownership/private/for_file_output_builder.rb
185
+ - lib/code_ownership/private/permit_pack_owner_top_level_key.rb
186
+ - lib/code_ownership/private/team_finder.rb
187
+ - lib/code_ownership/version.rb
188
+ homepage: https://github.com/rubyatscale/code_ownership
189
+ licenses:
190
+ - MIT
191
+ metadata:
192
+ homepage_uri: https://github.com/rubyatscale/code_ownership
193
+ source_code_uri: https://github.com/rubyatscale/code_ownership
194
+ changelog_uri: https://github.com/rubyatscale/code_ownership/releases
195
+ allowed_push_host: https://rubygems.org
196
+ cargo_crate_name: code_ownership
197
+ post_install_message:
198
+ rdoc_options: []
199
+ require_paths:
200
+ - lib
201
+ required_ruby_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '3.4'
206
+ - - "<"
207
+ - !ruby/object:Gem::Version
208
+ version: 4.1.dev
209
+ required_rubygems_version: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: '0'
214
+ requirements: []
215
+ rubygems_version: 3.5.23
216
+ signing_key:
217
+ specification_version: 4
218
+ summary: A gem to help engineering teams declare ownership of code
219
+ test_files: []