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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bf52f1bcb70650a93514852fec13bc3e1f4f275643ed712bad37f8439b6ae17
4
- data.tar.gz: ca47603f68d95683c4b948318532170a7156651ab24c2f0adcce292975b6076f
3
+ metadata.gz: f304bfe47eb64942fd8bb38849942a508c37fb9a531397071fea5375fd7430cb
4
+ data.tar.gz: 9f24b0084e84b9856f50a119178f661d7b09c50c4c5e2ff48a078dd4276674fd
5
5
  SHA512:
6
- metadata.gz: 037e1119a49812006add87fcef7c7a3d6de0505523c36dd7811e0583fbd654ccce450b79a7a6a4b298ef90e511a269ed11d076d823cd4025ceb693e3101c4316
7
- data.tar.gz: 5c6ef23bf3554f1531bcebeb6daf49353503a251ee8e3a51ff0223375d3fbb7d4abb1d2f4b0ed99e9368e5279d9c0f623f58906f84b2bc09ed0fb8fecc7d5e33
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(files)
15
- files_not_mapped_at_all = files_by_mapper.select { |_file, mapper_descriptions| mapper_descriptions.count == 0 }.keys
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(files)
13
+ files_by_mapper = Private.glob_cache.files_by_mapper
14
14
 
15
- files_mapped_by_multiple_mappers = files_by_mapper.select { |_file, mapper_descriptions| mapper_descriptions.count > 1 }.to_h
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
- codeowners_filepath = Pathname.pwd.join('.github/CODEOWNERS')
16
- FileUtils.mkdir_p(codeowners_filepath.dirname) if !codeowners_filepath.dirname.exist?
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
- codeowners_filepath.write(expected_contents)
23
+ CodeownersFile.write!
46
24
  if stage_changes
47
- `git add #{codeowners_filepath}`
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
- #{(expected_content_lines - actual_content_lines).map { |line| "- \"#{line}\""}.join("\n")}
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
- #{(actual_content_lines - expected_content_lines).map { |line| "- \"#{line}\""}.join("\n")}
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 actual_contents == ""
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
- @files_by_mapper = nil
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 { params(files: T::Array[String]).returns(T::Hash[String, T::Array[String]]) }
91
- def self.files_by_mapper(files)
92
- @files_by_mapper ||= T.let(@files_by_mapper, T.nilable(T::Hash[String, T::Array[String]]))
93
- @files_by_mapper ||= begin
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.3
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-07 00:00:00.000000000 Z
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