code_ownership 2.0.0.pre.1-x86_64-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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fc4bf43044b38af02a0e58167575c930364561d5a0cfa1fd7f7de91406c21e24
4
+ data.tar.gz: 3874a52d8fc03a1edc7de7e5c8a9d48ef8ea012b7b7993e23a7107a88ca35c52
5
+ SHA512:
6
+ metadata.gz: 98dd439775bb33f6efe3ee33925d0e8302078d8433cb5f2185bcbb58fa3f956efdc05ad403e14bd438f25270392e127b47a6d61f15de719fd6c4ff1ea1737e7e
7
+ data.tar.gz: 285db4ba1df6ae685b492c2110d6a0e919928972e356db0abc82e4bbb0d75dba0b04200aed3302d5dfb8d75d5738429dbbfaba5e2efa1d6d8ec0506e77a848e8
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
@@ -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,150 @@
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('--help', 'Shows this prompt') do
99
+ puts opts
100
+ exit
101
+ end
102
+ end
103
+ args = parser.order!(argv)
104
+ parser.parse!(args)
105
+
106
+ if files.count != 1
107
+ raise "Please pass in one file. Use `#{EXECUTABLE} for_file --help` for more info"
108
+ end
109
+
110
+ team = CodeOwnership.for_file(files.first)
111
+
112
+ team_name = team&.name || 'Unowned'
113
+ team_yml = team&.config_yml || 'Unowned'
114
+
115
+ if options[:json]
116
+ json = {
117
+ team_name: team_name,
118
+ team_yml: team_yml
119
+ }
120
+
121
+ puts json.to_json
122
+ else
123
+ puts <<~MSG
124
+ Team: #{team_name}
125
+ Team YML: #{team_yml}
126
+ MSG
127
+ end
128
+ end
129
+
130
+ def self.for_team(argv)
131
+ parser = OptionParser.new do |opts|
132
+ opts.banner = "Usage: #{EXECUTABLE} for_team 'Team Name'"
133
+
134
+ opts.on('--help', 'Shows this prompt') do
135
+ puts opts
136
+ exit
137
+ end
138
+ end
139
+ teams = argv.reject { |arg| arg.start_with?('--') }
140
+ args = parser.order!(argv)
141
+ parser.parse!(args)
142
+
143
+ if teams.count != 1
144
+ raise "Please pass in one team. Use `#{EXECUTABLE} for_team --help` for more info"
145
+ end
146
+
147
+ puts CodeOwnership.for_team(teams.first).join("\n")
148
+ end
149
+ end
150
+ 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,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,75 @@
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).returns(T.nilable(CodeTeams::Team)) }
16
+ def for_file(file_path)
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])), T.nilable(CodeTeams::Team)))
28
+ end
29
+
30
+ FilePathTeamCache.get(file_path)
31
+ end
32
+
33
+ sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
34
+ def for_class(klass)
35
+ file_path = FilePathFinder.path_from_klass(klass)
36
+ return nil if file_path.nil?
37
+
38
+ for_file(file_path)
39
+ end
40
+
41
+ sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
42
+ def for_package(package)
43
+ owner_name = package.raw_hash['owner'] || package.metadata['owner']
44
+ return nil if owner_name.nil?
45
+
46
+ find_team!(owner_name)
47
+ end
48
+
49
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
50
+ def for_backtrace(backtrace, excluded_teams: [])
51
+ first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)&.first
52
+ end
53
+
54
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
55
+ def first_owned_file_for_backtrace(backtrace, excluded_teams: [])
56
+ FilePathFinder.from_backtrace(backtrace).each do |file|
57
+ team = for_file(file)
58
+ if team && !excluded_teams.include?(team)
59
+ return [team, file]
60
+ end
61
+ end
62
+
63
+ nil
64
+ end
65
+
66
+ sig { params(team_name: String).returns(CodeTeams::Team) }
67
+ def find_team!(team_name)
68
+ CodeTeams.find(team_name) ||
69
+ raise(StandardError, "Could not find team with name: `#{team_name}`. Make sure the team is one of `#{CodeTeams.all.map(&:name).sort}`")
70
+ end
71
+
72
+ private_class_method(:find_team!)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CodeOwnership
4
+ VERSION = '2.0.0-1'
5
+ end
@@ -0,0 +1,107 @@
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/cli'
15
+
16
+ begin
17
+ RUBY_VERSION =~ /(\d+\.\d+)/
18
+ require "code_ownership/#{Regexp.last_match(1)}/code_ownership"
19
+ rescue LoadError
20
+ require 'code_ownership/code_ownership'
21
+ end
22
+
23
+ if defined?(Packwerk)
24
+ require 'code_ownership/private/permit_pack_owner_top_level_key'
25
+ end
26
+
27
+ module CodeOwnership
28
+ module_function
29
+
30
+ extend T::Sig
31
+ extend T::Helpers
32
+
33
+ requires_ancestor { Kernel }
34
+ GlobsToOwningTeamMap = T.type_alias { T::Hash[String, CodeTeams::Team] }
35
+
36
+ sig { returns(T::Array[String]) }
37
+ def version
38
+ ["code_ownership version: #{VERSION}",
39
+ "codeowners-rs version: #{::RustCodeOwners.version}"]
40
+ end
41
+
42
+ sig { params(file: String).returns(T.nilable(CodeTeams::Team)) }
43
+ def for_file(file)
44
+ Private::TeamFinder.for_file(file)
45
+ end
46
+
47
+ sig { params(team: T.any(CodeTeams::Team, String)).returns(T::Array[String]) }
48
+ def for_team(team)
49
+ team = T.must(CodeTeams.find(team)) if team.is_a?(String)
50
+ ::RustCodeOwners.for_team(team.name)
51
+ end
52
+
53
+ class InvalidCodeOwnershipConfigurationError < StandardError
54
+ end
55
+
56
+ sig do
57
+ params(
58
+ autocorrect: T::Boolean,
59
+ stage_changes: T::Boolean,
60
+ files: T.nilable(T::Array[String])
61
+ ).void
62
+ end
63
+ def validate!(
64
+ autocorrect: true,
65
+ stage_changes: true,
66
+ files: nil
67
+ )
68
+ if autocorrect
69
+ ::RustCodeOwners.generate_and_validate(!stage_changes)
70
+ else
71
+ ::RustCodeOwners.validate
72
+ end
73
+ end
74
+
75
+ # Given a backtrace from either `Exception#backtrace` or `caller`, find the
76
+ # first line that corresponds to a file with assigned ownership
77
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
78
+ def for_backtrace(backtrace, excluded_teams: [])
79
+ Private::TeamFinder.for_backtrace(backtrace, excluded_teams: excluded_teams)
80
+ end
81
+
82
+ # Given a backtrace from either `Exception#backtrace` or `caller`, find the
83
+ # first owned file in it, useful for figuring out which file is being blamed.
84
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
85
+ def first_owned_file_for_backtrace(backtrace, excluded_teams: [])
86
+ Private::TeamFinder.first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)
87
+ end
88
+
89
+ sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
90
+ def for_class(klass)
91
+ Private::TeamFinder.for_class(klass)
92
+ end
93
+
94
+ sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
95
+ def for_package(package)
96
+ Private::TeamFinder.for_package(package)
97
+ end
98
+
99
+ # Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
100
+ # Namely, the set of files, packages, and directories which are tracked for ownership should not change.
101
+ # The primary reason this is helpful is for clients of CodeOwnership who want to test their code, and each test context
102
+ # has different ownership and tracked files.
103
+ sig { void }
104
+ def self.bust_caches!
105
+ Private::FilePathTeamCache.bust_cache!
106
+ end
107
+ end
metadata ADDED
@@ -0,0 +1,218 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: code_ownership
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0.pre.1
5
+ platform: x86_64-darwin
6
+ authors:
7
+ - Gusto Engineers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-08-14 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/permit_pack_owner_top_level_key.rb
185
+ - lib/code_ownership/private/team_finder.rb
186
+ - lib/code_ownership/version.rb
187
+ homepage: https://github.com/rubyatscale/code_ownership
188
+ licenses:
189
+ - MIT
190
+ metadata:
191
+ homepage_uri: https://github.com/rubyatscale/code_ownership
192
+ source_code_uri: https://github.com/rubyatscale/code_ownership
193
+ changelog_uri: https://github.com/rubyatscale/code_ownership/releases
194
+ allowed_push_host: https://rubygems.org
195
+ cargo_crate_name: code_ownership
196
+ post_install_message:
197
+ rdoc_options: []
198
+ require_paths:
199
+ - lib
200
+ required_ruby_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: '3.2'
205
+ - - "<"
206
+ - !ruby/object:Gem::Version
207
+ version: 3.5.dev
208
+ required_rubygems_version: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
213
+ requirements: []
214
+ rubygems_version: 3.5.23
215
+ signing_key:
216
+ specification_version: 4
217
+ summary: A gem to help engineering teams declare ownership of code
218
+ test_files: []