code_ownership 1.32.3 → 1.32.4
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/lib/code_ownership/mapper.rb +16 -0
- data/lib/code_ownership/private/codeowners_file.rb +85 -0
- data/lib/code_ownership/private/glob_cache.rb +72 -0
- data/lib/code_ownership/private/validations/files_have_owners.rb +5 -2
- data/lib/code_ownership/private/validations/files_have_unique_owners.rb +8 -2
- data/lib/code_ownership/private/validations/github_codeowners_up_to_date.rb +9 -61
- data/lib/code_ownership/private.rb +7 -16
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f304bfe47eb64942fd8bb38849942a508c37fb9a531397071fea5375fd7430cb
|
|
4
|
+
data.tar.gz: 9f24b0084e84b9856f50a119178f661d7b09c50c4c5e2ff48a078dd4276674fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 384aa25cbb2b268bf9b820de2065783c506b46b560efdec3cc7dc2761dbeab0755d6a5bdc6c8c19b5932f9e36ce581ea13d4f14bfbf138d787977e78e7fa95e1
|
|
7
|
+
data.tar.gz: 5917554485514a031239bc110b5ce99f4e84ef9b9a1af69df0564e552889be4fb5d7081d4708961f28ff497607e678c72e0ca19ceda573b6e75b81f85ac675e0
|
|
@@ -58,5 +58,21 @@ module CodeOwnership
|
|
|
58
58
|
sig { abstract.void }
|
|
59
59
|
def bust_caches!
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
sig { returns(Private::GlobCache) }
|
|
63
|
+
def self.to_glob_cache
|
|
64
|
+
glob_to_owner_map_by_mapper_description = {}
|
|
65
|
+
|
|
66
|
+
Mapper.all.each do |mapper|
|
|
67
|
+
mapped_files = mapper.codeowners_lines_to_owners
|
|
68
|
+
mapped_files.each do |glob, owner|
|
|
69
|
+
next if owner.nil?
|
|
70
|
+
glob_to_owner_map_by_mapper_description[mapper.description] ||= {}
|
|
71
|
+
glob_to_owner_map_by_mapper_description.fetch(mapper.description)[glob] = owner
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Private::GlobCache.new(glob_to_owner_map_by_mapper_description)
|
|
76
|
+
end
|
|
61
77
|
end
|
|
62
78
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CodeOwnership
|
|
5
|
+
module Private
|
|
6
|
+
#
|
|
7
|
+
# This class is responsible for turning CodeOwnership directives (e.g. annotations, package owners)
|
|
8
|
+
# into a GitHub CODEOWNERS file, as specified here:
|
|
9
|
+
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
|
10
|
+
#
|
|
11
|
+
class CodeownersFile
|
|
12
|
+
extend T::Sig
|
|
13
|
+
|
|
14
|
+
sig { returns(T::Array[String]) }
|
|
15
|
+
def self.actual_contents_lines
|
|
16
|
+
(path.exist? ? path.read : "").split("\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { returns(T::Array[T.nilable(String)]) }
|
|
20
|
+
def self.expected_contents_lines
|
|
21
|
+
cache = Private.glob_cache.raw_cache_contents
|
|
22
|
+
|
|
23
|
+
header = <<~HEADER
|
|
24
|
+
# STOP! - DO NOT EDIT THIS FILE MANUALLY
|
|
25
|
+
# This file was automatically generated by "bin/codeownership validate".
|
|
26
|
+
#
|
|
27
|
+
# CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub
|
|
28
|
+
# teams. This is useful when developers create Pull Requests since the
|
|
29
|
+
# code/file owner is notified. Reference GitHub docs for more details:
|
|
30
|
+
# https://help.github.com/en/articles/about-code-owners
|
|
31
|
+
HEADER
|
|
32
|
+
ignored_teams = T.let(Set.new, T::Set[String])
|
|
33
|
+
|
|
34
|
+
github_team_map = CodeTeams.all.each_with_object({}) do |team, map|
|
|
35
|
+
team_github = TeamPlugins::Github.for(team).github
|
|
36
|
+
if team_github.do_not_add_to_codeowners_file
|
|
37
|
+
ignored_teams << team.name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
map[team.name] = team_github.team
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
codeowners_file_lines = T.let([], T::Array[String])
|
|
44
|
+
|
|
45
|
+
cache.each do |mapper_description, ownership_map_cache|
|
|
46
|
+
ownership_entries = []
|
|
47
|
+
ownership_map_cache.each do |path, code_team|
|
|
48
|
+
team_mapping = github_team_map[code_team.name]
|
|
49
|
+
next if team_mapping.nil?
|
|
50
|
+
next if ignored_teams.include?(code_team.name)
|
|
51
|
+
entry = "/#{path} #{team_mapping}"
|
|
52
|
+
# In order to use the codeowners file as a proper cache, we'll need to insert commented out entries for ignored teams
|
|
53
|
+
# entry = if ignored_teams.include?(code_team.name)
|
|
54
|
+
# "# /#{path} #{team_mapping}"
|
|
55
|
+
# else
|
|
56
|
+
# "/#{path} #{team_mapping}"
|
|
57
|
+
# end
|
|
58
|
+
ownership_entries << entry
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
next if ownership_entries.none?
|
|
62
|
+
codeowners_file_lines += ['', "# #{mapper_description}", *ownership_entries.sort]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
[
|
|
66
|
+
*header.split("\n"),
|
|
67
|
+
nil, # For line between header and codeowners_file_lines
|
|
68
|
+
*codeowners_file_lines,
|
|
69
|
+
nil, # For end-of-file newline
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { void }
|
|
74
|
+
def self.write!
|
|
75
|
+
FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
|
|
76
|
+
path.write(expected_contents_lines.join("\n"))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { returns(Pathname) }
|
|
80
|
+
def self.path
|
|
81
|
+
Pathname.pwd.join('.github/CODEOWNERS')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module CodeOwnership
|
|
5
|
+
module Private
|
|
6
|
+
class GlobCache
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
MapperDescription = T.type_alias { String }
|
|
10
|
+
GlobsByMapper = T.type_alias { T::Hash[String, CodeTeams::Team] }
|
|
11
|
+
|
|
12
|
+
CacheShape = T.type_alias do
|
|
13
|
+
T::Hash[
|
|
14
|
+
MapperDescription,
|
|
15
|
+
GlobsByMapper
|
|
16
|
+
]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
FilesByMapper = T.type_alias do
|
|
20
|
+
T::Hash[
|
|
21
|
+
String,
|
|
22
|
+
T::Array[MapperDescription]
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { params(raw_cache_contents: CacheShape).void }
|
|
27
|
+
def initialize(raw_cache_contents)
|
|
28
|
+
@raw_cache_contents = raw_cache_contents
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { returns(CacheShape) }
|
|
32
|
+
def raw_cache_contents
|
|
33
|
+
@raw_cache_contents
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { returns(CacheShape) }
|
|
37
|
+
def expanded_cache
|
|
38
|
+
@expanded_cache = T.let(@expanded_cache, T.nilable(CacheShape))
|
|
39
|
+
|
|
40
|
+
@expanded_cache ||= begin
|
|
41
|
+
expanded_cache = {}
|
|
42
|
+
@raw_cache_contents.each do |mapper_description, globs_by_owner|
|
|
43
|
+
expanded_cache[mapper_description] = {}
|
|
44
|
+
globs_by_owner.each do |glob, owner|
|
|
45
|
+
Dir.glob(glob).each do |file, owner|
|
|
46
|
+
expanded_cache[mapper_description][file] = owner
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
expanded_cache
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { returns(FilesByMapper) }
|
|
56
|
+
def files_by_mapper
|
|
57
|
+
@files_by_mapper ||= T.let(@files_by_mapper, T.nilable(FilesByMapper))
|
|
58
|
+
@files_by_mapper ||= begin
|
|
59
|
+
files_by_mapper = {}
|
|
60
|
+
expanded_cache.each do |mapper_description, file_by_owner|
|
|
61
|
+
file_by_owner.each do |file, owner|
|
|
62
|
+
files_by_mapper[file] ||= []
|
|
63
|
+
files_by_mapper[file] << mapper_description
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
files_by_mapper
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -11,8 +11,11 @@ module CodeOwnership
|
|
|
11
11
|
sig { override.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
|
|
12
12
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
|
13
13
|
allow_list = Dir.glob(Private.configuration.unowned_globs)
|
|
14
|
-
files_by_mapper = Private.files_by_mapper
|
|
15
|
-
|
|
14
|
+
files_by_mapper = Private.glob_cache.files_by_mapper
|
|
15
|
+
|
|
16
|
+
files_not_mapped_at_all = files.select do |file|
|
|
17
|
+
files_by_mapper.fetch(file, []).count == 0
|
|
18
|
+
end
|
|
16
19
|
|
|
17
20
|
files_without_owners = files_not_mapped_at_all - allow_list
|
|
18
21
|
|
|
@@ -10,9 +10,15 @@ module CodeOwnership
|
|
|
10
10
|
|
|
11
11
|
sig { override.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
|
|
12
12
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
|
13
|
-
files_by_mapper = Private.files_by_mapper
|
|
13
|
+
files_by_mapper = Private.glob_cache.files_by_mapper
|
|
14
14
|
|
|
15
|
-
files_mapped_by_multiple_mappers =
|
|
15
|
+
files_mapped_by_multiple_mappers = {}
|
|
16
|
+
files.each do |file|
|
|
17
|
+
mappers = files_by_mapper.fetch(file, [])
|
|
18
|
+
if mappers.count > 1
|
|
19
|
+
files_mapped_by_multiple_mappers[file] = mappers
|
|
20
|
+
end
|
|
21
|
+
end
|
|
16
22
|
|
|
17
23
|
errors = T.let([], T::Array[String])
|
|
18
24
|
|
|
@@ -12,55 +12,34 @@ module CodeOwnership
|
|
|
12
12
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
|
13
13
|
return [] if Private.configuration.skip_codeowners_validation
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
header = <<~HEADER
|
|
19
|
-
# STOP! - DO NOT EDIT THIS FILE MANUALLY
|
|
20
|
-
# This file was automatically generated by "bin/codeownership validate".
|
|
21
|
-
#
|
|
22
|
-
# CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub
|
|
23
|
-
# teams. This is useful when developers create Pull Requests since the
|
|
24
|
-
# code/file owner is notified. Reference GitHub docs for more details:
|
|
25
|
-
# https://help.github.com/en/articles/about-code-owners
|
|
26
|
-
HEADER
|
|
27
|
-
|
|
28
|
-
expected_content_lines = [
|
|
29
|
-
*header.split("\n"),
|
|
30
|
-
nil, # For line between header and codeowners_file_lines
|
|
31
|
-
*codeowners_file_lines,
|
|
32
|
-
nil, # For end-of-file newline
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
expected_contents = expected_content_lines.join("\n")
|
|
36
|
-
actual_contents = codeowners_filepath.exist? ? codeowners_filepath.read : ""
|
|
37
|
-
actual_content_lines = actual_contents.split("\n")
|
|
38
|
-
|
|
39
|
-
codeowners_up_to_date = actual_contents == expected_contents
|
|
15
|
+
actual_content_lines = CodeownersFile.actual_contents_lines
|
|
16
|
+
expected_content_lines = CodeownersFile.expected_contents_lines
|
|
17
|
+
codeowners_up_to_date = actual_content_lines == expected_content_lines
|
|
40
18
|
|
|
41
19
|
errors = T.let([], T::Array[String])
|
|
42
20
|
|
|
43
21
|
if !codeowners_up_to_date
|
|
44
22
|
if autocorrect
|
|
45
|
-
|
|
23
|
+
CodeownersFile.write!
|
|
46
24
|
if stage_changes
|
|
47
|
-
`git add #{
|
|
25
|
+
`git add #{CodeownersFile.path}`
|
|
48
26
|
end
|
|
49
27
|
else
|
|
50
28
|
# If there is no current file or its empty, display a shorter message.
|
|
51
29
|
missing_lines = expected_content_lines - actual_content_lines
|
|
52
30
|
extra_lines = actual_content_lines - expected_content_lines
|
|
31
|
+
|
|
53
32
|
missing_lines_text = if missing_lines.any?
|
|
54
33
|
<<~COMMENT
|
|
55
34
|
CODEOWNERS should contain the following lines, but does not:
|
|
56
|
-
#{(
|
|
35
|
+
#{(missing_lines).map { |line| "- \"#{line}\""}.join("\n")}
|
|
57
36
|
COMMENT
|
|
58
37
|
end
|
|
59
38
|
|
|
60
39
|
extra_lines_text = if extra_lines.any?
|
|
61
40
|
<<~COMMENT
|
|
62
41
|
CODEOWNERS should not contain the following lines, but it does:
|
|
63
|
-
#{(
|
|
42
|
+
#{(extra_lines).map { |line| "- \"#{line}\""}.join("\n")}
|
|
64
43
|
COMMENT
|
|
65
44
|
end
|
|
66
45
|
|
|
@@ -74,7 +53,7 @@ module CodeOwnership
|
|
|
74
53
|
""
|
|
75
54
|
end
|
|
76
55
|
|
|
77
|
-
if
|
|
56
|
+
if actual_content_lines == []
|
|
78
57
|
errors << <<~CODEOWNERS_ERROR
|
|
79
58
|
CODEOWNERS out of date. Run `bin/codeownership validate` to update the CODEOWNERS file
|
|
80
59
|
CODEOWNERS_ERROR
|
|
@@ -90,37 +69,6 @@ module CodeOwnership
|
|
|
90
69
|
|
|
91
70
|
errors
|
|
92
71
|
end
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
# Generate the contents of a CODEOWNERS file that GitHub can use to
|
|
97
|
-
# automatically assign reviewers
|
|
98
|
-
# https://help.github.com/articles/about-codeowners/
|
|
99
|
-
sig { returns(T::Array[String]) }
|
|
100
|
-
def codeowners_file_lines
|
|
101
|
-
github_team_map = CodeTeams.all.each_with_object({}) do |team, map|
|
|
102
|
-
team_github = TeamPlugins::Github.for(team).github
|
|
103
|
-
next if team_github.do_not_add_to_codeowners_file
|
|
104
|
-
|
|
105
|
-
map[team.name] = team_github.team
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
Mapper.all.flat_map do |mapper|
|
|
109
|
-
codeowners_lines = mapper.codeowners_lines_to_owners.filter_map do |line, team|
|
|
110
|
-
team_mapping = github_team_map[team&.name]
|
|
111
|
-
next unless team_mapping
|
|
112
|
-
|
|
113
|
-
"/#{line} #{team_mapping}"
|
|
114
|
-
end
|
|
115
|
-
next [] if codeowners_lines.empty?
|
|
116
|
-
|
|
117
|
-
[
|
|
118
|
-
'',
|
|
119
|
-
"# #{mapper.description}",
|
|
120
|
-
*codeowners_lines.sort,
|
|
121
|
-
]
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
72
|
end
|
|
125
73
|
end
|
|
126
74
|
end
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
require 'code_ownership/private/extension_loader'
|
|
6
6
|
require 'code_ownership/private/team_plugins/ownership'
|
|
7
7
|
require 'code_ownership/private/team_plugins/github'
|
|
8
|
+
require 'code_ownership/private/codeowners_file'
|
|
8
9
|
require 'code_ownership/private/parse_js_packages'
|
|
10
|
+
require 'code_ownership/private/glob_cache'
|
|
9
11
|
require 'code_ownership/private/validations/files_have_owners'
|
|
10
12
|
require 'code_ownership/private/validations/github_codeowners_up_to_date'
|
|
11
13
|
require 'code_ownership/private/validations/files_have_unique_owners'
|
|
@@ -37,7 +39,7 @@ module CodeOwnership
|
|
|
37
39
|
def self.bust_caches!
|
|
38
40
|
@configuration = nil
|
|
39
41
|
@tracked_files = nil
|
|
40
|
-
@
|
|
42
|
+
@glob_cache = nil
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
sig { params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).void }
|
|
@@ -87,21 +89,10 @@ module CodeOwnership
|
|
|
87
89
|
end
|
|
88
90
|
end
|
|
89
91
|
|
|
90
|
-
sig {
|
|
91
|
-
def self.
|
|
92
|
-
@
|
|
93
|
-
@
|
|
94
|
-
files_by_mapper = files.map { |file| [file, []] }.to_h
|
|
95
|
-
|
|
96
|
-
Mapper.all.each do |mapper|
|
|
97
|
-
mapper.map_files_to_owners(files).each do |file, _team|
|
|
98
|
-
files_by_mapper[file] ||= []
|
|
99
|
-
T.must(files_by_mapper[file]) << mapper.description
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
files_by_mapper
|
|
104
|
-
end
|
|
92
|
+
sig { returns(GlobCache) }
|
|
93
|
+
def self.glob_cache
|
|
94
|
+
@glob_cache ||= T.let(@glob_cache, T.nilable(GlobCache))
|
|
95
|
+
@glob_cache ||= Mapper.to_glob_cache
|
|
105
96
|
end
|
|
106
97
|
end
|
|
107
98
|
|
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.32.
|
|
4
|
+
version: 1.32.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gusto Engineers
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-04-
|
|
11
|
+
date: 2023-04-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: code_teams
|
|
@@ -137,7 +137,9 @@ files:
|
|
|
137
137
|
- lib/code_ownership/configuration.rb
|
|
138
138
|
- lib/code_ownership/mapper.rb
|
|
139
139
|
- lib/code_ownership/private.rb
|
|
140
|
+
- lib/code_ownership/private/codeowners_file.rb
|
|
140
141
|
- lib/code_ownership/private/extension_loader.rb
|
|
142
|
+
- lib/code_ownership/private/glob_cache.rb
|
|
141
143
|
- lib/code_ownership/private/ownership_mappers/file_annotations.rb
|
|
142
144
|
- lib/code_ownership/private/ownership_mappers/js_package_ownership.rb
|
|
143
145
|
- lib/code_ownership/private/ownership_mappers/package_ownership.rb
|