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