Skip to content

Commit f2731e5

Browse files
author
Alex Evanczuk
authored
Refactor Codeowners File input/output as a dedicated ruby PORO (#45)
* Refactor Codeowners File input/output as a dedicated ruby PORO * Remove Development module for now
1 parent 1fa1ac8 commit f2731e5

File tree

3 files changed

+105
-61
lines changed

3 files changed

+105
-61
lines changed

lib/code_ownership/private.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'code_ownership/private/extension_loader'
66
require 'code_ownership/private/team_plugins/ownership'
77
require 'code_ownership/private/team_plugins/github'
8+
require 'code_ownership/private/codeowners_file'
89
require 'code_ownership/private/parse_js_packages'
910
require 'code_ownership/private/validations/files_have_owners'
1011
require 'code_ownership/private/validations/github_codeowners_up_to_date'
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
# Eventually, we'll get this from the `GlobCache`
22+
glob_to_owner_map_by_mapper_description = {}
23+
24+
Mapper.all.each do |mapper|
25+
mapped_files = mapper.codeowners_lines_to_owners
26+
mapped_files.each do |glob, owner|
27+
next if owner.nil?
28+
glob_to_owner_map_by_mapper_description[mapper.description] ||= {}
29+
glob_to_owner_map_by_mapper_description.fetch(mapper.description)[glob] = owner
30+
end
31+
end
32+
33+
header = <<~HEADER
34+
# STOP! - DO NOT EDIT THIS FILE MANUALLY
35+
# This file was automatically generated by "bin/codeownership validate".
36+
#
37+
# CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub
38+
# teams. This is useful when developers create Pull Requests since the
39+
# code/file owner is notified. Reference GitHub docs for more details:
40+
# https://help.github.com/en/articles/about-code-owners
41+
HEADER
42+
ignored_teams = T.let(Set.new, T::Set[String])
43+
44+
github_team_map = CodeTeams.all.each_with_object({}) do |team, map|
45+
team_github = TeamPlugins::Github.for(team).github
46+
if team_github.do_not_add_to_codeowners_file
47+
ignored_teams << team.name
48+
end
49+
50+
map[team.name] = team_github.team
51+
end
52+
53+
codeowners_file_lines = T.let([], T::Array[String])
54+
55+
glob_to_owner_map_by_mapper_description.each do |mapper_description, ownership_map_cache|
56+
ownership_entries = []
57+
ownership_map_cache.each do |path, code_team|
58+
team_mapping = github_team_map[code_team.name]
59+
next if team_mapping.nil?
60+
next if ignored_teams.include?(code_team.name)
61+
entry = "/#{path} #{team_mapping}"
62+
# In order to use the codeowners file as a proper cache, we'll need to insert commented out entries for ignored teams
63+
# entry = if ignored_teams.include?(code_team.name)
64+
# "# /#{path} #{team_mapping}"
65+
# else
66+
# "/#{path} #{team_mapping}"
67+
# end
68+
ownership_entries << entry
69+
end
70+
71+
next if ownership_entries.none?
72+
codeowners_file_lines += ['', "# #{mapper_description}", *ownership_entries.sort]
73+
end
74+
75+
[
76+
*header.split("\n"),
77+
nil, # For line between header and codeowners_file_lines
78+
*codeowners_file_lines,
79+
nil, # For end-of-file newline
80+
]
81+
end
82+
83+
sig { void }
84+
def self.write!
85+
FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
86+
path.write(expected_contents_lines.join("\n"))
87+
end
88+
89+
sig { returns(Pathname) }
90+
def self.path
91+
Pathname.pwd.join('.github/CODEOWNERS')
92+
end
93+
end
94+
end
95+
end

lib/code_ownership/private/validations/github_codeowners_up_to_date.rb

Lines changed: 9 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -12,55 +12,34 @@ class GithubCodeownersUpToDate
1212
def validation_errors(files:, autocorrect: true, stage_changes: true)
1313
return [] if Private.configuration.skip_codeowners_validation
1414

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
4018

4119
errors = T.let([], T::Array[String])
4220

4321
if !codeowners_up_to_date
4422
if autocorrect
45-
codeowners_filepath.write(expected_contents)
23+
CodeownersFile.write!
4624
if stage_changes
47-
`git add #{codeowners_filepath}`
25+
`git add #{CodeownersFile.path}`
4826
end
4927
else
5028
# If there is no current file or its empty, display a shorter message.
5129
missing_lines = expected_content_lines - actual_content_lines
5230
extra_lines = actual_content_lines - expected_content_lines
31+
5332
missing_lines_text = if missing_lines.any?
5433
<<~COMMENT
5534
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")}
5736
COMMENT
5837
end
5938

6039
extra_lines_text = if extra_lines.any?
6140
<<~COMMENT
6241
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")}
6443
COMMENT
6544
end
6645

@@ -74,7 +53,7 @@ def validation_errors(files:, autocorrect: true, stage_changes: true)
7453
""
7554
end
7655

77-
if actual_contents == ""
56+
if actual_content_lines == []
7857
errors << <<~CODEOWNERS_ERROR
7958
CODEOWNERS out of date. Run `bin/codeownership validate` to update the CODEOWNERS file
8059
CODEOWNERS_ERROR
@@ -90,37 +69,6 @@ def validation_errors(files:, autocorrect: true, stage_changes: true)
9069

9170
errors
9271
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
12472
end
12573
end
12674
end

0 commit comments

Comments
 (0)