code_ownership 1.36.2 → 1.37.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 +4 -4
- data/README.md +24 -0
- data/lib/code_ownership/cli.rb +19 -22
- data/lib/code_ownership/configuration.rb +2 -2
- data/lib/code_ownership/mapper.rb +10 -14
- data/lib/code_ownership/private/codeowners_file.rb +18 -14
- data/lib/code_ownership/private/extension_loader.rb +1 -1
- data/lib/code_ownership/private/glob_cache.rb +11 -35
- data/lib/code_ownership/private/ownership_mappers/directory_ownership.rb +29 -8
- data/lib/code_ownership/private/ownership_mappers/file_annotations.rb +18 -19
- data/lib/code_ownership/private/ownership_mappers/js_package_ownership.rb +6 -6
- data/lib/code_ownership/private/ownership_mappers/package_ownership.rb +5 -6
- data/lib/code_ownership/private/ownership_mappers/team_globs.rb +14 -16
- data/lib/code_ownership/private/ownership_mappers/team_yml_ownership.rb +9 -9
- data/lib/code_ownership/private/parse_js_packages.rb +5 -5
- data/lib/code_ownership/private/permit_pack_owner_top_level_key.rb +0 -1
- data/lib/code_ownership/private/validations/files_have_owners.rb +3 -3
- data/lib/code_ownership/private/validations/files_have_unique_owners.rb +1 -1
- data/lib/code_ownership/private/validations/github_codeowners_up_to_date.rb +24 -24
- data/lib/code_ownership/private.rb +6 -8
- data/lib/code_ownership/validator.rb +1 -2
- data/lib/code_ownership.rb +14 -12
- metadata +31 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c854c774447149078df69925e183be064e8ac92ea464da9eea83cec2cfb10931
|
4
|
+
data.tar.gz: 5a42650db0cdc8f14fd8878a0c70587b21a722a284a45104051062464f73a4a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25025e91fa585614f864ab1f84d829037b5769cd491dcca93bf822e25ecb3222388d8d9315bf7dd3fe5a645645855c391028a9df01592199ce0857d47276656e
|
7
|
+
data.tar.gz: 8f8035f102b184ac7ea866df6bff7cee92f0bd2fe5337b49956c85e2ead483a5868c0969030392c63dda1575f9bf33ae085058c0114ef027140b3bf629a60895
|
data/README.md
CHANGED
@@ -8,6 +8,30 @@ Check out [`code_ownership_spec.rb`](https://github.com/rubyatscale/code_ownersh
|
|
8
8
|
|
9
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
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
|
+
|
11
35
|
## Usage: Declaring Ownership
|
12
36
|
|
13
37
|
There are three ways to declare code ownership using this gem.
|
data/lib/code_ownership/cli.rb
CHANGED
@@ -14,7 +14,7 @@ module CodeOwnership
|
|
14
14
|
for_file(argv)
|
15
15
|
elsif command == 'for_team'
|
16
16
|
for_team(argv)
|
17
|
-
elsif [nil,
|
17
|
+
elsif [nil, 'help'].include?(command)
|
18
18
|
puts <<~USAGE
|
19
19
|
Usage: bin/codeownership <subcommand>
|
20
20
|
|
@@ -27,7 +27,6 @@ module CodeOwnership
|
|
27
27
|
else
|
28
28
|
puts "'#{command}' is not a code_ownership command. See `bin/codeownership help`."
|
29
29
|
end
|
30
|
-
|
31
30
|
end
|
32
31
|
|
33
32
|
def self.validate!(argv)
|
@@ -53,16 +52,16 @@ module CodeOwnership
|
|
53
52
|
exit
|
54
53
|
end
|
55
54
|
end
|
56
|
-
args = parser.order!(argv)
|
55
|
+
args = parser.order!(argv)
|
57
56
|
parser.parse!(args)
|
58
57
|
|
59
58
|
files = if options[:diff]
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
59
|
+
ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
|
60
|
+
File.exist?(file)
|
61
|
+
end
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
66
65
|
|
67
66
|
CodeOwnership.validate!(
|
68
67
|
files: files,
|
@@ -79,7 +78,7 @@ module CodeOwnership
|
|
79
78
|
# Long-term, we probably want to use something like `thor` so we don't have to implement logic
|
80
79
|
# like this. In the short-term, this is a simple way for us to use the built-in OptionParser
|
81
80
|
# while having an ergonomic CLI.
|
82
|
-
files = argv.
|
81
|
+
files = argv.reject { |arg| arg.start_with?('--') }
|
83
82
|
|
84
83
|
parser = OptionParser.new do |opts|
|
85
84
|
opts.banner = 'Usage: bin/codeownership for_file [options]'
|
@@ -93,22 +92,22 @@ module CodeOwnership
|
|
93
92
|
exit
|
94
93
|
end
|
95
94
|
end
|
96
|
-
args = parser.order!(argv)
|
95
|
+
args = parser.order!(argv)
|
97
96
|
parser.parse!(args)
|
98
97
|
|
99
98
|
if files.count != 1
|
100
|
-
raise
|
99
|
+
raise 'Please pass in one file. Use `bin/codeownership for_file --help` for more info'
|
101
100
|
end
|
102
|
-
|
101
|
+
|
103
102
|
team = CodeOwnership.for_file(files.first)
|
104
103
|
|
105
|
-
team_name = team&.name ||
|
106
|
-
team_yml = team&.config_yml ||
|
104
|
+
team_name = team&.name || 'Unowned'
|
105
|
+
team_yml = team&.config_yml || 'Unowned'
|
107
106
|
|
108
107
|
if options[:json]
|
109
108
|
json = {
|
110
109
|
team_name: team_name,
|
111
|
-
team_yml: team_yml
|
110
|
+
team_yml: team_yml
|
112
111
|
}
|
113
112
|
|
114
113
|
puts json.to_json
|
@@ -121,8 +120,6 @@ module CodeOwnership
|
|
121
120
|
end
|
122
121
|
|
123
122
|
def self.for_team(argv)
|
124
|
-
options = {}
|
125
|
-
|
126
123
|
parser = OptionParser.new do |opts|
|
127
124
|
opts.banner = 'Usage: bin/codeownership for_team \'Team Name\''
|
128
125
|
|
@@ -131,14 +128,14 @@ module CodeOwnership
|
|
131
128
|
exit
|
132
129
|
end
|
133
130
|
end
|
134
|
-
teams = argv.
|
135
|
-
args = parser.order!(argv)
|
131
|
+
teams = argv.reject { |arg| arg.start_with?('--') }
|
132
|
+
args = parser.order!(argv)
|
136
133
|
parser.parse!(args)
|
137
134
|
|
138
135
|
if teams.count != 1
|
139
|
-
raise
|
136
|
+
raise 'Please pass in one team. Use `bin/codeownership for_team --help` for more info'
|
140
137
|
end
|
141
|
-
|
138
|
+
|
142
139
|
puts CodeOwnership.for_team(teams.first)
|
143
140
|
end
|
144
141
|
|
@@ -17,8 +17,8 @@ module CodeOwnership
|
|
17
17
|
def self.fetch
|
18
18
|
config_hash = YAML.load_file('config/code_ownership.yml')
|
19
19
|
|
20
|
-
if config_hash.key?(
|
21
|
-
config_hash[
|
20
|
+
if config_hash.key?('require')
|
21
|
+
config_hash['require'].each do |require_directive|
|
22
22
|
Private::ExtensionLoader.load(require_directive)
|
23
23
|
end
|
24
24
|
end
|
@@ -29,21 +29,19 @@ module CodeOwnership
|
|
29
29
|
# This should be fast when run with ONE file
|
30
30
|
#
|
31
31
|
sig do
|
32
|
-
abstract.params(file: String)
|
33
|
-
returns(T.nilable(::CodeTeams::Team))
|
34
|
-
end
|
35
|
-
def map_file_to_owner(file)
|
32
|
+
abstract.params(file: String)
|
33
|
+
.returns(T.nilable(::CodeTeams::Team))
|
36
34
|
end
|
35
|
+
def map_file_to_owner(file); end
|
37
36
|
|
38
37
|
#
|
39
38
|
# This should be fast when run with MANY files
|
40
39
|
#
|
41
40
|
sig do
|
42
|
-
abstract.params(files: T::Array[String])
|
43
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
44
|
-
end
|
45
|
-
def globs_to_owner(files)
|
41
|
+
abstract.params(files: T::Array[String])
|
42
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
46
43
|
end
|
44
|
+
def globs_to_owner(files); end
|
47
45
|
|
48
46
|
#
|
49
47
|
# This should be fast when run with MANY files
|
@@ -51,16 +49,13 @@ module CodeOwnership
|
|
51
49
|
sig do
|
52
50
|
abstract.params(cache: GlobsToOwningTeamMap, files: T::Array[String]).returns(GlobsToOwningTeamMap)
|
53
51
|
end
|
54
|
-
def update_cache(cache, files)
|
55
|
-
end
|
52
|
+
def update_cache(cache, files); end
|
56
53
|
|
57
54
|
sig { abstract.returns(String) }
|
58
|
-
def description
|
59
|
-
end
|
55
|
+
def description; end
|
60
56
|
|
61
57
|
sig { abstract.void }
|
62
|
-
def bust_caches
|
63
|
-
end
|
58
|
+
def bust_caches!; end
|
64
59
|
|
65
60
|
sig { returns(Private::GlobCache) }
|
66
61
|
def self.to_glob_cache
|
@@ -72,6 +67,7 @@ module CodeOwnership
|
|
72
67
|
|
73
68
|
mapped_files.each do |glob, owner|
|
74
69
|
next if owner.nil?
|
70
|
+
|
75
71
|
glob_to_owner_map_by_mapper_description.fetch(mapper.description)[glob] = owner
|
76
72
|
end
|
77
73
|
end
|
@@ -13,15 +13,15 @@ module CodeOwnership
|
|
13
13
|
|
14
14
|
sig { returns(T::Array[String]) }
|
15
15
|
def self.actual_contents_lines
|
16
|
-
if
|
17
|
-
[""]
|
18
|
-
else
|
16
|
+
if path.exist?
|
19
17
|
content = path.read
|
20
18
|
lines = path.read.split("\n")
|
21
19
|
if content.end_with?("\n")
|
22
|
-
lines <<
|
20
|
+
lines << ''
|
23
21
|
end
|
24
22
|
lines
|
23
|
+
else
|
24
|
+
['']
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
@@ -53,7 +53,7 @@ module CodeOwnership
|
|
53
53
|
|
54
54
|
cache.each do |mapper_description, ownership_map_cache|
|
55
55
|
ownership_entries = []
|
56
|
-
sorted_ownership_map_cache = ownership_map_cache.sort_by do |glob,
|
56
|
+
sorted_ownership_map_cache = ownership_map_cache.sort_by do |glob, _team|
|
57
57
|
glob
|
58
58
|
end
|
59
59
|
sorted_ownership_map_cache.to_h.each do |path, code_team|
|
@@ -64,22 +64,23 @@ module CodeOwnership
|
|
64
64
|
# 1) It allows the CODEOWNERS file to be used as a cache for validations
|
65
65
|
# 2) It allows users to specifically see what their team will not be notified about.
|
66
66
|
entry = if ignored_teams.include?(code_team.name)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
67
|
+
"# /#{path} #{team_mapping}"
|
68
|
+
else
|
69
|
+
"/#{path} #{team_mapping}"
|
70
|
+
end
|
71
71
|
ownership_entries << entry
|
72
72
|
end
|
73
73
|
|
74
74
|
next if ownership_entries.none?
|
75
|
+
|
75
76
|
codeowners_file_lines += ['', "# #{mapper_description}", *ownership_entries.sort]
|
76
77
|
end
|
77
78
|
|
78
79
|
[
|
79
80
|
*header.split("\n"),
|
80
|
-
|
81
|
+
'', # For line between header and codeowners_file_lines
|
81
82
|
*codeowners_file_lines,
|
82
|
-
|
83
|
+
'' # For end-of-file newline
|
83
84
|
]
|
84
85
|
end
|
85
86
|
|
@@ -123,20 +124,22 @@ module CodeOwnership
|
|
123
124
|
mapper_descriptions = Set.new(Mapper.all.map(&:description))
|
124
125
|
|
125
126
|
path.readlines.each do |line|
|
126
|
-
line_with_no_comment = line.chomp.gsub(
|
127
|
+
line_with_no_comment = line.chomp.gsub('# ', '')
|
127
128
|
if mapper_descriptions.include?(line_with_no_comment)
|
128
129
|
current_mapper = line_with_no_comment
|
129
130
|
else
|
130
131
|
next if current_mapper.nil?
|
131
|
-
next if line.chomp ==
|
132
|
+
next if line.chomp == ''
|
133
|
+
|
132
134
|
# The codeowners file stores paths relative to the root of directory
|
133
135
|
# Since a `/` means root of the file system from the perspective of ruby,
|
134
136
|
# we remove that beginning slash so we can correctly glob the files out.
|
135
|
-
normalized_line = line.gsub(/^# /, '').gsub(
|
137
|
+
normalized_line = line.gsub(/^# /, '').gsub(%r{^/}, '')
|
136
138
|
split_line = normalized_line.split
|
137
139
|
# Most lines will be in the format: /path/to/file my-github-team
|
138
140
|
# This will skip over lines that are not of the correct form
|
139
141
|
next if split_line.count > 2
|
142
|
+
|
140
143
|
entry, github_team = split_line
|
141
144
|
code_team = github_team_to_code_team_map[T.must(github_team)]
|
142
145
|
# If a GitHub team is changed and a user runs `bin/codeownership validate`, we won't be able to identify
|
@@ -144,6 +147,7 @@ module CodeOwnership
|
|
144
147
|
# Therefore, if we can't determine the team, we just skip it.
|
145
148
|
# This affects how complete the cache is, but that will still be caught by `bin/codeownership validate`.
|
146
149
|
next if code_team.nil?
|
150
|
+
|
147
151
|
raw_cache_contents[current_mapper] ||= {}
|
148
152
|
raw_cache_contents.fetch(current_mapper)[T.must(entry)] = github_team_to_code_team_map.fetch(T.must(github_team))
|
149
153
|
end
|
@@ -12,7 +12,7 @@ module CodeOwnership
|
|
12
12
|
def load(require_directive)
|
13
13
|
# We want to transform the require directive to behave differently
|
14
14
|
# if it's a specific local file being required versus a gem
|
15
|
-
if require_directive.start_with?(
|
15
|
+
if require_directive.start_with?('.')
|
16
16
|
require File.join(Pathname.pwd, require_directive)
|
17
17
|
else
|
18
18
|
require require_directive
|
@@ -34,14 +34,15 @@ module CodeOwnership
|
|
34
34
|
|
35
35
|
sig { params(files: T::Array[String]).returns(FilesByMapper) }
|
36
36
|
def mapper_descriptions_that_map_files(files)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
files_by_mappers_via_file_fnmatch(files)
|
37
|
+
files_by_mappers = files.to_h { |f| [f, Set.new([])] }
|
38
|
+
|
39
|
+
files_by_mappers_via_expanded_cache.each do |file, mappers|
|
40
|
+
mappers.each do |mapper|
|
41
|
+
T.must(files_by_mappers[file]) << mapper if files_by_mappers[file]
|
42
|
+
end
|
44
43
|
end
|
44
|
+
|
45
|
+
files_by_mappers
|
45
46
|
end
|
46
47
|
|
47
48
|
private
|
@@ -61,11 +62,11 @@ module CodeOwnership
|
|
61
62
|
|
62
63
|
sig { returns(FilesByMapper) }
|
63
64
|
def files_by_mappers_via_expanded_cache
|
64
|
-
@
|
65
|
-
@
|
65
|
+
@files_by_mappers_via_expanded_cache ||= T.let(@files_by_mappers_via_expanded_cache, T.nilable(FilesByMapper))
|
66
|
+
@files_by_mappers_via_expanded_cache ||= begin
|
66
67
|
files_by_mappers = T.let({}, FilesByMapper)
|
67
68
|
expanded_cache.each do |mapper_description, file_by_owner|
|
68
|
-
file_by_owner.
|
69
|
+
file_by_owner.each_key do |file|
|
69
70
|
files_by_mappers[file] ||= Set.new([])
|
70
71
|
files_by_mappers.fetch(file) << mapper_description
|
71
72
|
end
|
@@ -74,31 +75,6 @@ module CodeOwnership
|
|
74
75
|
files_by_mappers
|
75
76
|
end
|
76
77
|
end
|
77
|
-
|
78
|
-
sig { params(files: T::Array[String]).returns(FilesByMapper) }
|
79
|
-
def files_by_mappers_via_file_fnmatch(files)
|
80
|
-
files_by_mappers = T.let({}, FilesByMapper)
|
81
|
-
|
82
|
-
files.each do |file|
|
83
|
-
files_by_mappers[file] ||= Set.new([])
|
84
|
-
@raw_cache_contents.each do |mapper_description, globs_by_owner|
|
85
|
-
# As much as I'd like to *not* special case the file annotations mapper, using File.fnmatch? on the thousands of files mapped by the
|
86
|
-
# file annotations mapper is a lot of unnecessary extra work.
|
87
|
-
# Therefore we can just check if the file is in the globs directly for file annotations, otherwise use File.fnmatch
|
88
|
-
if mapper_description == OwnershipMappers::FileAnnotations::DESCRIPTION
|
89
|
-
files_by_mappers.fetch(file) << mapper_description if globs_by_owner[file]
|
90
|
-
else
|
91
|
-
globs_by_owner.each do |glob, owner|
|
92
|
-
if File.fnmatch?(glob, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
93
|
-
files_by_mappers.fetch(file) << mapper_description
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
files_by_mappers
|
101
|
-
end
|
102
78
|
end
|
103
79
|
end
|
104
80
|
end
|
@@ -14,8 +14,8 @@ module CodeOwnership
|
|
14
14
|
@@directory_cache = T.let({}, T::Hash[String, T.nilable(CodeTeams::Team)]) # rubocop:disable Style/ClassVars
|
15
15
|
|
16
16
|
sig do
|
17
|
-
override.params(file: String)
|
18
|
-
returns(T.nilable(::CodeTeams::Team))
|
17
|
+
override.params(file: String)
|
18
|
+
.returns(T.nilable(::CodeTeams::Team))
|
19
19
|
end
|
20
20
|
def map_file_to_owner(file)
|
21
21
|
map_file_to_relevant_owner(file)
|
@@ -37,8 +37,8 @@ module CodeOwnership
|
|
37
37
|
# subset of files, but rather we want code ownership for all files.
|
38
38
|
#
|
39
39
|
sig do
|
40
|
-
override.params(files: T::Array[String])
|
41
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
40
|
+
override.params(files: T::Array[String])
|
41
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
42
42
|
end
|
43
43
|
def globs_to_owner(files)
|
44
44
|
# The T.unsafe is because the upstream RBI is wrong for Pathname.glob
|
@@ -48,7 +48,8 @@ module CodeOwnership
|
|
48
48
|
.map(&:cleanpath)
|
49
49
|
.each_with_object({}) do |pathname, res|
|
50
50
|
owner = owner_for_codeowners_file(pathname)
|
51
|
-
|
51
|
+
glob = glob_for_codeowners_file(pathname)
|
52
|
+
res[glob] = owner
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
@@ -77,7 +78,7 @@ module CodeOwnership
|
|
77
78
|
# Takes a file and finds the relevant `.codeowner` file by walking up the directory
|
78
79
|
# structure. Example, given `a/b/c.rb`, this looks for `a/b/.codeowner`, `a/.codeowner`,
|
79
80
|
# and `.codeowner` in that order, stopping at the first file to actually exist.
|
80
|
-
# If the
|
81
|
+
# If the provided file is a directory, it will look for `.codeowner` in that directory and then upwards.
|
81
82
|
# We do additional caching so that we don't have to check for file existence every time.
|
82
83
|
sig { params(file: String).returns(T.nilable(CodeTeams::Team)) }
|
83
84
|
def map_file_to_relevant_owner(file)
|
@@ -97,7 +98,7 @@ module CodeOwnership
|
|
97
98
|
(path_components.length - 1).downto(0).each do |i|
|
98
99
|
team = get_team_from_codeowners_file_within_directory(
|
99
100
|
Pathname.new(File.join(*T.unsafe(path_components[0...i])))
|
100
|
-
|
101
|
+
)
|
101
102
|
return team unless team.nil?
|
102
103
|
end
|
103
104
|
|
@@ -121,7 +122,27 @@ module CodeOwnership
|
|
121
122
|
@@directory_cache[potential_codeowners_file_name] = nil
|
122
123
|
end
|
123
124
|
|
124
|
-
|
125
|
+
team
|
126
|
+
end
|
127
|
+
|
128
|
+
sig { params(codeowners_file: Pathname).returns(String) }
|
129
|
+
def glob_for_codeowners_file(codeowners_file)
|
130
|
+
unescaped = codeowners_file.dirname.cleanpath.join('**/**').to_s
|
131
|
+
|
132
|
+
# Globs can contain certain regex characters, like "[" and "]".
|
133
|
+
# However, when we are generating a glob from a .codeowner file, we
|
134
|
+
# need to escape bracket characters and interpret them literally.
|
135
|
+
# Otherwise the resulting glob will not actually match the directory
|
136
|
+
# containing the .codeowner file.
|
137
|
+
#
|
138
|
+
# Example
|
139
|
+
# file: "/some/[dir]/.codeowner"
|
140
|
+
# unescaped: "/some/[dir]/**/**"
|
141
|
+
# matches: "/some/d/file"
|
142
|
+
# matches: "/some/i/file"
|
143
|
+
# matches: "/some/r/file"
|
144
|
+
# does not match!: "/some/[dir]/file"
|
145
|
+
unescaped.gsub(/[\[\]]/) { |x| "\\#{x}" }
|
125
146
|
end
|
126
147
|
end
|
127
148
|
end
|
@@ -18,24 +18,24 @@ module CodeOwnership
|
|
18
18
|
extend T::Sig
|
19
19
|
include Mapper
|
20
20
|
|
21
|
-
TEAM_PATTERN = T.let(
|
21
|
+
TEAM_PATTERN = T.let(%r{\A(?:#|//) @team (?<team>.*)\Z}.freeze, Regexp)
|
22
22
|
DESCRIPTION = 'Annotations at the top of file'
|
23
23
|
|
24
24
|
sig do
|
25
|
-
override.params(file: String)
|
26
|
-
returns(T.nilable(::CodeTeams::Team))
|
25
|
+
override.params(file: String)
|
26
|
+
.returns(T.nilable(::CodeTeams::Team))
|
27
27
|
end
|
28
28
|
def map_file_to_owner(file)
|
29
29
|
file_annotation_based_owner(file)
|
30
30
|
end
|
31
31
|
|
32
32
|
sig do
|
33
|
-
override
|
34
|
-
params(files: T::Array[String])
|
35
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
33
|
+
override
|
34
|
+
.params(files: T::Array[String])
|
35
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
36
36
|
end
|
37
37
|
def globs_to_owner(files)
|
38
|
-
files.each_with_object({}) do |filename_relative_to_root, mapping|
|
38
|
+
files.each_with_object({}) do |filename_relative_to_root, mapping|
|
39
39
|
owner = file_annotation_based_owner(filename_relative_to_root)
|
40
40
|
next unless owner
|
41
41
|
|
@@ -56,10 +56,10 @@ module CodeOwnership
|
|
56
56
|
invalid_files = cache.keys.select do |file|
|
57
57
|
# If a file is not tracked, it should be removed from the cache
|
58
58
|
!Private.file_tracked?(file) ||
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
# If a file no longer has a file annotation (i.e. `globs_to_owner` doesn't map it)
|
60
|
+
# it should be removed from the cache
|
61
|
+
# We make sure to only apply this to the input files since otherwise `updated_cache_for_files.key?(file)` would always return `false` when files == []
|
62
|
+
(fileset.include?(file) && !updated_cache_for_files.key?(file))
|
63
63
|
end
|
64
64
|
|
65
65
|
invalid_files.each do |invalid_file|
|
@@ -83,14 +83,14 @@ module CodeOwnership
|
|
83
83
|
# and if the annotation isn't in the first two lines we assume it
|
84
84
|
# doesn't exist.
|
85
85
|
|
86
|
-
|
86
|
+
line1 = File.foreach(filename).first
|
87
87
|
|
88
|
-
return if !
|
88
|
+
return if !line1
|
89
89
|
|
90
90
|
begin
|
91
|
-
team =
|
92
|
-
rescue ArgumentError =>
|
93
|
-
if
|
91
|
+
team = line1[TEAM_PATTERN, :team]
|
92
|
+
rescue ArgumentError => e
|
93
|
+
if e.message.include?('invalid byte sequence')
|
94
94
|
team = nil
|
95
95
|
else
|
96
96
|
raise
|
@@ -110,7 +110,7 @@ module CodeOwnership
|
|
110
110
|
if file_annotation_based_owner(filename)
|
111
111
|
filepath = Pathname.new(filename)
|
112
112
|
lines = filepath.read.split("\n")
|
113
|
-
new_lines = lines.
|
113
|
+
new_lines = lines.reject { |line| line[TEAM_PATTERN] }
|
114
114
|
# We explicitly add a final new line since splitting by new line when reading the file lines
|
115
115
|
# ignores new lines at the ends of files
|
116
116
|
# We also remove leading new lines, since there is after a new line after an annotation
|
@@ -125,8 +125,7 @@ module CodeOwnership
|
|
125
125
|
end
|
126
126
|
|
127
127
|
sig { override.void }
|
128
|
-
def bust_caches
|
129
|
-
end
|
128
|
+
def bust_caches!; end
|
130
129
|
end
|
131
130
|
end
|
132
131
|
end
|
@@ -12,8 +12,8 @@ module CodeOwnership
|
|
12
12
|
@@package_json_cache = T.let({}, T::Hash[String, T.nilable(ParseJsPackages::Package)]) # rubocop:disable Style/ClassVars
|
13
13
|
|
14
14
|
sig do
|
15
|
-
override.params(file: String)
|
16
|
-
returns(T.nilable(::CodeTeams::Team))
|
15
|
+
override.params(file: String)
|
16
|
+
.returns(T.nilable(::CodeTeams::Team))
|
17
17
|
end
|
18
18
|
def map_file_to_owner(file)
|
19
19
|
package = map_file_to_relevant_package(file)
|
@@ -39,8 +39,8 @@ module CodeOwnership
|
|
39
39
|
# subset of files, but rather we want code ownership for all files.
|
40
40
|
#
|
41
41
|
sig do
|
42
|
-
override.params(files: T::Array[String])
|
43
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
42
|
+
override.params(files: T::Array[String])
|
43
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
44
44
|
end
|
45
45
|
def globs_to_owner(files)
|
46
46
|
ParseJsPackages.all.each_with_object({}) do |package, res|
|
@@ -85,8 +85,8 @@ module CodeOwnership
|
|
85
85
|
|
86
86
|
(path_components.length - 1).downto(0).each do |i|
|
87
87
|
potential_relative_path_name = T.must(path_components[0...i]).reduce(Pathname.new('')) { |built_path, path| built_path.join(path) }
|
88
|
-
potential_package_json_path = potential_relative_path_name
|
89
|
-
join(ParseJsPackages::PACKAGE_JSON_NAME)
|
88
|
+
potential_package_json_path = potential_relative_path_name
|
89
|
+
.join(ParseJsPackages::PACKAGE_JSON_NAME)
|
90
90
|
|
91
91
|
potential_package_json_string = potential_package_json_path.to_s
|
92
92
|
|
@@ -10,8 +10,8 @@ module CodeOwnership
|
|
10
10
|
include Mapper
|
11
11
|
|
12
12
|
sig do
|
13
|
-
override.params(file: String)
|
14
|
-
returns(T.nilable(::CodeTeams::Team))
|
13
|
+
override.params(file: String)
|
14
|
+
.returns(T.nilable(::CodeTeams::Team))
|
15
15
|
end
|
16
16
|
def map_file_to_owner(file)
|
17
17
|
package = Packs.for_file(file)
|
@@ -30,8 +30,8 @@ module CodeOwnership
|
|
30
30
|
# subset of files, but rather we want code ownership for all files.
|
31
31
|
#
|
32
32
|
sig do
|
33
|
-
override.params(files: T::Array[String])
|
34
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
33
|
+
override.params(files: T::Array[String])
|
34
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
35
35
|
end
|
36
36
|
def globs_to_owner(files)
|
37
37
|
Packs.all.each_with_object({}) do |package, res|
|
@@ -66,8 +66,7 @@ module CodeOwnership
|
|
66
66
|
end
|
67
67
|
|
68
68
|
sig { override.void }
|
69
|
-
def bust_caches
|
70
|
-
end
|
69
|
+
def bust_caches!; end
|
71
70
|
end
|
72
71
|
end
|
73
72
|
end
|
@@ -14,11 +14,11 @@ module CodeOwnership
|
|
14
14
|
@@map_files_to_owners = {} # rubocop:disable Style/ClassVars
|
15
15
|
|
16
16
|
sig do
|
17
|
-
params(files: T::Array[String])
|
18
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
17
|
+
params(files: T::Array[String])
|
18
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
19
19
|
end
|
20
|
-
def map_files_to_owners(files)
|
21
|
-
return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count
|
20
|
+
def map_files_to_owners(files)
|
21
|
+
return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count.positive?
|
22
22
|
|
23
23
|
@@map_files_to_owners = CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
|
24
24
|
TeamPlugins::Ownership.for(team).owned_globs.each do |glob|
|
@@ -42,7 +42,7 @@ module CodeOwnership
|
|
42
42
|
sig { returns(String) }
|
43
43
|
def description
|
44
44
|
# These are sorted only to prevent non-determinism in output between local and CI environments.
|
45
|
-
sorted_contexts = mapping_contexts.sort_by{|context| context.team.config_yml.to_s }
|
45
|
+
sorted_contexts = mapping_contexts.sort_by { |context| context.team.config_yml.to_s }
|
46
46
|
description_args = sorted_contexts.map do |context|
|
47
47
|
"`#{context.glob}` (from `#{context.team.config_yml}`)"
|
48
48
|
end
|
@@ -56,7 +56,7 @@ module CodeOwnership
|
|
56
56
|
end
|
57
57
|
def find_overlapping_globs
|
58
58
|
mapped_files = T.let({}, T::Hash[String, T::Array[MappingContext]])
|
59
|
-
CodeTeams.all.each_with_object({}) do |team,
|
59
|
+
CodeTeams.all.each_with_object({}) do |team, _map|
|
60
60
|
TeamPlugins::Ownership.for(team).owned_globs.each do |glob|
|
61
61
|
Dir.glob(glob).each do |filename|
|
62
62
|
mapped_files[filename] ||= []
|
@@ -66,24 +66,22 @@ module CodeOwnership
|
|
66
66
|
end
|
67
67
|
|
68
68
|
overlaps = T.let([], T::Array[GlobOverlap])
|
69
|
-
mapped_files.
|
69
|
+
mapped_files.each_value do |mapping_contexts|
|
70
70
|
if mapping_contexts.count > 1
|
71
71
|
overlaps << GlobOverlap.new(mapping_contexts: mapping_contexts)
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
75
|
-
|
75
|
+
overlaps.uniq do |glob_overlap|
|
76
76
|
glob_overlap.mapping_contexts.map do |context|
|
77
77
|
[context.glob, context.team.name]
|
78
78
|
end
|
79
79
|
end
|
80
|
-
|
81
|
-
deduplicated_overlaps
|
82
80
|
end
|
83
81
|
|
84
82
|
sig do
|
85
|
-
override.params(file: String)
|
86
|
-
returns(T.nilable(::CodeTeams::Team))
|
83
|
+
override.params(file: String)
|
84
|
+
.returns(T.nilable(::CodeTeams::Team))
|
87
85
|
end
|
88
86
|
def map_file_to_owner(file)
|
89
87
|
map_files_to_owners([file])[file]
|
@@ -97,11 +95,11 @@ module CodeOwnership
|
|
97
95
|
end
|
98
96
|
|
99
97
|
sig do
|
100
|
-
override.params(files: T::Array[String])
|
101
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
98
|
+
override.params(files: T::Array[String])
|
99
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
102
100
|
end
|
103
101
|
def globs_to_owner(files)
|
104
|
-
CodeTeams.all.each_with_object({}) do |team, map|
|
102
|
+
CodeTeams.all.each_with_object({}) do |team, map|
|
105
103
|
TeamPlugins::Ownership.for(team).owned_globs.each do |owned_glob|
|
106
104
|
map[owned_glob] = team
|
107
105
|
end
|
@@ -128,7 +126,7 @@ module CodeOwnership
|
|
128
126
|
errors << <<~MSG
|
129
127
|
`owned_globs` cannot overlap between teams. The following globs overlap:
|
130
128
|
|
131
|
-
#{overlapping_globs.map { |overlap| "- #{overlap.description}"}.join("\n")}
|
129
|
+
#{overlapping_globs.map { |overlap| "- #{overlap.description}" }.join("\n")}
|
132
130
|
MSG
|
133
131
|
end
|
134
132
|
|
@@ -13,11 +13,11 @@ module CodeOwnership
|
|
13
13
|
@@map_files_to_owners = {} # rubocop:disable Style/ClassVars
|
14
14
|
|
15
15
|
sig do
|
16
|
-
params(files: T::Array[String])
|
17
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
16
|
+
params(files: T::Array[String])
|
17
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
18
18
|
end
|
19
|
-
def map_files_to_owners(files)
|
20
|
-
return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count
|
19
|
+
def map_files_to_owners(files)
|
20
|
+
return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count.positive?
|
21
21
|
|
22
22
|
@@map_files_to_owners = CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
|
23
23
|
map[team.config_yml] = team
|
@@ -25,19 +25,19 @@ module CodeOwnership
|
|
25
25
|
end
|
26
26
|
|
27
27
|
sig do
|
28
|
-
override.params(file: String)
|
29
|
-
returns(T.nilable(::CodeTeams::Team))
|
28
|
+
override.params(file: String)
|
29
|
+
.returns(T.nilable(::CodeTeams::Team))
|
30
30
|
end
|
31
31
|
def map_file_to_owner(file)
|
32
32
|
map_files_to_owners([file])[file]
|
33
33
|
end
|
34
34
|
|
35
35
|
sig do
|
36
|
-
override.params(files: T::Array[String])
|
37
|
-
returns(T::Hash[String, ::CodeTeams::Team])
|
36
|
+
override.params(files: T::Array[String])
|
37
|
+
.returns(T::Hash[String, ::CodeTeams::Team])
|
38
38
|
end
|
39
39
|
def globs_to_owner(files)
|
40
|
-
CodeTeams.all.each_with_object({}) do |team, map|
|
40
|
+
CodeTeams.all.each_with_object({}) do |team, map|
|
41
41
|
map[team.config_yml] = team
|
42
42
|
end
|
43
43
|
end
|
@@ -23,10 +23,10 @@ module CodeOwnership
|
|
23
23
|
package_loaded_json = JSON.parse(pathname.read)
|
24
24
|
|
25
25
|
package_name = if pathname.dirname == Pathname.new('.')
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
ROOT_PACKAGE_NAME
|
27
|
+
else
|
28
|
+
pathname.dirname.cleanpath.to_s
|
29
|
+
end
|
30
30
|
|
31
31
|
new(
|
32
32
|
name: package_name,
|
@@ -41,7 +41,7 @@ module CodeOwnership
|
|
41
41
|
Please either make the JSON in that file valid or specify `js_package_paths` in config/code_ownership.yml.
|
42
42
|
MESSAGE
|
43
43
|
|
44
|
-
raise InvalidCodeOwnershipConfigurationError
|
44
|
+
raise InvalidCodeOwnershipConfigurationError, error_message
|
45
45
|
end
|
46
46
|
|
47
47
|
sig { returns(Pathname) }
|
@@ -12,8 +12,8 @@ module CodeOwnership
|
|
12
12
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
13
13
|
cache = Private.glob_cache
|
14
14
|
file_mappings = cache.mapper_descriptions_that_map_files(files)
|
15
|
-
files_not_mapped_at_all = file_mappings.select do |
|
16
|
-
mapper_descriptions.count
|
15
|
+
files_not_mapped_at_all = file_mappings.select do |_file, mapper_descriptions|
|
16
|
+
mapper_descriptions.count.zero?
|
17
17
|
end
|
18
18
|
|
19
19
|
errors = T.let([], T::Array[String])
|
@@ -22,7 +22,7 @@ module CodeOwnership
|
|
22
22
|
errors << <<~MSG
|
23
23
|
Some files are missing ownership:
|
24
24
|
|
25
|
-
#{files_not_mapped_at_all.map { |file,
|
25
|
+
#{files_not_mapped_at_all.map { |file, _mappers| "- #{file}" }.join("\n")}
|
26
26
|
MSG
|
27
27
|
end
|
28
28
|
|
@@ -12,7 +12,7 @@ module CodeOwnership
|
|
12
12
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
13
13
|
cache = Private.glob_cache
|
14
14
|
file_mappings = cache.mapper_descriptions_that_map_files(files)
|
15
|
-
files_mapped_by_multiple_mappers = file_mappings.select do |
|
15
|
+
files_mapped_by_multiple_mappers = file_mappings.select do |_file, mapper_descriptions|
|
16
16
|
mapper_descriptions.count > 1
|
17
17
|
end
|
18
18
|
|
@@ -25,7 +25,7 @@ module CodeOwnership
|
|
25
25
|
`git add #{CodeownersFile.path}`
|
26
26
|
end
|
27
27
|
# If there is no current file or its empty, display a shorter message.
|
28
|
-
elsif actual_content_lines == [
|
28
|
+
elsif actual_content_lines == ['']
|
29
29
|
errors << <<~CODEOWNERS_ERROR
|
30
30
|
CODEOWNERS out of date. Run `bin/codeownership validate` to update the CODEOWNERS file
|
31
31
|
CODEOWNERS_ERROR
|
@@ -34,33 +34,33 @@ module CodeOwnership
|
|
34
34
|
extra_lines = actual_content_lines - expected_content_lines
|
35
35
|
|
36
36
|
missing_lines_text = if missing_lines.any?
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
37
|
+
<<~COMMENT
|
38
|
+
CODEOWNERS should contain the following lines, but does not:
|
39
|
+
#{missing_lines.map { |line| "- \"#{line}\"" }.join("\n")}
|
40
|
+
COMMENT
|
41
|
+
end
|
42
42
|
|
43
43
|
extra_lines_text = if extra_lines.any?
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
44
|
+
<<~COMMENT
|
45
|
+
CODEOWNERS should not contain the following lines, but it does:
|
46
|
+
#{extra_lines.map { |line| "- \"#{line}\"" }.join("\n")}
|
47
|
+
COMMENT
|
48
|
+
end
|
49
49
|
|
50
50
|
diff_text = if missing_lines_text && extra_lines_text
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
51
|
+
"#{missing_lines_text}\n#{extra_lines_text}".chomp
|
52
|
+
elsif missing_lines_text
|
53
|
+
missing_lines_text
|
54
|
+
elsif extra_lines_text
|
55
|
+
extra_lines_text
|
56
|
+
else
|
57
|
+
<<~TEXT
|
58
|
+
There may be extra lines, or lines are out of order.
|
59
|
+
You can try to regenerate the CODEOWNERS file from scratch:
|
60
|
+
1) `rm .github/CODEOWNERS`
|
61
|
+
2) `bin/codeownership validate`
|
62
|
+
TEXT
|
63
|
+
end
|
64
64
|
|
65
65
|
errors << <<~CODEOWNERS_ERROR
|
66
66
|
CODEOWNERS out of date. Run `bin/codeownership validate` to update the CODEOWNERS file
|
@@ -92,7 +92,7 @@ module CodeOwnership
|
|
92
92
|
# However, globbing out can take 5 or more seconds on a large repository, dramatically slowing down
|
93
93
|
# invocations to `bin/codeownership validate --diff`.
|
94
94
|
# Using `File.fnmatch?` is a lot faster!
|
95
|
-
in_owned_globs = configuration.owned_globs.
|
95
|
+
in_owned_globs = configuration.owned_globs.any? do |owned_glob|
|
96
96
|
File.fnmatch?(owned_glob, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
97
97
|
end
|
98
98
|
|
@@ -115,13 +115,11 @@ module CodeOwnership
|
|
115
115
|
sig { returns(GlobCache) }
|
116
116
|
def self.glob_cache
|
117
117
|
@glob_cache ||= T.let(@glob_cache, T.nilable(GlobCache))
|
118
|
-
@glob_cache ||=
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
end
|
124
|
-
end
|
118
|
+
@glob_cache ||= if CodeownersFile.use_codeowners_cache?
|
119
|
+
CodeownersFile.to_glob_cache
|
120
|
+
else
|
121
|
+
Mapper.to_glob_cache
|
122
|
+
end
|
125
123
|
end
|
126
124
|
end
|
127
125
|
|
@@ -8,8 +8,7 @@ module CodeOwnership
|
|
8
8
|
interface!
|
9
9
|
|
10
10
|
sig { abstract.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
|
11
|
-
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
12
|
-
end
|
11
|
+
def validation_errors(files:, autocorrect: true, stage_changes: true); end
|
13
12
|
|
14
13
|
class << self
|
15
14
|
extend T::Sig
|
data/lib/code_ownership.rb
CHANGED
@@ -18,7 +18,8 @@ if defined?(Packwerk)
|
|
18
18
|
end
|
19
19
|
|
20
20
|
module CodeOwnership
|
21
|
-
|
21
|
+
module_function
|
22
|
+
|
22
23
|
extend T::Sig
|
23
24
|
extend T::Helpers
|
24
25
|
|
@@ -39,7 +40,7 @@ module CodeOwnership
|
|
39
40
|
|
40
41
|
Mapper.all.each do |mapper|
|
41
42
|
owner = mapper.map_file_to_owner(file)
|
42
|
-
break if owner
|
43
|
+
break if owner # TODO: what if there are multiple owners? Should we respond with an error instead of the first match?
|
43
44
|
end
|
44
45
|
|
45
46
|
@for_file[file] = owner
|
@@ -57,6 +58,7 @@ module CodeOwnership
|
|
57
58
|
ownership_for_mapper = []
|
58
59
|
glob_to_owning_team_map.each do |glob, owning_team|
|
59
60
|
next if owning_team != team
|
61
|
+
|
60
62
|
ownership_for_mapper << "- #{glob}"
|
61
63
|
end
|
62
64
|
|
@@ -66,7 +68,7 @@ module CodeOwnership
|
|
66
68
|
ownership_information += ownership_for_mapper.sort
|
67
69
|
end
|
68
70
|
|
69
|
-
ownership_information <<
|
71
|
+
ownership_information << ''
|
70
72
|
end
|
71
73
|
|
72
74
|
ownership_information.join("\n")
|
@@ -84,7 +86,7 @@ module CodeOwnership
|
|
84
86
|
params(
|
85
87
|
autocorrect: T::Boolean,
|
86
88
|
stage_changes: T::Boolean,
|
87
|
-
files: T.nilable(T::Array[String])
|
89
|
+
files: T.nilable(T::Array[String])
|
88
90
|
).void
|
89
91
|
end
|
90
92
|
def validate!(
|
@@ -95,10 +97,10 @@ module CodeOwnership
|
|
95
97
|
Private.load_configuration!
|
96
98
|
|
97
99
|
tracked_file_subset = if files
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
100
|
+
files.select { |f| Private.file_tracked?(f) }
|
101
|
+
else
|
102
|
+
Private.tracked_files
|
103
|
+
end
|
102
104
|
|
103
105
|
Private.validate!(files: tracked_file_subset, autocorrect: autocorrect, stage_changes: stage_changes)
|
104
106
|
end
|
@@ -150,7 +152,7 @@ module CodeOwnership
|
|
150
152
|
|
151
153
|
[
|
152
154
|
CodeOwnership.for_file(file),
|
153
|
-
file
|
155
|
+
file
|
154
156
|
]
|
155
157
|
end
|
156
158
|
end
|
@@ -161,15 +163,15 @@ module CodeOwnership
|
|
161
163
|
@memoized_values ||= T.let(@memoized_values, T.nilable(T::Hash[String, T.nilable(::CodeTeams::Team)]))
|
162
164
|
@memoized_values ||= {}
|
163
165
|
# We use key because the memoized value could be `nil`
|
164
|
-
if
|
166
|
+
if @memoized_values.key?(klass.to_s)
|
167
|
+
@memoized_values[klass.to_s]
|
168
|
+
else
|
165
169
|
path = Private.path_from_klass(klass)
|
166
170
|
return nil if path.nil?
|
167
171
|
|
168
172
|
value_to_memoize = for_file(path)
|
169
173
|
@memoized_values[klass.to_s] = value_to_memoize
|
170
174
|
value_to_memoize
|
171
|
-
else
|
172
|
-
@memoized_values[klass.to_s]
|
173
175
|
end
|
174
176
|
end
|
175
177
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: code_ownership
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.37.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gusto Engineers
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-08-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: code_teams
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: packwerk
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
@@ -81,21 +81,21 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: railties
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
89
|
+
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
96
|
+
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: rake
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
@@ -109,7 +109,21 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
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
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
114
128
|
requirements:
|
115
129
|
- - ">="
|
@@ -123,7 +137,7 @@ dependencies:
|
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: '0'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
140
|
+
name: sorbet
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
128
142
|
requirements:
|
129
143
|
- - ">="
|
@@ -137,7 +151,7 @@ dependencies:
|
|
137
151
|
- !ruby/object:Gem::Version
|
138
152
|
version: '0'
|
139
153
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
154
|
+
name: tapioca
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
142
156
|
requirements:
|
143
157
|
- - ">="
|
@@ -191,7 +205,7 @@ metadata:
|
|
191
205
|
source_code_uri: https://github.com/rubyatscale/code_ownership
|
192
206
|
changelog_uri: https://github.com/rubyatscale/code_ownership/releases
|
193
207
|
allowed_push_host: https://rubygems.org
|
194
|
-
post_install_message:
|
208
|
+
post_install_message:
|
195
209
|
rdoc_options: []
|
196
210
|
require_paths:
|
197
211
|
- lib
|
@@ -199,15 +213,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
199
213
|
requirements:
|
200
214
|
- - ">="
|
201
215
|
- !ruby/object:Gem::Version
|
202
|
-
version: '
|
216
|
+
version: '2.6'
|
203
217
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
204
218
|
requirements:
|
205
219
|
- - ">="
|
206
220
|
- !ruby/object:Gem::Version
|
207
221
|
version: '0'
|
208
222
|
requirements: []
|
209
|
-
rubygems_version: 3.
|
210
|
-
signing_key:
|
223
|
+
rubygems_version: 3.5.11
|
224
|
+
signing_key:
|
211
225
|
specification_version: 4
|
212
226
|
summary: A gem to help engineering teams declare ownership of code
|
213
227
|
test_files: []
|