code_ownership 1.32.3 → 1.32.5

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: 18268f7457aeb632f100ea698349530dc29a4be7ae62611f416fe43782705a5b
4
+ data.tar.gz: 937353d0f07b1ff21ddf813dff047e59d4029c8ff6b08a29f75f31db61e342bb
5
5
  SHA512:
6
- metadata.gz: 037e1119a49812006add87fcef7c7a3d6de0505523c36dd7811e0583fbd654ccce450b79a7a6a4b298ef90e511a269ed11d076d823cd4025ceb693e3101c4316
7
- data.tar.gz: 5c6ef23bf3554f1531bcebeb6daf49353503a251ee8e3a51ff0223375d3fbb7d4abb1d2f4b0ed99e9368e5279d9c0f623f58906f84b2bc09ed0fb8fecc7d5e33
6
+ metadata.gz: 33c6f36e7a037a8495a370b738715cc2eb557912d4cb2f14df9184b329a6cc18e2c8e0a3f56fc0afd86d028ea321c2737043b8f94a88e5b3ea7c3486ff026216
7
+ data.tar.gz: 1665092ef4a69c4eecd08039fa1315be6bca38aaec17ead77573ede1c408c4b1e4a9c5a1746505310abaebe07f14c95cb088b691d71ee66b35eaa84ef40db8bd
@@ -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,94 @@
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
+ if !path.exist?
17
+ [""]
18
+ else
19
+ content = path.read
20
+ lines = path.read.split("\n")
21
+ if content.end_with?("\n")
22
+ lines << ""
23
+ end
24
+ lines
25
+ end
26
+ end
27
+
28
+ sig { returns(T::Array[T.nilable(String)]) }
29
+ def self.expected_contents_lines
30
+ cache = Private.glob_cache.raw_cache_contents
31
+
32
+ header = <<~HEADER
33
+ # STOP! - DO NOT EDIT THIS FILE MANUALLY
34
+ # This file was automatically generated by "bin/codeownership validate".
35
+ #
36
+ # CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub
37
+ # teams. This is useful when developers create Pull Requests since the
38
+ # code/file owner is notified. Reference GitHub docs for more details:
39
+ # https://help.github.com/en/articles/about-code-owners
40
+ HEADER
41
+ ignored_teams = T.let(Set.new, T::Set[String])
42
+
43
+ github_team_map = CodeTeams.all.each_with_object({}) do |team, map|
44
+ team_github = TeamPlugins::Github.for(team).github
45
+ if team_github.do_not_add_to_codeowners_file
46
+ ignored_teams << team.name
47
+ end
48
+
49
+ map[team.name] = team_github.team
50
+ end
51
+
52
+ codeowners_file_lines = T.let([], T::Array[String])
53
+
54
+ cache.each do |mapper_description, ownership_map_cache|
55
+ ownership_entries = []
56
+ ownership_map_cache.each do |path, code_team|
57
+ team_mapping = github_team_map[code_team.name]
58
+ next if team_mapping.nil?
59
+ next if ignored_teams.include?(code_team.name)
60
+ entry = "/#{path} #{team_mapping}"
61
+ # In order to use the codeowners file as a proper cache, we'll need to insert commented out entries for ignored teams
62
+ # entry = if ignored_teams.include?(code_team.name)
63
+ # "# /#{path} #{team_mapping}"
64
+ # else
65
+ # "/#{path} #{team_mapping}"
66
+ # end
67
+ ownership_entries << entry
68
+ end
69
+
70
+ next if ownership_entries.none?
71
+ codeowners_file_lines += ['', "# #{mapper_description}", *ownership_entries.sort]
72
+ end
73
+
74
+ [
75
+ *header.split("\n"),
76
+ "", # For line between header and codeowners_file_lines
77
+ *codeowners_file_lines,
78
+ "", # For end-of-file newline
79
+ ]
80
+ end
81
+
82
+ sig { void }
83
+ def self.write!
84
+ FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
85
+ path.write(expected_contents_lines.join("\n"))
86
+ end
87
+
88
+ sig { returns(Pathname) }
89
+ def self.path
90
+ Pathname.pwd.join('.github/CODEOWNERS')
91
+ end
92
+ end
93
+ end
94
+ 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
+ missing_lines = expected_content_lines - actual_content_lines
18
+ extra_lines = actual_content_lines - expected_content_lines
40
19
 
20
+ codeowners_up_to_date = !missing_lines.any? && !extra_lines.any?
41
21
  errors = T.let([], T::Array[String])
42
22
 
43
23
  if !codeowners_up_to_date
44
24
  if autocorrect
45
- codeowners_filepath.write(expected_contents)
25
+ CodeownersFile.write!
46
26
  if stage_changes
47
- `git add #{codeowners_filepath}`
27
+ `git add #{CodeownersFile.path}`
48
28
  end
49
29
  else
50
30
  # If there is no current file or its empty, display a shorter message.
51
- missing_lines = expected_content_lines - actual_content_lines
52
- 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.5
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