code_ownership 2.0.0-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 +7 -0
- data/LICENSE +21 -0
- data/README.md +201 -0
- data/bin/codeownership +5 -0
- data/lib/code_ownership/3.2/code_ownership.bundle +0 -0
- data/lib/code_ownership/3.4/code_ownership.bundle +0 -0
- data/lib/code_ownership/cli.rb +137 -0
- data/lib/code_ownership/private/file_path_finder.rb +63 -0
- data/lib/code_ownership/private/file_path_team_cache.rb +41 -0
- data/lib/code_ownership/private/for_file_output_builder.rb +83 -0
- data/lib/code_ownership/private/permit_pack_owner_top_level_key.rb +23 -0
- data/lib/code_ownership/private/team_finder.rb +106 -0
- data/lib/code_ownership/version.rb +6 -0
- data/lib/code_ownership.rb +274 -0
- metadata +219 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6dccf7af0dc4e4f80d30b5c4e60c845c3005fcc9d5b0de64fa745e8c0af25c47
|
4
|
+
data.tar.gz: 06d40270b78ec948983d041ba695226f497cc5b86fde83ae24270515a78a821d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9d9af6e0d695787d86ea0dc075e083dff170c9d8cc6a4ff4e5116605af4689af0ff941b7745db2ccd10a414bf2f6240b952f2212557ae3ef03c98badf3b17d6f
|
7
|
+
data.tar.gz: 540277ea9d1e9efccb5658b29154ce9d9778314354bafcedebcb27f3f5a2a2c666728a2f8cd9f364fd87c077c9e108b8076aaf8fa0a683e5c665c0198ae45044
|
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,201 @@
|
|
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]([url](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
|
+
```yml
|
17
|
+
owned_globs:
|
18
|
+
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
19
|
+
js_package_paths: []
|
20
|
+
unowned_globs:
|
21
|
+
- db/**/*
|
22
|
+
- app/services/some_file1.rb
|
23
|
+
- app/services/some_file2.rb
|
24
|
+
- frontend/javascripts/**/__generated__/**/*
|
25
|
+
```
|
26
|
+
2) Declare some teams. Here's an example, that would live at `config/teams/operations.yml`:
|
27
|
+
```yml
|
28
|
+
name: Operations
|
29
|
+
github:
|
30
|
+
team: '@my-org/operations-team'
|
31
|
+
```
|
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.
|
34
|
+
|
35
|
+
## Usage: Declaring Ownership
|
36
|
+
|
37
|
+
There are five ways to declare code ownership using this gem:
|
38
|
+
|
39
|
+
### Directory-Based Ownership
|
40
|
+
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.
|
41
|
+
```
|
42
|
+
Team
|
43
|
+
```
|
44
|
+
|
45
|
+
### File-Annotation Based Ownership
|
46
|
+
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:
|
47
|
+
```ruby
|
48
|
+
# @team MyTeam
|
49
|
+
```
|
50
|
+
|
51
|
+
### Package-Based Ownership
|
52
|
+
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:
|
53
|
+
```yml
|
54
|
+
enforce_dependency: true
|
55
|
+
enforce_privacy: true
|
56
|
+
metadata:
|
57
|
+
owner: Team
|
58
|
+
```
|
59
|
+
|
60
|
+
You can also define `owner` as a top-level key, e.g.
|
61
|
+
```yml
|
62
|
+
enforce_dependency: true
|
63
|
+
enforce_privacy: true
|
64
|
+
owner: Team
|
65
|
+
```
|
66
|
+
|
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.
|
68
|
+
|
69
|
+
### Glob-Based Ownership
|
70
|
+
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`:
|
71
|
+
```yml
|
72
|
+
name: My Team
|
73
|
+
owned_globs:
|
74
|
+
- app/services/stuff_belonging_to_my_team/**/**
|
75
|
+
- app/controllers/other_stuff_belonging_to_my_team/**/**
|
76
|
+
unowned_globs:
|
77
|
+
- app/controllers/other_stuff_belonging_to_my_team/that_one_weird_dir_we_dont_own/*
|
78
|
+
```
|
79
|
+
|
80
|
+
### Javascript Package Ownership
|
81
|
+
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
|
+
|
83
|
+
```json
|
84
|
+
{
|
85
|
+
// other keys
|
86
|
+
"metadata": {
|
87
|
+
"owner": "My Team"
|
88
|
+
}
|
89
|
+
// other keys
|
90
|
+
}
|
91
|
+
```
|
92
|
+
|
93
|
+
You can also tell `code_ownership` where to find JS packages in the configuration, like this:
|
94
|
+
```yml
|
95
|
+
js_package_paths:
|
96
|
+
- frontend/javascripts/packages/*
|
97
|
+
- frontend/other_location_for_packages/*
|
98
|
+
```
|
99
|
+
|
100
|
+
This defaults `**/`, which makes it look for `package.json` files across your application.
|
101
|
+
|
102
|
+
> [!NOTE]
|
103
|
+
> 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.
|
104
|
+
```yml
|
105
|
+
js_package_paths: []
|
106
|
+
```
|
107
|
+
|
108
|
+
## Usage: Reading CodeOwnership
|
109
|
+
### `for_file`
|
110
|
+
`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
|
+
|
112
|
+
```ruby
|
113
|
+
CodeOwnership.for_file('path/to/file/relative/to/application/root.rb')
|
114
|
+
```
|
115
|
+
|
116
|
+
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).
|
117
|
+
|
118
|
+
See `code_ownership_spec.rb` for examples.
|
119
|
+
|
120
|
+
### `for_backtrace`
|
121
|
+
`CodeOwnership.for_backtrace` can be given a backtrace and will either return `nil`, or a `CodeTeams::Team`.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
CodeOwnership.for_backtrace(exception.backtrace)
|
125
|
+
```
|
126
|
+
|
127
|
+
This will go through the backtrace, and return the first found owner of the files associated with frames within the backtrace.
|
128
|
+
|
129
|
+
See `code_ownership_spec.rb` for an example.
|
130
|
+
|
131
|
+
### `for_class`
|
132
|
+
|
133
|
+
`CodeOwnership.for_class` can be given a class and will either return `nil`, or a `CodeTeams::Team`.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
CodeOwnership.for_class(MyClass)
|
137
|
+
```
|
138
|
+
|
139
|
+
Under the hood, this finds the file where the class is defined and returns the owner of that file.
|
140
|
+
|
141
|
+
See `code_ownership_spec.rb` for an example.
|
142
|
+
|
143
|
+
### `for_team`
|
144
|
+
`CodeOwnership.for_team` can be used to generate an ownership report for a team.
|
145
|
+
```ruby
|
146
|
+
CodeOwnership.for_team('My Team')
|
147
|
+
```
|
148
|
+
|
149
|
+
You can shovel this into a markdown file for easy viewing using the CLI:
|
150
|
+
```
|
151
|
+
bin/codeownership for_team 'My Team' > tmp/ownership_report.md
|
152
|
+
```
|
153
|
+
|
154
|
+
## Usage: Generating a `CODEOWNERS` file
|
155
|
+
|
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.
|
157
|
+
|
158
|
+
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
|
+
|
160
|
+
## Proper Configuration & Validation
|
161
|
+
|
162
|
+
CodeOwnership comes with a validation function to ensure the following things are true:
|
163
|
+
|
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`.
|
168
|
+
|
169
|
+
CodeOwnership also allows you to specify which globs and file extensions should be considered ownable.
|
170
|
+
|
171
|
+
Here is an example `config/code_ownership.yml`.
|
172
|
+
```yml
|
173
|
+
owned_globs:
|
174
|
+
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
175
|
+
unowned_globs:
|
176
|
+
- db/**/*
|
177
|
+
- app/services/some_file1.rb
|
178
|
+
- app/services/some_file2.rb
|
179
|
+
- frontend/javascripts/**/__generated__/**/*
|
180
|
+
```
|
181
|
+
You can call the validation function with the Ruby API
|
182
|
+
```ruby
|
183
|
+
CodeOwnership.validate!
|
184
|
+
```
|
185
|
+
or the CLI
|
186
|
+
```
|
187
|
+
bin/codeownership validate
|
188
|
+
```
|
189
|
+
|
190
|
+
## Development
|
191
|
+
|
192
|
+
Please add to `CHANGELOG.md` and this `README.md` when you make make changes.
|
193
|
+
|
194
|
+
## Running specs
|
195
|
+
```sh
|
196
|
+
bundle install
|
197
|
+
bundle exec rake
|
198
|
+
```
|
199
|
+
|
200
|
+
## Creating a new release
|
201
|
+
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
Binary file
|
Binary file
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'pathname'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module CodeOwnership
|
8
|
+
class Cli
|
9
|
+
EXECUTABLE = 'bin/codeownership'
|
10
|
+
|
11
|
+
def self.run!(argv)
|
12
|
+
command = argv.shift
|
13
|
+
if command == 'validate'
|
14
|
+
validate!(argv)
|
15
|
+
elsif command == 'for_file'
|
16
|
+
for_file(argv)
|
17
|
+
elsif command == 'for_team'
|
18
|
+
for_team(argv)
|
19
|
+
elsif command == 'version'
|
20
|
+
version
|
21
|
+
elsif [nil, 'help'].include?(command)
|
22
|
+
puts <<~USAGE
|
23
|
+
Usage: #{EXECUTABLE} <subcommand>
|
24
|
+
|
25
|
+
Subcommands:
|
26
|
+
validate - run all validations
|
27
|
+
for_file - find code ownership for a single file
|
28
|
+
for_team - find code ownership information for a team
|
29
|
+
help - display help information about code_ownership
|
30
|
+
USAGE
|
31
|
+
else
|
32
|
+
puts "'#{command}' is not a code_ownership command. See `#{EXECUTABLE} help`."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.validate!(argv)
|
37
|
+
options = {}
|
38
|
+
|
39
|
+
parser = OptionParser.new do |opts|
|
40
|
+
opts.banner = "Usage: #{EXECUTABLE} validate [options]"
|
41
|
+
|
42
|
+
opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .github/CODEOWNERS file') do
|
43
|
+
options[:skip_autocorrect] = true
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on('-d', '--diff', 'Only run validations with staged files') do
|
47
|
+
options[:diff] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on('-s', '--skip-stage', 'Skips staging the CODEOWNERS file') do
|
51
|
+
options[:skip_stage] = true
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on('--help', 'Shows this prompt') do
|
55
|
+
puts opts
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
args = parser.order!(argv)
|
60
|
+
parser.parse!(args)
|
61
|
+
|
62
|
+
files = if options[:diff]
|
63
|
+
ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
|
64
|
+
File.exist?(file)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
CodeOwnership.validate!(
|
71
|
+
files: files,
|
72
|
+
autocorrect: !options[:skip_autocorrect],
|
73
|
+
stage_changes: !options[:skip_stage]
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.version
|
78
|
+
puts CodeOwnership.version.join("\n")
|
79
|
+
end
|
80
|
+
|
81
|
+
# For now, this just returns team ownership
|
82
|
+
# Later, this could also return code ownership errors about that file.
|
83
|
+
def self.for_file(argv)
|
84
|
+
options = {}
|
85
|
+
|
86
|
+
# Long-term, we probably want to use something like `thor` so we don't have to implement logic
|
87
|
+
# like this. In the short-term, this is a simple way for us to use the built-in OptionParser
|
88
|
+
# while having an ergonomic CLI.
|
89
|
+
files = argv.reject { |arg| arg.start_with?('--') }
|
90
|
+
|
91
|
+
parser = OptionParser.new do |opts|
|
92
|
+
opts.banner = "Usage: #{EXECUTABLE} for_file [options]"
|
93
|
+
|
94
|
+
opts.on('--json', 'Output as JSON') do
|
95
|
+
options[:json] = true
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on('--verbose', 'Output verbose information') do
|
99
|
+
options[:verbose] = true
|
100
|
+
end
|
101
|
+
|
102
|
+
opts.on('--help', 'Shows this prompt') do
|
103
|
+
puts opts
|
104
|
+
exit
|
105
|
+
end
|
106
|
+
end
|
107
|
+
args = parser.order!(argv)
|
108
|
+
parser.parse!(args)
|
109
|
+
|
110
|
+
if files.count != 1
|
111
|
+
raise "Please pass in one file. Use `#{EXECUTABLE} for_file --help` for more info"
|
112
|
+
end
|
113
|
+
|
114
|
+
puts CodeOwnership::Private::ForFileOutputBuilder.build(file_path: files.first, json: !!options[:json], verbose: !!options[:verbose])
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.for_team(argv)
|
118
|
+
parser = OptionParser.new do |opts|
|
119
|
+
opts.banner = "Usage: #{EXECUTABLE} for_team 'Team Name'"
|
120
|
+
|
121
|
+
opts.on('--help', 'Shows this prompt') do
|
122
|
+
puts opts
|
123
|
+
exit
|
124
|
+
end
|
125
|
+
end
|
126
|
+
teams = argv.reject { |arg| arg.start_with?('--') }
|
127
|
+
args = parser.order!(argv)
|
128
|
+
parser.parse!(args)
|
129
|
+
|
130
|
+
if teams.count != 1
|
131
|
+
raise "Please pass in one team. Use `#{EXECUTABLE} for_team --help` for more info"
|
132
|
+
end
|
133
|
+
|
134
|
+
puts CodeOwnership.for_team(teams.first).join("\n")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module FilePathFinder
|
8
|
+
module_function
|
9
|
+
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
|
13
|
+
# Returns a string version of the relative path to a Rails constant,
|
14
|
+
# 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)
|
17
|
+
if klass
|
18
|
+
path = Object.const_source_location(klass.to_s)&.first
|
19
|
+
(path && Pathname.new(path).relative_path_from(Pathname.pwd).to_s) || nil
|
20
|
+
end
|
21
|
+
rescue NameError
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(backtrace: T.nilable(T::Array[String])).returns(T::Enumerable[String]) }
|
26
|
+
def from_backtrace(backtrace)
|
27
|
+
return [] unless backtrace
|
28
|
+
|
29
|
+
# The pattern for a backtrace hasn't changed in forever and is considered
|
30
|
+
# stable: https://github.com/ruby/ruby/blob/trunk/vm_backtrace.c#L303-L317
|
31
|
+
#
|
32
|
+
# This pattern matches a line like the following:
|
33
|
+
#
|
34
|
+
# ./app/controllers/some_controller.rb:43:in `block (3 levels) in create'
|
35
|
+
#
|
36
|
+
backtrace_line = if RUBY_VERSION >= '3.4.0'
|
37
|
+
%r{\A(#{Pathname.pwd}/|\./)?
|
38
|
+
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
|
39
|
+
:
|
40
|
+
(?<line>\d+) # Matches '43'
|
41
|
+
:in\s
|
42
|
+
'(?<function>.*)' # Matches "`block (3 levels) in create'"
|
43
|
+
\z}x
|
44
|
+
else
|
45
|
+
%r{\A(#{Pathname.pwd}/|\./)?
|
46
|
+
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
|
47
|
+
:
|
48
|
+
(?<line>\d+) # Matches '43'
|
49
|
+
:in\s
|
50
|
+
`(?<function>.*)' # Matches "`block (3 levels) in create'"
|
51
|
+
\z}x
|
52
|
+
end
|
53
|
+
|
54
|
+
backtrace.lazy.filter_map do |line|
|
55
|
+
match = line.match(backtrace_line)
|
56
|
+
next unless match
|
57
|
+
|
58
|
+
T.must(match[:file])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module FilePathTeamCache
|
8
|
+
module_function
|
9
|
+
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
|
13
|
+
sig { params(file_path: String).returns(T.nilable(CodeTeams::Team)) }
|
14
|
+
def get(file_path)
|
15
|
+
cache[file_path]
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(file_path: String, team: T.nilable(CodeTeams::Team)).void }
|
19
|
+
def set(file_path, team)
|
20
|
+
cache[file_path] = team
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { params(file_path: String).returns(T::Boolean) }
|
24
|
+
def cached?(file_path)
|
25
|
+
cache.key?(file_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { void }
|
29
|
+
def bust_cache!
|
30
|
+
@cache = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
|
34
|
+
def cache
|
35
|
+
@cache ||= T.let(@cache,
|
36
|
+
T.nilable(T::Hash[String, T.nilable(CodeTeams::Team)]))
|
37
|
+
@cache ||= {}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -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
|
@@ -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,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module TeamFinder
|
8
|
+
module_function
|
9
|
+
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
|
13
|
+
requires_ancestor { Kernel }
|
14
|
+
|
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
|
+
return nil if file_path.start_with?('./')
|
18
|
+
|
19
|
+
return FilePathTeamCache.get(file_path) if FilePathTeamCache.cached?(file_path)
|
20
|
+
|
21
|
+
result = T.let(RustCodeOwners.for_file(file_path), T.nilable(T::Hash[Symbol, String]))
|
22
|
+
return if result.nil?
|
23
|
+
|
24
|
+
if result[:team_name].nil?
|
25
|
+
FilePathTeamCache.set(file_path, nil)
|
26
|
+
else
|
27
|
+
FilePathTeamCache.set(file_path, T.let(find_team!(T.must(result[:team_name]), allow_raise: allow_raise), T.nilable(CodeTeams::Team)))
|
28
|
+
end
|
29
|
+
|
30
|
+
FilePathTeamCache.get(file_path)
|
31
|
+
end
|
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
|
+
result = {}
|
36
|
+
|
37
|
+
# Collect cached results and identify non-cached files
|
38
|
+
not_cached_files = []
|
39
|
+
files.each do |file_path|
|
40
|
+
if FilePathTeamCache.cached?(file_path)
|
41
|
+
result[file_path] = FilePathTeamCache.get(file_path)
|
42
|
+
else
|
43
|
+
not_cached_files << file_path
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
return result if not_cached_files.empty?
|
48
|
+
|
49
|
+
# Process non-cached files
|
50
|
+
::RustCodeOwners.teams_for_files(not_cached_files).each do |path_team|
|
51
|
+
file_path, team = path_team
|
52
|
+
found_team = team ? find_team!(team[:team_name], allow_raise: allow_raise) : nil
|
53
|
+
FilePathTeamCache.set(file_path, found_team)
|
54
|
+
result[file_path] = found_team
|
55
|
+
end
|
56
|
+
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
|
61
|
+
def for_class(klass)
|
62
|
+
file_path = FilePathFinder.path_from_klass(klass)
|
63
|
+
return nil if file_path.nil?
|
64
|
+
|
65
|
+
for_file(file_path)
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
|
69
|
+
def for_package(package)
|
70
|
+
owner_name = package.raw_hash['owner'] || package.metadata['owner']
|
71
|
+
return nil if owner_name.nil?
|
72
|
+
|
73
|
+
find_team!(owner_name, allow_raise: true)
|
74
|
+
end
|
75
|
+
|
76
|
+
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: [])
|
78
|
+
first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)&.first
|
79
|
+
end
|
80
|
+
|
81
|
+
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: [])
|
83
|
+
FilePathFinder.from_backtrace(backtrace).each do |file|
|
84
|
+
team = for_file(file)
|
85
|
+
if team && !excluded_teams.include?(team)
|
86
|
+
return [team, file]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
|
93
|
+
sig { params(team_name: String, allow_raise: T::Boolean).returns(T.nilable(CodeTeams::Team)) }
|
94
|
+
def find_team!(team_name, allow_raise: false)
|
95
|
+
team = CodeTeams.find(team_name)
|
96
|
+
if team.nil? && allow_raise
|
97
|
+
raise(StandardError, "Could not find team with name: `#{team_name}`. Make sure the team is one of `#{CodeTeams.all.map(&:name).sort}`")
|
98
|
+
end
|
99
|
+
|
100
|
+
team
|
101
|
+
end
|
102
|
+
|
103
|
+
private_class_method(:find_team!)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
require 'set'
|
6
|
+
require 'code_teams'
|
7
|
+
require 'sorbet-runtime'
|
8
|
+
require 'json'
|
9
|
+
require 'packs-specification'
|
10
|
+
require 'code_ownership/version'
|
11
|
+
require 'code_ownership/private/file_path_finder'
|
12
|
+
require 'code_ownership/private/file_path_team_cache'
|
13
|
+
require 'code_ownership/private/team_finder'
|
14
|
+
require 'code_ownership/private/for_file_output_builder'
|
15
|
+
require 'code_ownership/cli'
|
16
|
+
|
17
|
+
begin
|
18
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
19
|
+
require "code_ownership/#{Regexp.last_match(1)}/code_ownership"
|
20
|
+
rescue LoadError
|
21
|
+
require 'code_ownership/code_ownership'
|
22
|
+
end
|
23
|
+
|
24
|
+
if defined?(Packwerk)
|
25
|
+
require 'code_ownership/private/permit_pack_owner_top_level_key'
|
26
|
+
end
|
27
|
+
|
28
|
+
module CodeOwnership
|
29
|
+
module_function
|
30
|
+
|
31
|
+
extend T::Sig
|
32
|
+
extend T::Helpers
|
33
|
+
|
34
|
+
requires_ancestor { Kernel }
|
35
|
+
GlobsToOwningTeamMap = T.type_alias { T::Hash[String, CodeTeams::Team] }
|
36
|
+
|
37
|
+
# Returns the version of the code_ownership gem and the codeowners-rs gem.
|
38
|
+
sig { returns(T::Array[String]) }
|
39
|
+
def version
|
40
|
+
["code_ownership version: #{VERSION}",
|
41
|
+
"codeowners-rs version: #{::RustCodeOwners.version}"]
|
42
|
+
end
|
43
|
+
|
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)
|
166
|
+
end
|
167
|
+
|
168
|
+
sig { params(team: T.any(CodeTeams::Team, String)).returns(T::Array[String]) }
|
169
|
+
def for_team(team)
|
170
|
+
team = T.must(CodeTeams.find(team)) if team.is_a?(String)
|
171
|
+
::RustCodeOwners.for_team(team.name)
|
172
|
+
end
|
173
|
+
|
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
|
+
#
|
223
|
+
sig do
|
224
|
+
params(
|
225
|
+
autocorrect: T::Boolean,
|
226
|
+
stage_changes: T::Boolean,
|
227
|
+
files: T.nilable(T::Array[String])
|
228
|
+
).void
|
229
|
+
end
|
230
|
+
def validate!(
|
231
|
+
autocorrect: true,
|
232
|
+
stage_changes: true,
|
233
|
+
files: nil
|
234
|
+
)
|
235
|
+
if autocorrect
|
236
|
+
::RustCodeOwners.generate_and_validate(files, !stage_changes)
|
237
|
+
else
|
238
|
+
::RustCodeOwners.validate(files)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
|
243
|
+
# first line that corresponds to a file with assigned ownership
|
244
|
+
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: [])
|
246
|
+
Private::TeamFinder.for_backtrace(backtrace, excluded_teams: excluded_teams)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
|
250
|
+
# first owned file in it, useful for figuring out which file is being blamed.
|
251
|
+
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: [])
|
253
|
+
Private::TeamFinder.first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)
|
254
|
+
end
|
255
|
+
|
256
|
+
sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
|
257
|
+
def for_class(klass)
|
258
|
+
Private::TeamFinder.for_class(klass)
|
259
|
+
end
|
260
|
+
|
261
|
+
sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
|
262
|
+
def for_package(package)
|
263
|
+
Private::TeamFinder.for_package(package)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
|
267
|
+
# Namely, the set of files, packages, and directories which are tracked for ownership should not change.
|
268
|
+
# The primary reason this is helpful is for clients of CodeOwnership who want to test their code, and each test context
|
269
|
+
# has different ownership and tracked files.
|
270
|
+
sig { void }
|
271
|
+
def self.bust_caches!
|
272
|
+
Private::FilePathTeamCache.bust_cache!
|
273
|
+
end
|
274
|
+
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.0.0
|
5
|
+
platform: arm64-darwin
|
6
|
+
authors:
|
7
|
+
- Gusto Engineers
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-10-22 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.5.11249
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.5.11249
|
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.2/code_ownership.bundle
|
180
|
+
- lib/code_ownership/3.4/code_ownership.bundle
|
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.2'
|
206
|
+
- - "<"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 3.5.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: []
|