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 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