code_ownership 1.36.1 → 1.36.3

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: 668edc0471999590cd95e9953151e6e30d02bf64b2bb14e544e5f7c56099c998
4
- data.tar.gz: bbdff6a9af05c8026b33c206d63f083d1c9ed82831bd7b1f3cf3707aa0f54a67
3
+ metadata.gz: eb464ff5e39c0c9f2712a7af6498f30b3b42e3c456e7da52a5209d7f30467346
4
+ data.tar.gz: 5fd3f8974a041ff298fbdaceca0b3eccee027f4d9c94bdfaa344ae9b8ade72d5
5
5
  SHA512:
6
- metadata.gz: 54a6b78ddb108894b20ddee99d0a6637770485b27c154b6676021f512d2aae39159d052e8f2c7986b71c824eb9f3f7bc4a3b9d5bc0c9e1848e7bceecee3e7728
7
- data.tar.gz: b4f39290cfc23f31b73642d3d6a5747ec79efc07210541c4b18c913f448c1df32efe6b4657e91f736055ab30ecf72b4910c22a0266503842aff84ad0e375be62
6
+ metadata.gz: ef51049577f74ab3faed01b4a37df119eaf577465fa10f342b79c0fabf7bc89b59ed993f8c1d8f687c53a3957745dfe579430e081473b9eb63f9b3cbe1f8eb19
7
+ data.tar.gz: ba03e839b81e0dcdfd1c07356b99dbf850b9abddaa83049c2c74e458012c4b81bdfaead12ed6c59fc744848261e078d715f6512df876d1d1fa238362910bfcb9
@@ -14,7 +14,7 @@ module CodeOwnership
14
14
  for_file(argv)
15
15
  elsif command == 'for_team'
16
16
  for_team(argv)
17
- elsif [nil, "help"].include?(command)
17
+ elsif [nil, 'help'].include?(command)
18
18
  puts <<~USAGE
19
19
  Usage: bin/codeownership <subcommand>
20
20
 
@@ -27,7 +27,6 @@ module CodeOwnership
27
27
  else
28
28
  puts "'#{command}' is not a code_ownership command. See `bin/codeownership help`."
29
29
  end
30
-
31
30
  end
32
31
 
33
32
  def self.validate!(argv)
@@ -53,16 +52,16 @@ module CodeOwnership
53
52
  exit
54
53
  end
55
54
  end
56
- args = parser.order!(argv) {}
55
+ args = parser.order!(argv)
57
56
  parser.parse!(args)
58
57
 
59
58
  files = if options[:diff]
60
- ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
61
- File.exist?(file)
62
- end
63
- else
64
- nil
65
- end
59
+ ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
60
+ File.exist?(file)
61
+ end
62
+ else
63
+ nil
64
+ end
66
65
 
67
66
  CodeOwnership.validate!(
68
67
  files: files,
@@ -79,7 +78,7 @@ module CodeOwnership
79
78
  # Long-term, we probably want to use something like `thor` so we don't have to implement logic
80
79
  # like this. In the short-term, this is a simple way for us to use the built-in OptionParser
81
80
  # while having an ergonomic CLI.
82
- files = argv.select { |arg| !arg.start_with?('--') }
81
+ files = argv.reject { |arg| arg.start_with?('--') }
83
82
 
84
83
  parser = OptionParser.new do |opts|
85
84
  opts.banner = 'Usage: bin/codeownership for_file [options]'
@@ -93,22 +92,22 @@ module CodeOwnership
93
92
  exit
94
93
  end
95
94
  end
96
- args = parser.order!(argv) {}
95
+ args = parser.order!(argv)
97
96
  parser.parse!(args)
98
97
 
99
98
  if files.count != 1
100
- raise "Please pass in one file. Use `bin/codeownership for_file --help` for more info"
99
+ raise 'Please pass in one file. Use `bin/codeownership for_file --help` for more info'
101
100
  end
102
-
101
+
103
102
  team = CodeOwnership.for_file(files.first)
104
103
 
105
- team_name = team&.name || "Unowned"
106
- team_yml = team&.config_yml || "Unowned"
104
+ team_name = team&.name || 'Unowned'
105
+ team_yml = team&.config_yml || 'Unowned'
107
106
 
108
107
  if options[:json]
109
108
  json = {
110
109
  team_name: team_name,
111
- team_yml: team_yml,
110
+ team_yml: team_yml
112
111
  }
113
112
 
114
113
  puts json.to_json
@@ -121,8 +120,6 @@ module CodeOwnership
121
120
  end
122
121
 
123
122
  def self.for_team(argv)
124
- options = {}
125
-
126
123
  parser = OptionParser.new do |opts|
127
124
  opts.banner = 'Usage: bin/codeownership for_team \'Team Name\''
128
125
 
@@ -131,14 +128,14 @@ module CodeOwnership
131
128
  exit
132
129
  end
133
130
  end
134
- teams = argv.select { |arg| !arg.start_with?('--') }
135
- args = parser.order!(argv) {}
131
+ teams = argv.reject { |arg| arg.start_with?('--') }
132
+ args = parser.order!(argv)
136
133
  parser.parse!(args)
137
134
 
138
135
  if teams.count != 1
139
- raise "Please pass in one team. Use `bin/codeownership for_team --help` for more info"
136
+ raise 'Please pass in one team. Use `bin/codeownership for_team --help` for more info'
140
137
  end
141
-
138
+
142
139
  puts CodeOwnership.for_team(teams.first)
143
140
  end
144
141
 
@@ -17,8 +17,8 @@ module CodeOwnership
17
17
  def self.fetch
18
18
  config_hash = YAML.load_file('config/code_ownership.yml')
19
19
 
20
- if config_hash.key?("require")
21
- config_hash["require"].each do |require_directive|
20
+ if config_hash.key?('require')
21
+ config_hash['require'].each do |require_directive|
22
22
  Private::ExtensionLoader.load(require_directive)
23
23
  end
24
24
  end
@@ -29,21 +29,19 @@ module CodeOwnership
29
29
  # This should be fast when run with ONE file
30
30
  #
31
31
  sig do
32
- abstract.params(file: String).
33
- returns(T.nilable(::CodeTeams::Team))
34
- end
35
- def map_file_to_owner(file)
32
+ abstract.params(file: String)
33
+ .returns(T.nilable(::CodeTeams::Team))
36
34
  end
35
+ def map_file_to_owner(file); end
37
36
 
38
37
  #
39
38
  # This should be fast when run with MANY files
40
39
  #
41
40
  sig do
42
- abstract.params(files: T::Array[String]).
43
- returns(T::Hash[String, ::CodeTeams::Team])
44
- end
45
- def globs_to_owner(files)
41
+ abstract.params(files: T::Array[String])
42
+ .returns(T::Hash[String, ::CodeTeams::Team])
46
43
  end
44
+ def globs_to_owner(files); end
47
45
 
48
46
  #
49
47
  # This should be fast when run with MANY files
@@ -51,16 +49,13 @@ module CodeOwnership
51
49
  sig do
52
50
  abstract.params(cache: GlobsToOwningTeamMap, files: T::Array[String]).returns(GlobsToOwningTeamMap)
53
51
  end
54
- def update_cache(cache, files)
55
- end
52
+ def update_cache(cache, files); end
56
53
 
57
54
  sig { abstract.returns(String) }
58
- def description
59
- end
55
+ def description; end
60
56
 
61
57
  sig { abstract.void }
62
- def bust_caches!
63
- end
58
+ def bust_caches!; end
64
59
 
65
60
  sig { returns(Private::GlobCache) }
66
61
  def self.to_glob_cache
@@ -72,6 +67,7 @@ module CodeOwnership
72
67
 
73
68
  mapped_files.each do |glob, owner|
74
69
  next if owner.nil?
70
+
75
71
  glob_to_owner_map_by_mapper_description.fetch(mapper.description)[glob] = owner
76
72
  end
77
73
  end
@@ -13,15 +13,15 @@ module CodeOwnership
13
13
 
14
14
  sig { returns(T::Array[String]) }
15
15
  def self.actual_contents_lines
16
- if !path.exist?
17
- [""]
18
- else
16
+ if path.exist?
19
17
  content = path.read
20
18
  lines = path.read.split("\n")
21
19
  if content.end_with?("\n")
22
- lines << ""
20
+ lines << ''
23
21
  end
24
22
  lines
23
+ else
24
+ ['']
25
25
  end
26
26
  end
27
27
 
@@ -53,7 +53,7 @@ module CodeOwnership
53
53
 
54
54
  cache.each do |mapper_description, ownership_map_cache|
55
55
  ownership_entries = []
56
- sorted_ownership_map_cache = ownership_map_cache.sort_by do |glob, team|
56
+ sorted_ownership_map_cache = ownership_map_cache.sort_by do |glob, _team|
57
57
  glob
58
58
  end
59
59
  sorted_ownership_map_cache.to_h.each do |path, code_team|
@@ -64,22 +64,23 @@ module CodeOwnership
64
64
  # 1) It allows the CODEOWNERS file to be used as a cache for validations
65
65
  # 2) It allows users to specifically see what their team will not be notified about.
66
66
  entry = if ignored_teams.include?(code_team.name)
67
- "# /#{path} #{team_mapping}"
68
- else
69
- "/#{path} #{team_mapping}"
70
- end
67
+ "# /#{path} #{team_mapping}"
68
+ else
69
+ "/#{path} #{team_mapping}"
70
+ end
71
71
  ownership_entries << entry
72
72
  end
73
73
 
74
74
  next if ownership_entries.none?
75
+
75
76
  codeowners_file_lines += ['', "# #{mapper_description}", *ownership_entries.sort]
76
77
  end
77
78
 
78
79
  [
79
80
  *header.split("\n"),
80
- "", # For line between header and codeowners_file_lines
81
+ '', # For line between header and codeowners_file_lines
81
82
  *codeowners_file_lines,
82
- "", # For end-of-file newline
83
+ '' # For end-of-file newline
83
84
  ]
84
85
  end
85
86
 
@@ -123,20 +124,22 @@ module CodeOwnership
123
124
  mapper_descriptions = Set.new(Mapper.all.map(&:description))
124
125
 
125
126
  path.readlines.each do |line|
126
- line_with_no_comment = line.chomp.gsub("# ", "")
127
+ line_with_no_comment = line.chomp.gsub('# ', '')
127
128
  if mapper_descriptions.include?(line_with_no_comment)
128
129
  current_mapper = line_with_no_comment
129
130
  else
130
131
  next if current_mapper.nil?
131
- next if line.chomp == ""
132
+ next if line.chomp == ''
133
+
132
134
  # The codeowners file stores paths relative to the root of directory
133
135
  # Since a `/` means root of the file system from the perspective of ruby,
134
136
  # we remove that beginning slash so we can correctly glob the files out.
135
- normalized_line = line.gsub(/^# /, '').gsub(/^\//, '')
137
+ normalized_line = line.gsub(/^# /, '').gsub(%r{^/}, '')
136
138
  split_line = normalized_line.split
137
139
  # Most lines will be in the format: /path/to/file my-github-team
138
140
  # This will skip over lines that are not of the correct form
139
141
  next if split_line.count > 2
142
+
140
143
  entry, github_team = split_line
141
144
  code_team = github_team_to_code_team_map[T.must(github_team)]
142
145
  # If a GitHub team is changed and a user runs `bin/codeownership validate`, we won't be able to identify
@@ -144,6 +147,7 @@ module CodeOwnership
144
147
  # Therefore, if we can't determine the team, we just skip it.
145
148
  # This affects how complete the cache is, but that will still be caught by `bin/codeownership validate`.
146
149
  next if code_team.nil?
150
+
147
151
  raw_cache_contents[current_mapper] ||= {}
148
152
  raw_cache_contents.fetch(current_mapper)[T.must(entry)] = github_team_to_code_team_map.fetch(T.must(github_team))
149
153
  end
@@ -12,7 +12,7 @@ module CodeOwnership
12
12
  def load(require_directive)
13
13
  # We want to transform the require directive to behave differently
14
14
  # if it's a specific local file being required versus a gem
15
- if require_directive.start_with?(".")
15
+ if require_directive.start_with?('.')
16
16
  require File.join(Pathname.pwd, require_directive)
17
17
  else
18
18
  require require_directive
@@ -34,14 +34,15 @@ module CodeOwnership
34
34
 
35
35
  sig { params(files: T::Array[String]).returns(FilesByMapper) }
36
36
  def mapper_descriptions_that_map_files(files)
37
- if files.count > 100
38
- # When looking at many files, expanding the cache out using Dir.glob and checking for intersections is faster
39
- files_by_mappers = files.map{ |f| [f, Set.new([]) ]}.to_h
40
- files_by_mappers.merge(files_by_mappers_via_expanded_cache)
41
- else
42
- # When looking at few files, using File.fnmatch is faster
43
- files_by_mappers_via_file_fnmatch(files)
37
+ files_by_mappers = files.to_h { |f| [f, Set.new([])] }
38
+
39
+ files_by_mappers_via_expanded_cache.each do |file, mappers|
40
+ mappers.each do |mapper|
41
+ T.must(files_by_mappers[file]) << mapper if files_by_mappers[file]
42
+ end
44
43
  end
44
+
45
+ files_by_mappers
45
46
  end
46
47
 
47
48
  private
@@ -53,25 +54,19 @@ module CodeOwnership
53
54
  @expanded_cache ||= begin
54
55
  expanded_cache = {}
55
56
  @raw_cache_contents.each do |mapper_description, globs_by_owner|
56
- expanded_cache[mapper_description] = {}
57
- globs_by_owner.each do |glob, owner|
58
- Dir.glob(glob).each do |file, owner|
59
- expanded_cache[mapper_description][file] = owner
60
- end
61
- end
57
+ expanded_cache[mapper_description] = OwnerAssigner.assign_owners(globs_by_owner)
62
58
  end
63
-
64
59
  expanded_cache
65
60
  end
66
61
  end
67
62
 
68
63
  sig { returns(FilesByMapper) }
69
64
  def files_by_mappers_via_expanded_cache
70
- @files_by_mappers ||= T.let(@files_by_mappers, T.nilable(FilesByMapper))
71
- @files_by_mappers ||= begin
65
+ @files_by_mappers_via_expanded_cache ||= T.let(@files_by_mappers_via_expanded_cache, T.nilable(FilesByMapper))
66
+ @files_by_mappers_via_expanded_cache ||= begin
72
67
  files_by_mappers = T.let({}, FilesByMapper)
73
68
  expanded_cache.each do |mapper_description, file_by_owner|
74
- file_by_owner.each do |file, owner|
69
+ file_by_owner.each_key do |file|
75
70
  files_by_mappers[file] ||= Set.new([])
76
71
  files_by_mappers.fetch(file) << mapper_description
77
72
  end
@@ -80,31 +75,6 @@ module CodeOwnership
80
75
  files_by_mappers
81
76
  end
82
77
  end
83
-
84
- sig { params(files: T::Array[String]).returns(FilesByMapper) }
85
- def files_by_mappers_via_file_fnmatch(files)
86
- files_by_mappers = T.let({}, FilesByMapper)
87
-
88
- files.each do |file|
89
- files_by_mappers[file] ||= Set.new([])
90
- @raw_cache_contents.each do |mapper_description, globs_by_owner|
91
- # As much as I'd like to *not* special case the file annotations mapper, using File.fnmatch? on the thousands of files mapped by the
92
- # file annotations mapper is a lot of unnecessary extra work.
93
- # Therefore we can just check if the file is in the globs directly for file annotations, otherwise use File.fnmatch
94
- if mapper_description == OwnershipMappers::FileAnnotations::DESCRIPTION
95
- files_by_mappers.fetch(file) << mapper_description if globs_by_owner[file]
96
- else
97
- globs_by_owner.each do |glob, owner|
98
- if File.fnmatch?(glob, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
99
- files_by_mappers.fetch(file) << mapper_description
100
- end
101
- end
102
- end
103
- end
104
- end
105
-
106
- files_by_mappers
107
- end
108
78
  end
109
79
  end
110
80
  end
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CodeOwnership
5
+ module Private
6
+ class OwnerAssigner
7
+ extend T::Sig
8
+
9
+ sig { params(globs_to_owning_team_map: GlobsToOwningTeamMap).returns(GlobsToOwningTeamMap) }
10
+ def self.assign_owners(globs_to_owning_team_map)
11
+ globs_to_owning_team_map.each_with_object({}) do |(glob, owner), mapping|
12
+ # addresses the case where a directory name includes regex characters
13
+ # such as `app/services/[test]/some_other_file.ts`
14
+ mapping[glob] = owner if File.exist?(glob)
15
+ Dir.glob(glob).each do |file|
16
+ mapping[file] ||= owner
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -14,8 +14,8 @@ module CodeOwnership
14
14
  @@directory_cache = T.let({}, T::Hash[String, T.nilable(CodeTeams::Team)]) # rubocop:disable Style/ClassVars
15
15
 
16
16
  sig do
17
- override.params(file: String).
18
- returns(T.nilable(::CodeTeams::Team))
17
+ override.params(file: String)
18
+ .returns(T.nilable(::CodeTeams::Team))
19
19
  end
20
20
  def map_file_to_owner(file)
21
21
  map_file_to_relevant_owner(file)
@@ -37,8 +37,8 @@ module CodeOwnership
37
37
  # subset of files, but rather we want code ownership for all files.
38
38
  #
39
39
  sig do
40
- override.params(files: T::Array[String]).
41
- returns(T::Hash[String, ::CodeTeams::Team])
40
+ override.params(files: T::Array[String])
41
+ .returns(T::Hash[String, ::CodeTeams::Team])
42
42
  end
43
43
  def globs_to_owner(files)
44
44
  # The T.unsafe is because the upstream RBI is wrong for Pathname.glob
@@ -97,7 +97,7 @@ module CodeOwnership
97
97
  (path_components.length - 1).downto(0).each do |i|
98
98
  team = get_team_from_codeowners_file_within_directory(
99
99
  Pathname.new(File.join(*T.unsafe(path_components[0...i])))
100
- )
100
+ )
101
101
  return team unless team.nil?
102
102
  end
103
103
 
@@ -121,7 +121,7 @@ module CodeOwnership
121
121
  @@directory_cache[potential_codeowners_file_name] = nil
122
122
  end
123
123
 
124
- return team
124
+ team
125
125
  end
126
126
  end
127
127
  end
@@ -18,24 +18,24 @@ module CodeOwnership
18
18
  extend T::Sig
19
19
  include Mapper
20
20
 
21
- TEAM_PATTERN = T.let(/\A(?:#|\/\/) @team (?<team>.*)\Z/.freeze, Regexp)
21
+ TEAM_PATTERN = T.let(%r{\A(?:#|//) @team (?<team>.*)\Z}.freeze, Regexp)
22
22
  DESCRIPTION = 'Annotations at the top of file'
23
23
 
24
24
  sig do
25
- override.params(file: String).
26
- returns(T.nilable(::CodeTeams::Team))
25
+ override.params(file: String)
26
+ .returns(T.nilable(::CodeTeams::Team))
27
27
  end
28
28
  def map_file_to_owner(file)
29
29
  file_annotation_based_owner(file)
30
30
  end
31
31
 
32
32
  sig do
33
- override.
34
- params(files: T::Array[String]).
35
- returns(T::Hash[String, ::CodeTeams::Team])
33
+ override
34
+ .params(files: T::Array[String])
35
+ .returns(T::Hash[String, ::CodeTeams::Team])
36
36
  end
37
37
  def globs_to_owner(files)
38
- files.each_with_object({}) do |filename_relative_to_root, mapping| # rubocop:disable Style/ClassVars
38
+ files.each_with_object({}) do |filename_relative_to_root, mapping|
39
39
  owner = file_annotation_based_owner(filename_relative_to_root)
40
40
  next unless owner
41
41
 
@@ -56,10 +56,10 @@ module CodeOwnership
56
56
  invalid_files = cache.keys.select do |file|
57
57
  # If a file is not tracked, it should be removed from the cache
58
58
  !Private.file_tracked?(file) ||
59
- # If a file no longer has a file annotation (i.e. `globs_to_owner` doesn't map it)
60
- # it should be removed from the cache
61
- # We make sure to only apply this to the input files since otherwise `updated_cache_for_files.key?(file)` would always return `false` when files == []
62
- (fileset.include?(file) && !updated_cache_for_files.key?(file))
59
+ # If a file no longer has a file annotation (i.e. `globs_to_owner` doesn't map it)
60
+ # it should be removed from the cache
61
+ # We make sure to only apply this to the input files since otherwise `updated_cache_for_files.key?(file)` would always return `false` when files == []
62
+ (fileset.include?(file) && !updated_cache_for_files.key?(file))
63
63
  end
64
64
 
65
65
  invalid_files.each do |invalid_file|
@@ -83,14 +83,14 @@ module CodeOwnership
83
83
  # and if the annotation isn't in the first two lines we assume it
84
84
  # doesn't exist.
85
85
 
86
- line_1 = File.foreach(filename).first
86
+ line1 = File.foreach(filename).first
87
87
 
88
- return if !line_1
88
+ return if !line1
89
89
 
90
90
  begin
91
- team = line_1[TEAM_PATTERN, :team]
92
- rescue ArgumentError => ex
93
- if ex.message.include?('invalid byte sequence')
91
+ team = line1[TEAM_PATTERN, :team]
92
+ rescue ArgumentError => e
93
+ if e.message.include?('invalid byte sequence')
94
94
  team = nil
95
95
  else
96
96
  raise
@@ -110,7 +110,7 @@ module CodeOwnership
110
110
  if file_annotation_based_owner(filename)
111
111
  filepath = Pathname.new(filename)
112
112
  lines = filepath.read.split("\n")
113
- new_lines = lines.select { |line| !line[TEAM_PATTERN] }
113
+ new_lines = lines.reject { |line| line[TEAM_PATTERN] }
114
114
  # We explicitly add a final new line since splitting by new line when reading the file lines
115
115
  # ignores new lines at the ends of files
116
116
  # We also remove leading new lines, since there is after a new line after an annotation
@@ -125,8 +125,7 @@ module CodeOwnership
125
125
  end
126
126
 
127
127
  sig { override.void }
128
- def bust_caches!
129
- end
128
+ def bust_caches!; end
130
129
  end
131
130
  end
132
131
  end
@@ -12,8 +12,8 @@ module CodeOwnership
12
12
  @@package_json_cache = T.let({}, T::Hash[String, T.nilable(ParseJsPackages::Package)]) # rubocop:disable Style/ClassVars
13
13
 
14
14
  sig do
15
- override.params(file: String).
16
- returns(T.nilable(::CodeTeams::Team))
15
+ override.params(file: String)
16
+ .returns(T.nilable(::CodeTeams::Team))
17
17
  end
18
18
  def map_file_to_owner(file)
19
19
  package = map_file_to_relevant_package(file)
@@ -39,8 +39,8 @@ module CodeOwnership
39
39
  # subset of files, but rather we want code ownership for all files.
40
40
  #
41
41
  sig do
42
- override.params(files: T::Array[String]).
43
- returns(T::Hash[String, ::CodeTeams::Team])
42
+ override.params(files: T::Array[String])
43
+ .returns(T::Hash[String, ::CodeTeams::Team])
44
44
  end
45
45
  def globs_to_owner(files)
46
46
  ParseJsPackages.all.each_with_object({}) do |package, res|
@@ -85,8 +85,8 @@ module CodeOwnership
85
85
 
86
86
  (path_components.length - 1).downto(0).each do |i|
87
87
  potential_relative_path_name = T.must(path_components[0...i]).reduce(Pathname.new('')) { |built_path, path| built_path.join(path) }
88
- potential_package_json_path = potential_relative_path_name.
89
- join(ParseJsPackages::PACKAGE_JSON_NAME)
88
+ potential_package_json_path = potential_relative_path_name
89
+ .join(ParseJsPackages::PACKAGE_JSON_NAME)
90
90
 
91
91
  potential_package_json_string = potential_package_json_path.to_s
92
92
 
@@ -10,8 +10,8 @@ module CodeOwnership
10
10
  include Mapper
11
11
 
12
12
  sig do
13
- override.params(file: String).
14
- returns(T.nilable(::CodeTeams::Team))
13
+ override.params(file: String)
14
+ .returns(T.nilable(::CodeTeams::Team))
15
15
  end
16
16
  def map_file_to_owner(file)
17
17
  package = Packs.for_file(file)
@@ -30,8 +30,8 @@ module CodeOwnership
30
30
  # subset of files, but rather we want code ownership for all files.
31
31
  #
32
32
  sig do
33
- override.params(files: T::Array[String]).
34
- returns(T::Hash[String, ::CodeTeams::Team])
33
+ override.params(files: T::Array[String])
34
+ .returns(T::Hash[String, ::CodeTeams::Team])
35
35
  end
36
36
  def globs_to_owner(files)
37
37
  Packs.all.each_with_object({}) do |package, res|
@@ -66,8 +66,7 @@ module CodeOwnership
66
66
  end
67
67
 
68
68
  sig { override.void }
69
- def bust_caches!
70
- end
69
+ def bust_caches!; end
71
70
  end
72
71
  end
73
72
  end
@@ -14,11 +14,11 @@ module CodeOwnership
14
14
  @@map_files_to_owners = {} # rubocop:disable Style/ClassVars
15
15
 
16
16
  sig do
17
- params(files: T::Array[String]).
18
- returns(T::Hash[String, ::CodeTeams::Team])
17
+ params(files: T::Array[String])
18
+ .returns(T::Hash[String, ::CodeTeams::Team])
19
19
  end
20
- def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
21
- return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count > 0
20
+ def map_files_to_owners(files)
21
+ return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count.positive?
22
22
 
23
23
  @@map_files_to_owners = CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
24
24
  TeamPlugins::Ownership.for(team).owned_globs.each do |glob|
@@ -42,7 +42,7 @@ module CodeOwnership
42
42
  sig { returns(String) }
43
43
  def description
44
44
  # These are sorted only to prevent non-determinism in output between local and CI environments.
45
- sorted_contexts = mapping_contexts.sort_by{|context| context.team.config_yml.to_s }
45
+ sorted_contexts = mapping_contexts.sort_by { |context| context.team.config_yml.to_s }
46
46
  description_args = sorted_contexts.map do |context|
47
47
  "`#{context.glob}` (from `#{context.team.config_yml}`)"
48
48
  end
@@ -56,7 +56,7 @@ module CodeOwnership
56
56
  end
57
57
  def find_overlapping_globs
58
58
  mapped_files = T.let({}, T::Hash[String, T::Array[MappingContext]])
59
- CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
59
+ CodeTeams.all.each_with_object({}) do |team, _map|
60
60
  TeamPlugins::Ownership.for(team).owned_globs.each do |glob|
61
61
  Dir.glob(glob).each do |filename|
62
62
  mapped_files[filename] ||= []
@@ -66,24 +66,22 @@ module CodeOwnership
66
66
  end
67
67
 
68
68
  overlaps = T.let([], T::Array[GlobOverlap])
69
- mapped_files.each do |filename, mapping_contexts|
69
+ mapped_files.each_value do |mapping_contexts|
70
70
  if mapping_contexts.count > 1
71
71
  overlaps << GlobOverlap.new(mapping_contexts: mapping_contexts)
72
72
  end
73
73
  end
74
74
 
75
- deduplicated_overlaps = overlaps.uniq do |glob_overlap|
75
+ overlaps.uniq do |glob_overlap|
76
76
  glob_overlap.mapping_contexts.map do |context|
77
77
  [context.glob, context.team.name]
78
78
  end
79
79
  end
80
-
81
- deduplicated_overlaps
82
80
  end
83
81
 
84
82
  sig do
85
- override.params(file: String).
86
- returns(T.nilable(::CodeTeams::Team))
83
+ override.params(file: String)
84
+ .returns(T.nilable(::CodeTeams::Team))
87
85
  end
88
86
  def map_file_to_owner(file)
89
87
  map_files_to_owners([file])[file]
@@ -97,11 +95,11 @@ module CodeOwnership
97
95
  end
98
96
 
99
97
  sig do
100
- override.params(files: T::Array[String]).
101
- returns(T::Hash[String, ::CodeTeams::Team])
98
+ override.params(files: T::Array[String])
99
+ .returns(T::Hash[String, ::CodeTeams::Team])
102
100
  end
103
101
  def globs_to_owner(files)
104
- CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
102
+ CodeTeams.all.each_with_object({}) do |team, map|
105
103
  TeamPlugins::Ownership.for(team).owned_globs.each do |owned_glob|
106
104
  map[owned_glob] = team
107
105
  end
@@ -128,7 +126,7 @@ module CodeOwnership
128
126
  errors << <<~MSG
129
127
  `owned_globs` cannot overlap between teams. The following globs overlap:
130
128
 
131
- #{overlapping_globs.map { |overlap| "- #{overlap.description}"}.join("\n")}
129
+ #{overlapping_globs.map { |overlap| "- #{overlap.description}" }.join("\n")}
132
130
  MSG
133
131
  end
134
132
 
@@ -13,11 +13,11 @@ module CodeOwnership
13
13
  @@map_files_to_owners = {} # rubocop:disable Style/ClassVars
14
14
 
15
15
  sig do
16
- params(files: T::Array[String]).
17
- returns(T::Hash[String, ::CodeTeams::Team])
16
+ params(files: T::Array[String])
17
+ .returns(T::Hash[String, ::CodeTeams::Team])
18
18
  end
19
- def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
20
- return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count > 0
19
+ def map_files_to_owners(files)
20
+ return @@map_files_to_owners if @@map_files_to_owners&.keys && @@map_files_to_owners.keys.count.positive?
21
21
 
22
22
  @@map_files_to_owners = CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
23
23
  map[team.config_yml] = team
@@ -25,19 +25,19 @@ module CodeOwnership
25
25
  end
26
26
 
27
27
  sig do
28
- override.params(file: String).
29
- returns(T.nilable(::CodeTeams::Team))
28
+ override.params(file: String)
29
+ .returns(T.nilable(::CodeTeams::Team))
30
30
  end
31
31
  def map_file_to_owner(file)
32
32
  map_files_to_owners([file])[file]
33
33
  end
34
34
 
35
35
  sig do
36
- override.params(files: T::Array[String]).
37
- returns(T::Hash[String, ::CodeTeams::Team])
36
+ override.params(files: T::Array[String])
37
+ .returns(T::Hash[String, ::CodeTeams::Team])
38
38
  end
39
39
  def globs_to_owner(files)
40
- CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
40
+ CodeTeams.all.each_with_object({}) do |team, map|
41
41
  map[team.config_yml] = team
42
42
  end
43
43
  end
@@ -23,10 +23,10 @@ module CodeOwnership
23
23
  package_loaded_json = JSON.parse(pathname.read)
24
24
 
25
25
  package_name = if pathname.dirname == Pathname.new('.')
26
- ROOT_PACKAGE_NAME
27
- else
28
- pathname.dirname.cleanpath.to_s
29
- end
26
+ ROOT_PACKAGE_NAME
27
+ else
28
+ pathname.dirname.cleanpath.to_s
29
+ end
30
30
 
31
31
  new(
32
32
  name: package_name,
@@ -41,7 +41,7 @@ module CodeOwnership
41
41
  Please either make the JSON in that file valid or specify `js_package_paths` in config/code_ownership.yml.
42
42
  MESSAGE
43
43
 
44
- raise InvalidCodeOwnershipConfigurationError.new(error_message)
44
+ raise InvalidCodeOwnershipConfigurationError, error_message
45
45
  end
46
46
 
47
47
  sig { returns(Pathname) }
@@ -1,4 +1,3 @@
1
-
2
1
  # typed: strict
3
2
  # frozen_string_literal: true
4
3
 
@@ -12,8 +12,8 @@ module CodeOwnership
12
12
  def validation_errors(files:, autocorrect: true, stage_changes: true)
13
13
  cache = Private.glob_cache
14
14
  file_mappings = cache.mapper_descriptions_that_map_files(files)
15
- files_not_mapped_at_all = file_mappings.select do |file, mapper_descriptions|
16
- mapper_descriptions.count == 0
15
+ files_not_mapped_at_all = file_mappings.select do |_file, mapper_descriptions|
16
+ mapper_descriptions.count.zero?
17
17
  end
18
18
 
19
19
  errors = T.let([], T::Array[String])
@@ -22,7 +22,7 @@ module CodeOwnership
22
22
  errors << <<~MSG
23
23
  Some files are missing ownership:
24
24
 
25
- #{files_not_mapped_at_all.map { |file, mappers| "- #{file}" }.join("\n")}
25
+ #{files_not_mapped_at_all.map { |file, _mappers| "- #{file}" }.join("\n")}
26
26
  MSG
27
27
  end
28
28
 
@@ -12,7 +12,7 @@ module CodeOwnership
12
12
  def validation_errors(files:, autocorrect: true, stage_changes: true)
13
13
  cache = Private.glob_cache
14
14
  file_mappings = cache.mapper_descriptions_that_map_files(files)
15
- files_mapped_by_multiple_mappers = file_mappings.select do |file, mapper_descriptions|
15
+ files_mapped_by_multiple_mappers = file_mappings.select do |_file, mapper_descriptions|
16
16
  mapper_descriptions.count > 1
17
17
  end
18
18
 
@@ -25,7 +25,7 @@ module CodeOwnership
25
25
  `git add #{CodeownersFile.path}`
26
26
  end
27
27
  # If there is no current file or its empty, display a shorter message.
28
- elsif actual_content_lines == [""]
28
+ elsif actual_content_lines == ['']
29
29
  errors << <<~CODEOWNERS_ERROR
30
30
  CODEOWNERS out of date. Run `bin/codeownership validate` to update the CODEOWNERS file
31
31
  CODEOWNERS_ERROR
@@ -34,33 +34,33 @@ module CodeOwnership
34
34
  extra_lines = actual_content_lines - expected_content_lines
35
35
 
36
36
  missing_lines_text = if missing_lines.any?
37
- <<~COMMENT
38
- CODEOWNERS should contain the following lines, but does not:
39
- #{(missing_lines).map { |line| "- \"#{line}\""}.join("\n")}
40
- COMMENT
41
- end
37
+ <<~COMMENT
38
+ CODEOWNERS should contain the following lines, but does not:
39
+ #{missing_lines.map { |line| "- \"#{line}\"" }.join("\n")}
40
+ COMMENT
41
+ end
42
42
 
43
43
  extra_lines_text = if extra_lines.any?
44
- <<~COMMENT
45
- CODEOWNERS should not contain the following lines, but it does:
46
- #{(extra_lines).map { |line| "- \"#{line}\""}.join("\n")}
47
- COMMENT
48
- end
44
+ <<~COMMENT
45
+ CODEOWNERS should not contain the following lines, but it does:
46
+ #{extra_lines.map { |line| "- \"#{line}\"" }.join("\n")}
47
+ COMMENT
48
+ end
49
49
 
50
50
  diff_text = if missing_lines_text && extra_lines_text
51
- "#{missing_lines_text}\n#{extra_lines_text}".chomp
52
- elsif missing_lines_text
53
- missing_lines_text
54
- elsif extra_lines_text
55
- extra_lines_text
56
- else
57
- <<~TEXT
58
- There may be extra lines, or lines are out of order.
59
- You can try to regenerate the CODEOWNERS file from scratch:
60
- 1) `rm .github/CODEOWNERS`
61
- 2) `bin/codeownership validate`
62
- TEXT
63
- end
51
+ "#{missing_lines_text}\n#{extra_lines_text}".chomp
52
+ elsif missing_lines_text
53
+ missing_lines_text
54
+ elsif extra_lines_text
55
+ extra_lines_text
56
+ else
57
+ <<~TEXT
58
+ There may be extra lines, or lines are out of order.
59
+ You can try to regenerate the CODEOWNERS file from scratch:
60
+ 1) `rm .github/CODEOWNERS`
61
+ 2) `bin/codeownership validate`
62
+ TEXT
63
+ end
64
64
 
65
65
  errors << <<~CODEOWNERS_ERROR
66
66
  CODEOWNERS out of date. Run `bin/codeownership validate` to update the CODEOWNERS file
@@ -8,6 +8,7 @@ require 'code_ownership/private/team_plugins/github'
8
8
  require 'code_ownership/private/codeowners_file'
9
9
  require 'code_ownership/private/parse_js_packages'
10
10
  require 'code_ownership/private/glob_cache'
11
+ require 'code_ownership/private/owner_assigner'
11
12
  require 'code_ownership/private/validations/files_have_owners'
12
13
  require 'code_ownership/private/validations/github_codeowners_up_to_date'
13
14
  require 'code_ownership/private/validations/files_have_unique_owners'
@@ -91,14 +92,13 @@ module CodeOwnership
91
92
  # However, globbing out can take 5 or more seconds on a large repository, dramatically slowing down
92
93
  # invocations to `bin/codeownership validate --diff`.
93
94
  # Using `File.fnmatch?` is a lot faster!
94
- in_owned_globs = configuration.owned_globs.all? do |owned_glob|
95
+ in_owned_globs = configuration.owned_globs.any? do |owned_glob|
95
96
  File.fnmatch?(owned_glob, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
96
97
  end
97
98
 
98
99
  in_unowned_globs = configuration.unowned_globs.any? do |unowned_glob|
99
100
  File.fnmatch?(unowned_glob, file, File::FNM_PATHNAME | File::FNM_EXTGLOB)
100
101
  end
101
-
102
102
  in_owned_globs && !in_unowned_globs && File.exist?(file)
103
103
  end
104
104
 
@@ -115,13 +115,11 @@ module CodeOwnership
115
115
  sig { returns(GlobCache) }
116
116
  def self.glob_cache
117
117
  @glob_cache ||= T.let(@glob_cache, T.nilable(GlobCache))
118
- @glob_cache ||= begin
119
- if CodeownersFile.use_codeowners_cache?
120
- CodeownersFile.to_glob_cache
121
- else
122
- Mapper.to_glob_cache
123
- end
124
- end
118
+ @glob_cache ||= if CodeownersFile.use_codeowners_cache?
119
+ CodeownersFile.to_glob_cache
120
+ else
121
+ Mapper.to_glob_cache
122
+ end
125
123
  end
126
124
  end
127
125
 
@@ -8,8 +8,7 @@ module CodeOwnership
8
8
  interface!
9
9
 
10
10
  sig { abstract.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
11
- def validation_errors(files:, autocorrect: true, stage_changes: true)
12
- end
11
+ def validation_errors(files:, autocorrect: true, stage_changes: true); end
13
12
 
14
13
  class << self
15
14
  extend T::Sig
@@ -18,7 +18,8 @@ if defined?(Packwerk)
18
18
  end
19
19
 
20
20
  module CodeOwnership
21
- extend self
21
+ module_function
22
+
22
23
  extend T::Sig
23
24
  extend T::Helpers
24
25
 
@@ -39,7 +40,7 @@ module CodeOwnership
39
40
 
40
41
  Mapper.all.each do |mapper|
41
42
  owner = mapper.map_file_to_owner(file)
42
- break if owner
43
+ break if owner # TODO: what if there are multiple owners? Should we respond with an error instead of the first match?
43
44
  end
44
45
 
45
46
  @for_file[file] = owner
@@ -57,6 +58,7 @@ module CodeOwnership
57
58
  ownership_for_mapper = []
58
59
  glob_to_owning_team_map.each do |glob, owning_team|
59
60
  next if owning_team != team
61
+
60
62
  ownership_for_mapper << "- #{glob}"
61
63
  end
62
64
 
@@ -66,7 +68,7 @@ module CodeOwnership
66
68
  ownership_information += ownership_for_mapper.sort
67
69
  end
68
70
 
69
- ownership_information << ""
71
+ ownership_information << ''
70
72
  end
71
73
 
72
74
  ownership_information.join("\n")
@@ -84,7 +86,7 @@ module CodeOwnership
84
86
  params(
85
87
  autocorrect: T::Boolean,
86
88
  stage_changes: T::Boolean,
87
- files: T.nilable(T::Array[String]),
89
+ files: T.nilable(T::Array[String])
88
90
  ).void
89
91
  end
90
92
  def validate!(
@@ -95,10 +97,10 @@ module CodeOwnership
95
97
  Private.load_configuration!
96
98
 
97
99
  tracked_file_subset = if files
98
- files.select{|f| Private.file_tracked?(f)}
99
- else
100
- Private.tracked_files
101
- end
100
+ files.select { |f| Private.file_tracked?(f) }
101
+ else
102
+ Private.tracked_files
103
+ end
102
104
 
103
105
  Private.validate!(files: tracked_file_subset, autocorrect: autocorrect, stage_changes: stage_changes)
104
106
  end
@@ -150,7 +152,7 @@ module CodeOwnership
150
152
 
151
153
  [
152
154
  CodeOwnership.for_file(file),
153
- file,
155
+ file
154
156
  ]
155
157
  end
156
158
  end
@@ -161,15 +163,15 @@ module CodeOwnership
161
163
  @memoized_values ||= T.let(@memoized_values, T.nilable(T::Hash[String, T.nilable(::CodeTeams::Team)]))
162
164
  @memoized_values ||= {}
163
165
  # We use key because the memoized value could be `nil`
164
- if !@memoized_values.key?(klass.to_s)
166
+ if @memoized_values.key?(klass.to_s)
167
+ @memoized_values[klass.to_s]
168
+ else
165
169
  path = Private.path_from_klass(klass)
166
170
  return nil if path.nil?
167
171
 
168
172
  value_to_memoize = for_file(path)
169
173
  @memoized_values[klass.to_s] = value_to_memoize
170
174
  value_to_memoize
171
- else
172
- @memoized_values[klass.to_s]
173
175
  end
174
176
  end
175
177
 
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.36.1
4
+ version: 1.36.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineers
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-12 00:00:00.000000000 Z
11
+ date: 2024-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: code_teams
@@ -44,16 +44,16 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 0.5.10821
47
+ version: 0.5.11249
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 0.5.10821
54
+ version: 0.5.11249
55
55
  - !ruby/object:Gem::Dependency
56
- name: pry
56
+ name: debug
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rake
70
+ name: packwerk
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,21 +81,21 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rspec
84
+ name: railties
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '3.0'
89
+ version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '3.0'
96
+ version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: sorbet
98
+ name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -109,7 +109,21 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: tapioca
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
113
127
  requirement: !ruby/object:Gem::Requirement
114
128
  requirements:
115
129
  - - ">="
@@ -123,7 +137,7 @@ dependencies:
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
- name: packwerk
140
+ name: sorbet
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - ">="
@@ -137,7 +151,7 @@ dependencies:
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
- name: railties
154
+ name: tapioca
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
157
  - - ">="
@@ -168,6 +182,7 @@ files:
168
182
  - lib/code_ownership/private/codeowners_file.rb
169
183
  - lib/code_ownership/private/extension_loader.rb
170
184
  - lib/code_ownership/private/glob_cache.rb
185
+ - lib/code_ownership/private/owner_assigner.rb
171
186
  - lib/code_ownership/private/ownership_mappers/directory_ownership.rb
172
187
  - lib/code_ownership/private/ownership_mappers/file_annotations.rb
173
188
  - lib/code_ownership/private/ownership_mappers/js_package_ownership.rb
@@ -190,7 +205,7 @@ metadata:
190
205
  source_code_uri: https://github.com/rubyatscale/code_ownership
191
206
  changelog_uri: https://github.com/rubyatscale/code_ownership/releases
192
207
  allowed_push_host: https://rubygems.org
193
- post_install_message:
208
+ post_install_message:
194
209
  rdoc_options: []
195
210
  require_paths:
196
211
  - lib
@@ -198,15 +213,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
198
213
  requirements:
199
214
  - - ">="
200
215
  - !ruby/object:Gem::Version
201
- version: '0'
216
+ version: '2.6'
202
217
  required_rubygems_version: !ruby/object:Gem::Requirement
203
218
  requirements:
204
219
  - - ">="
205
220
  - !ruby/object:Gem::Version
206
221
  version: '0'
207
222
  requirements: []
208
- rubygems_version: 3.1.6
209
- signing_key:
223
+ rubygems_version: 3.5.11
224
+ signing_key:
210
225
  specification_version: 4
211
226
  summary: A gem to help engineering teams declare ownership of code
212
227
  test_files: []