code_ownership 1.23.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|