code_ownership 1.23.0
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/README.md +101 -0
- data/bin/codeownership +5 -0
- data/lib/code_ownership/cli.rb +60 -0
- data/lib/code_ownership/private/configuration.rb +37 -0
- data/lib/code_ownership/private/ownership_mappers/file_annotations.rb +119 -0
- data/lib/code_ownership/private/ownership_mappers/interface.rb +50 -0
- data/lib/code_ownership/private/ownership_mappers/js_package_ownership.rb +121 -0
- data/lib/code_ownership/private/ownership_mappers/package_ownership.rb +121 -0
- data/lib/code_ownership/private/ownership_mappers/team_globs.rb +68 -0
- data/lib/code_ownership/private/parse_js_packages.rb +59 -0
- data/lib/code_ownership/private/team_plugins/github.rb +24 -0
- data/lib/code_ownership/private/team_plugins/ownership.rb +17 -0
- data/lib/code_ownership/private/validations/files_have_owners.rb +34 -0
- data/lib/code_ownership/private/validations/files_have_unique_owners.rb +32 -0
- data/lib/code_ownership/private/validations/github_codeowners_up_to_date.rb +85 -0
- data/lib/code_ownership/private/validations/interface.rb +18 -0
- data/lib/code_ownership/private.rb +124 -0
- data/lib/code_ownership.rb +129 -0
- data/sorbet/config +4 -0
- data/sorbet/rbi/gems/bigrails-teams@0.1.0.rbi +120 -0
- data/sorbet/rbi/gems/parse_packwerk@0.7.0.rbi +111 -0
- data/sorbet/rbi/todo.rbi +6 -0
- metadata +182 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c664c64c94e083083f3187002fc77ffb7d40cbc7c5c76ae0291b9d5c11cd224f
|
4
|
+
data.tar.gz: cf6dcc613f67a6e7723fa73a079dabfa8782ddba886441afd8e5608eaa9249ec
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 54373f777f534bd2f600510e1ee94b85151bc5016d076d35ef5ed7d140a1d8b53d1bcea24a1af2badebc5939c52c1e2660e066eb49fcbcc4fb7a330cf227664f
|
7
|
+
data.tar.gz: c94fd3d689b09a27b8db89b339e3037ec8e6c1fe76413226b22662ce068599ecd30a6aa80e0f90e389a0d1a17bb58afebd650ed4c8bd57523336bd688517f819
|
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# CodeOwnership
|
2
|
+
This gem helps engineering teams declare ownership of code.
|
3
|
+
|
4
|
+
Check out `lib/code_ownership.rb` to see the public API.
|
5
|
+
|
6
|
+
Check out `code_ownership_spec.rb` to see examples of how code ownership is used.
|
7
|
+
|
8
|
+
## Usage: Declaring Ownership
|
9
|
+
There are three ways to declare code ownership using this gem.
|
10
|
+
### Package-Based Ownership
|
11
|
+
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:
|
12
|
+
```yml
|
13
|
+
enforce_dependency: true
|
14
|
+
enforce_privacy: true
|
15
|
+
metadata:
|
16
|
+
owner: Team
|
17
|
+
```
|
18
|
+
|
19
|
+
### Glob-Based Ownership
|
20
|
+
In your team's configured YML (see [`bigrails-teams`](https://github.com/bigrails/bigrails-teams)), you can set `owned_globs` to be a glob of files your team owns. For example, in `my_team.yml`:
|
21
|
+
```yml
|
22
|
+
name: My Team
|
23
|
+
owned_globs:
|
24
|
+
- app/services/stuff_belonging_to_my_team/**/**
|
25
|
+
- app/controllers/other_stuff_belonging_to_my_team/**/**
|
26
|
+
```
|
27
|
+
### File-Annotation Based Ownership
|
28
|
+
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:
|
29
|
+
```ruby
|
30
|
+
# @team MyTeam
|
31
|
+
```
|
32
|
+
## Usage: Reading CodeOwnership
|
33
|
+
### `for_file`
|
34
|
+
`CodeOwnership.for_file`, given a relative path to a file returns a `Teams::Team` if there is a team that owns the file, `nil` otherwise.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
CodeOwnership.for_file('path/to/file/relative/to/application/root.rb')
|
38
|
+
```
|
39
|
+
|
40
|
+
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).
|
41
|
+
|
42
|
+
See `code_ownership_spec.rb` for examples.
|
43
|
+
|
44
|
+
### `for_backtrace`
|
45
|
+
`CodeOwnership.for_backtrace` can be given a backtrace and will either return `nil`, or a `Teams::Team`.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
CodeOwnership.for_backtrace(exception.backtrace)
|
49
|
+
```
|
50
|
+
|
51
|
+
This will go through the backtrace, and return the first found owner of the files associated with frames within the backtrace.
|
52
|
+
|
53
|
+
See `code_ownership_spec.rb` for an example.
|
54
|
+
|
55
|
+
### `for_class`
|
56
|
+
|
57
|
+
`CodeOwnership.for_class` can be given a class and will either return `nil`, or a `Teams::Team`.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
CodeOwnership.for_class(MyClass.name)
|
61
|
+
```
|
62
|
+
|
63
|
+
Under the hood, this finds the file where the class is defined and returns the owner of that file.
|
64
|
+
|
65
|
+
See `code_ownership_spec.rb` for an example.
|
66
|
+
|
67
|
+
## Usage: Generating a `CODEOWNERS` file
|
68
|
+
|
69
|
+
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.
|
70
|
+
|
71
|
+
## Proper Configuration & Validation
|
72
|
+
CodeOwnership comes with a validation function to ensure the following things are true:
|
73
|
+
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.
|
74
|
+
2) All teams referenced as an owner for any file or package is a valid team (i.e. it's in the list of `Teams.all`).
|
75
|
+
3) All files have ownership. You can specify in `unowned_globs` to represent a TODO list of files to add ownership to.
|
76
|
+
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 `code_ownership.yml`.
|
77
|
+
|
78
|
+
CodeOwnership also allows you to specify which globs and file extensions should be considered ownable.
|
79
|
+
|
80
|
+
Here is an example `config/code_ownership.yml`.
|
81
|
+
```yml
|
82
|
+
owned_globs:
|
83
|
+
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
84
|
+
unowned_globs:
|
85
|
+
- db/**/*
|
86
|
+
- app/services/some_file1.rb
|
87
|
+
- app/services/some_file2.rb
|
88
|
+
- frontend/javascripts/**/__generated__/**/*
|
89
|
+
```
|
90
|
+
You can call the validation function with the Ruby API
|
91
|
+
```ruby
|
92
|
+
CodeOwnership.validate!
|
93
|
+
```
|
94
|
+
or the CLI
|
95
|
+
```
|
96
|
+
bin/codeownership validate
|
97
|
+
```
|
98
|
+
|
99
|
+
## Development
|
100
|
+
|
101
|
+
Please add to `CHANGELOG.md` and this `README.md` when you make make changes.
|
data/bin/codeownership
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module CodeOwnership
|
7
|
+
class Cli
|
8
|
+
def self.run!(argv)
|
9
|
+
# Someday we might support other subcommands. When we do that, we can call
|
10
|
+
# argv.shift to get the first argument and check if it's a given subcommand.
|
11
|
+
command = argv.shift
|
12
|
+
if command == 'validate'
|
13
|
+
validate!(argv)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.validate!(argv)
|
18
|
+
options = {}
|
19
|
+
|
20
|
+
parser = OptionParser.new do |opts|
|
21
|
+
opts.banner = 'Usage: bin/codeownership validate [options]'
|
22
|
+
|
23
|
+
opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .github/CODEOWNERS file') do
|
24
|
+
options[:skip_autocorrect] = true
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-d', '--diff', 'Only run validations with staged files') do
|
28
|
+
options[:diff] = true
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('-s', '--skip-stage', 'Skips staging the CODEOWNERS file') do
|
32
|
+
options[:skip_stage] = true
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on('--help', 'Shows this prompt') do
|
36
|
+
puts opts
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
end
|
40
|
+
args = parser.order!(argv) {}
|
41
|
+
parser.parse!(args)
|
42
|
+
|
43
|
+
files = if options[:diff]
|
44
|
+
ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
|
45
|
+
File.exist?(file)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
Private.tracked_files
|
49
|
+
end
|
50
|
+
|
51
|
+
CodeOwnership.validate!(
|
52
|
+
files: files,
|
53
|
+
autocorrect: !options[:skip_autocorrect],
|
54
|
+
stage_changes: !options[:skip_stage]
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
private_class_method :validate!
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module CodeOwnership
|
4
|
+
module Private
|
5
|
+
class Configuration < T::Struct
|
6
|
+
extend T::Sig
|
7
|
+
DEFAULT_JS_PACKAGE_PATHS = T.let(['**/'], T::Array[String])
|
8
|
+
|
9
|
+
const :owned_globs, T::Array[String]
|
10
|
+
const :unowned_globs, T::Array[String]
|
11
|
+
const :js_package_paths, T::Array[String]
|
12
|
+
const :skip_codeowners_validation, T::Boolean
|
13
|
+
|
14
|
+
sig { returns(Configuration) }
|
15
|
+
def self.fetch
|
16
|
+
config_hash = YAML.load_file('config/code_ownership.yml')
|
17
|
+
|
18
|
+
new(
|
19
|
+
owned_globs: config_hash.fetch('owned_globs', []),
|
20
|
+
unowned_globs: config_hash.fetch('unowned_globs', []),
|
21
|
+
js_package_paths: js_package_paths(config_hash),
|
22
|
+
skip_codeowners_validation: config_hash.fetch('skip_codeowners_validation', false)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { params(config_hash: T::Hash[T.untyped, T.untyped]).returns(T::Array[String]) }
|
27
|
+
def self.js_package_paths(config_hash)
|
28
|
+
specified_package_paths = config_hash['js_package_paths']
|
29
|
+
if specified_package_paths.nil?
|
30
|
+
DEFAULT_JS_PACKAGE_PATHS.dup
|
31
|
+
else
|
32
|
+
Array(specified_package_paths)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module OwnershipMappers
|
8
|
+
# Calculate, cache, and return a mapping of file names (relative to the root
|
9
|
+
# of the repository) to team name.
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# {
|
14
|
+
# 'app/models/company.rb' => Team.find('Setup & Onboarding'),
|
15
|
+
# ...
|
16
|
+
# }
|
17
|
+
class FileAnnotations
|
18
|
+
extend T::Sig
|
19
|
+
include Interface
|
20
|
+
|
21
|
+
@@map_files_to_owners = T.let({}, T.nilable(T::Hash[String, T.nilable(::Teams::Team)])) # rubocop:disable Style/ClassVars
|
22
|
+
|
23
|
+
TEAM_PATTERN = T.let(/\A(?:#|\/\/) @team (?<team>.*)\Z/.freeze, Regexp)
|
24
|
+
|
25
|
+
sig do
|
26
|
+
override.params(file: String).
|
27
|
+
returns(T.nilable(::Teams::Team))
|
28
|
+
end
|
29
|
+
def map_file_to_owner(file)
|
30
|
+
file_annotation_based_owner(file)
|
31
|
+
end
|
32
|
+
|
33
|
+
sig do
|
34
|
+
override.
|
35
|
+
params(files: T::Array[String]).
|
36
|
+
returns(T::Hash[String, T.nilable(::Teams::Team)])
|
37
|
+
end
|
38
|
+
def map_files_to_owners(files)
|
39
|
+
return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count > 0
|
40
|
+
|
41
|
+
@@map_files_to_owners = files.each_with_object({}) do |filename_relative_to_root, mapping| # rubocop:disable Style/ClassVars
|
42
|
+
owner = file_annotation_based_owner(filename_relative_to_root)
|
43
|
+
next unless owner
|
44
|
+
|
45
|
+
mapping[filename_relative_to_root] = owner
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(filename: String).returns(T.nilable(Teams::Team)) }
|
50
|
+
def file_annotation_based_owner(filename)
|
51
|
+
# If for a directory is named with an ownable extension, we need to skip
|
52
|
+
# so File.foreach doesn't blow up below. This was needed because Cypress
|
53
|
+
# screenshots are saved to a folder with the test suite filename.
|
54
|
+
return if File.directory?(filename)
|
55
|
+
return unless File.file?(filename)
|
56
|
+
|
57
|
+
# The annotation should be on line 1 but as of this comment
|
58
|
+
# there's no linter installed to enforce that. We therefore check the
|
59
|
+
# first line (the Ruby VM makes a single `read(1)` call for 8KB),
|
60
|
+
# and if the annotation isn't in the first two lines we assume it
|
61
|
+
# doesn't exist.
|
62
|
+
|
63
|
+
line_1 = File.foreach(filename).first
|
64
|
+
|
65
|
+
return if !line_1
|
66
|
+
|
67
|
+
begin
|
68
|
+
team = line_1[TEAM_PATTERN, :team]
|
69
|
+
rescue ArgumentError => ex
|
70
|
+
if ex.message.include?('invalid byte sequence')
|
71
|
+
team = nil
|
72
|
+
else
|
73
|
+
raise
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
return unless team
|
78
|
+
|
79
|
+
Private.find_team!(
|
80
|
+
team,
|
81
|
+
filename
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
sig { params(filename: String).void }
|
86
|
+
def remove_file_annotation!(filename)
|
87
|
+
if file_annotation_based_owner(filename)
|
88
|
+
filepath = Pathname.new(filename)
|
89
|
+
lines = filepath.read.split("\n")
|
90
|
+
new_lines = lines.select { |line| !line[TEAM_PATTERN] }
|
91
|
+
# We explicitly add a final new line since splitting by new line when reading the file lines
|
92
|
+
# ignores new lines at the ends of files
|
93
|
+
# We also remove leading new lines, since there is after a new line after an annotation
|
94
|
+
new_file_contents = "#{new_lines.join("\n")}\n".gsub(/\A\n+/, '')
|
95
|
+
filepath.write(new_file_contents)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
sig do
|
100
|
+
override.returns(T::Hash[String, T.nilable(::Teams::Team)])
|
101
|
+
end
|
102
|
+
def codeowners_lines_to_owners
|
103
|
+
@@map_files_to_owners = nil # rubocop:disable Style/ClassVars
|
104
|
+
map_files_to_owners(Private.tracked_files)
|
105
|
+
end
|
106
|
+
|
107
|
+
sig { override.returns(String) }
|
108
|
+
def description
|
109
|
+
'Annotations at the top of file'
|
110
|
+
end
|
111
|
+
|
112
|
+
sig { override.void }
|
113
|
+
def bust_caches!
|
114
|
+
@@map_files_to_owners = {} # rubocop:disable Style/ClassVars
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module OwnershipMappers
|
8
|
+
module Interface
|
9
|
+
extend T::Sig
|
10
|
+
extend T::Helpers
|
11
|
+
|
12
|
+
interface!
|
13
|
+
|
14
|
+
#
|
15
|
+
# This should be fast when run with ONE file
|
16
|
+
#
|
17
|
+
sig do
|
18
|
+
abstract.params(file: String).
|
19
|
+
returns(T.nilable(::Teams::Team))
|
20
|
+
end
|
21
|
+
def map_file_to_owner(file)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# This should be fast when run with MANY files
|
26
|
+
#
|
27
|
+
sig do
|
28
|
+
abstract.params(files: T::Array[String]).
|
29
|
+
returns(T::Hash[String, T.nilable(::Teams::Team)])
|
30
|
+
end
|
31
|
+
def map_files_to_owners(files)
|
32
|
+
end
|
33
|
+
|
34
|
+
sig do
|
35
|
+
abstract.returns(T::Hash[String, T.nilable(::Teams::Team)])
|
36
|
+
end
|
37
|
+
def codeowners_lines_to_owners
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { abstract.returns(String) }
|
41
|
+
def description
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { abstract.void }
|
45
|
+
def bust_caches!
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module OwnershipMappers
|
8
|
+
class JsPackageOwnership
|
9
|
+
extend T::Sig
|
10
|
+
include Interface
|
11
|
+
|
12
|
+
@@package_json_cache = T.let({}, T::Hash[String, T.nilable(ParseJsPackages::Package)]) # rubocop:disable Style/ClassVars
|
13
|
+
|
14
|
+
sig do
|
15
|
+
override.params(file: String).
|
16
|
+
returns(T.nilable(::Teams::Team))
|
17
|
+
end
|
18
|
+
def map_file_to_owner(file)
|
19
|
+
package = map_file_to_relevant_package(file)
|
20
|
+
|
21
|
+
return nil if package.nil?
|
22
|
+
|
23
|
+
owner_for_package(package)
|
24
|
+
end
|
25
|
+
|
26
|
+
sig do
|
27
|
+
override.
|
28
|
+
params(files: T::Array[String]).
|
29
|
+
returns(T::Hash[String, T.nilable(::Teams::Team)])
|
30
|
+
end
|
31
|
+
def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
|
32
|
+
ParseJsPackages.all.each_with_object({}) do |package, res|
|
33
|
+
owner = owner_for_package(package)
|
34
|
+
next if owner.nil?
|
35
|
+
|
36
|
+
glob = package.directory.join('**/**').to_s
|
37
|
+
Dir.glob(glob).each do |path|
|
38
|
+
res[path] = owner
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Package ownership ignores the passed in files when generating code owners lines.
|
45
|
+
# This is because Package ownership knows that the fastest way to find code owners for package based ownership
|
46
|
+
# is to simply iterate over the packages and grab the owner, rather than iterating over each file just to get what package it is in
|
47
|
+
# In theory this means that we may generate code owners lines that cover files that are not in the passed in argument,
|
48
|
+
# but in practice this is not of consequence because in reality we never really want to generate code owners for only a
|
49
|
+
# subset of files, but rather we want code ownership for all files.
|
50
|
+
#
|
51
|
+
sig do
|
52
|
+
override.returns(T::Hash[String, T.nilable(::Teams::Team)])
|
53
|
+
end
|
54
|
+
def codeowners_lines_to_owners
|
55
|
+
ParseJsPackages.all.each_with_object({}) do |package, res|
|
56
|
+
owner = owner_for_package(package)
|
57
|
+
next if owner.nil?
|
58
|
+
|
59
|
+
res[package.directory.join('**/**').to_s] = owner
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { override.returns(String) }
|
64
|
+
def description
|
65
|
+
'Owner metadata key in package.json'
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(package: ParseJsPackages::Package).returns(T.nilable(Teams::Team)) }
|
69
|
+
def owner_for_package(package)
|
70
|
+
raw_owner_value = package.metadata['owner']
|
71
|
+
return nil if !raw_owner_value
|
72
|
+
|
73
|
+
Private.find_team!(
|
74
|
+
raw_owner_value,
|
75
|
+
package.name
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { override.void }
|
80
|
+
def bust_caches!
|
81
|
+
@@package_json_cache = {} # rubocop:disable Style/ClassVars
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# takes a file and finds the relevant `package.json` file by walking up the directory
|
87
|
+
# structure. Example, given `packages/a/b/c.rb`, this looks for `packages/a/b/package.json`, `packages/a/package.json`,
|
88
|
+
# `packages/package.json`, and `package.json` in that order, stopping at the first file to actually exist.
|
89
|
+
# We do additional caching so that we don't have to check for file existence every time
|
90
|
+
sig { params(file: String).returns(T.nilable(ParseJsPackages::Package)) }
|
91
|
+
def map_file_to_relevant_package(file)
|
92
|
+
file_path = Pathname.new(file)
|
93
|
+
path_components = file_path.each_filename.to_a.map { |path| Pathname.new(path) }
|
94
|
+
|
95
|
+
(path_components.length - 1).downto(0).each do |i|
|
96
|
+
potential_relative_path_name = T.must(path_components[0...i]).reduce(Pathname.new('')) { |built_path, path| built_path.join(path) }
|
97
|
+
potential_package_json_path = potential_relative_path_name.
|
98
|
+
join(ParseJsPackages::PACKAGE_JSON_NAME)
|
99
|
+
|
100
|
+
potential_package_json_string = potential_package_json_path.to_s
|
101
|
+
|
102
|
+
package = nil
|
103
|
+
if @@package_json_cache.key?(potential_package_json_string)
|
104
|
+
package = @@package_json_cache[potential_package_json_string]
|
105
|
+
elsif potential_package_json_path.exist?
|
106
|
+
package = ParseJsPackages::Package.from(potential_package_json_path)
|
107
|
+
|
108
|
+
@@package_json_cache[potential_package_json_string] = package
|
109
|
+
else
|
110
|
+
@@package_json_cache[potential_package_json_string] = nil
|
111
|
+
end
|
112
|
+
|
113
|
+
return package unless package.nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module OwnershipMappers
|
8
|
+
class PackageOwnership
|
9
|
+
extend T::Sig
|
10
|
+
include Interface
|
11
|
+
|
12
|
+
@@package_yml_cache = T.let({}, T::Hash[String, T.nilable(ParsePackwerk::Package)]) # rubocop:disable Style/ClassVars
|
13
|
+
|
14
|
+
sig do
|
15
|
+
override.params(file: String).
|
16
|
+
returns(T.nilable(::Teams::Team))
|
17
|
+
end
|
18
|
+
def map_file_to_owner(file)
|
19
|
+
package = map_file_to_relevant_package(file)
|
20
|
+
|
21
|
+
return nil if package.nil?
|
22
|
+
|
23
|
+
owner_for_package(package)
|
24
|
+
end
|
25
|
+
|
26
|
+
sig do
|
27
|
+
override.
|
28
|
+
params(files: T::Array[String]).
|
29
|
+
returns(T::Hash[String, T.nilable(::Teams::Team)])
|
30
|
+
end
|
31
|
+
def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
|
32
|
+
ParsePackwerk.all.each_with_object({}) do |package, res|
|
33
|
+
owner = owner_for_package(package)
|
34
|
+
next if owner.nil?
|
35
|
+
|
36
|
+
glob = package.directory.join('**/**').to_s
|
37
|
+
Dir.glob(glob).each do |path|
|
38
|
+
res[path] = owner
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Package ownership ignores the passed in files when generating code owners lines.
|
45
|
+
# This is because Package ownership knows that the fastest way to find code owners for package based ownership
|
46
|
+
# is to simply iterate over the packages and grab the owner, rather than iterating over each file just to get what package it is in
|
47
|
+
# In theory this means that we may generate code owners lines that cover files that are not in the passed in argument,
|
48
|
+
# but in practice this is not of consequence because in reality we never really want to generate code owners for only a
|
49
|
+
# subset of files, but rather we want code ownership for all files.
|
50
|
+
#
|
51
|
+
sig do
|
52
|
+
override.returns(T::Hash[String, T.nilable(::Teams::Team)])
|
53
|
+
end
|
54
|
+
def codeowners_lines_to_owners
|
55
|
+
ParsePackwerk.all.each_with_object({}) do |package, res|
|
56
|
+
owner = owner_for_package(package)
|
57
|
+
next if owner.nil?
|
58
|
+
|
59
|
+
res[package.directory.join('**/**').to_s] = owner
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { override.returns(String) }
|
64
|
+
def description
|
65
|
+
'Owner metadata key in package.yml'
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(package: ParsePackwerk::Package).returns(T.nilable(Teams::Team)) }
|
69
|
+
def owner_for_package(package)
|
70
|
+
raw_owner_value = package.metadata['owner']
|
71
|
+
return nil if !raw_owner_value
|
72
|
+
|
73
|
+
Private.find_team!(
|
74
|
+
raw_owner_value,
|
75
|
+
package.yml.to_s
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { override.void }
|
80
|
+
def bust_caches!
|
81
|
+
@@package_yml_cache = {} # rubocop:disable Style/ClassVars
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# takes a file and finds the relevant `package.yml` file by walking up the directory
|
87
|
+
# structure. Example, given `packs/a/b/c.rb`, this looks for `packs/a/b/package.yml`, `packs/a/package.yml`,
|
88
|
+
# `packs/package.yml`, and `package.yml` in that order, stopping at the first file to actually exist.
|
89
|
+
# We do additional caching so that we don't have to check for file existence every time
|
90
|
+
sig { params(file: String).returns(T.nilable(ParsePackwerk::Package)) }
|
91
|
+
def map_file_to_relevant_package(file)
|
92
|
+
file_path = Pathname.new(file)
|
93
|
+
path_components = file_path.each_filename.to_a.map { |path| Pathname.new(path) }
|
94
|
+
|
95
|
+
(path_components.length - 1).downto(0).each do |i|
|
96
|
+
potential_relative_path_name = T.must(path_components[0...i]).reduce(Pathname.new('')) { |built_path, path| built_path.join(path) }
|
97
|
+
potential_package_yml_path = potential_relative_path_name.
|
98
|
+
join(ParsePackwerk::PACKAGE_YML_NAME)
|
99
|
+
|
100
|
+
potential_package_yml_string = potential_package_yml_path.to_s
|
101
|
+
|
102
|
+
package = nil
|
103
|
+
if @@package_yml_cache.key?(potential_package_yml_string)
|
104
|
+
package = @@package_yml_cache[potential_package_yml_string]
|
105
|
+
elsif potential_package_yml_path.exist?
|
106
|
+
package = ParsePackwerk::Package.from(potential_package_yml_path)
|
107
|
+
|
108
|
+
@@package_yml_cache[potential_package_yml_string] = package
|
109
|
+
else
|
110
|
+
@@package_yml_cache[potential_package_yml_string] = nil
|
111
|
+
end
|
112
|
+
|
113
|
+
return package unless package.nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module OwnershipMappers
|
8
|
+
class TeamGlobs
|
9
|
+
extend T::Sig
|
10
|
+
include Interface
|
11
|
+
|
12
|
+
@@map_files_to_owners = T.let(@map_files_to_owners, T.nilable(T::Hash[String, T.nilable(::Teams::Team)])) # rubocop:disable Style/ClassVars
|
13
|
+
@@map_files_to_owners = {} # rubocop:disable Style/ClassVars
|
14
|
+
@@codeowners_lines_to_owners = T.let(@codeowners_lines_to_owners, T.nilable(T::Hash[String, T.nilable(::Teams::Team)])) # rubocop:disable Style/ClassVars
|
15
|
+
@@codeowners_lines_to_owners = {} # rubocop:disable Style/ClassVars
|
16
|
+
|
17
|
+
sig do
|
18
|
+
override.
|
19
|
+
params(files: T::Array[String]).
|
20
|
+
returns(T::Hash[String, T.nilable(::Teams::Team)])
|
21
|
+
end
|
22
|
+
def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
|
23
|
+
return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count > 0
|
24
|
+
|
25
|
+
@@map_files_to_owners = Teams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
|
26
|
+
TeamPlugins::Ownership.for(team).owned_globs.each do |glob|
|
27
|
+
Dir.glob(glob).each do |filename|
|
28
|
+
map[filename] = team
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
sig do
|
35
|
+
override.params(file: String).
|
36
|
+
returns(T.nilable(::Teams::Team))
|
37
|
+
end
|
38
|
+
def map_file_to_owner(file)
|
39
|
+
map_files_to_owners([file])[file]
|
40
|
+
end
|
41
|
+
|
42
|
+
sig do
|
43
|
+
override.returns(T::Hash[String, T.nilable(::Teams::Team)])
|
44
|
+
end
|
45
|
+
def codeowners_lines_to_owners
|
46
|
+
return @@codeowners_lines_to_owners if @@codeowners_lines_to_owners&.keys && @@codeowners_lines_to_owners.keys.count > 0
|
47
|
+
|
48
|
+
@@codeowners_lines_to_owners = Teams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
|
49
|
+
TeamPlugins::Ownership.for(team).owned_globs.each do |owned_glob|
|
50
|
+
map[owned_glob] = team
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { override.void }
|
56
|
+
def bust_caches!
|
57
|
+
@@codeowners_lines_to_owners = {} # rubocop:disable Style/ClassVars
|
58
|
+
@@map_files_to_owners = {} # rubocop:disable Style/ClassVars
|
59
|
+
end
|
60
|
+
|
61
|
+
sig { override.returns(String) }
|
62
|
+
def description
|
63
|
+
'Team-specific owned globs'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|