cleo_codeowners 0.2.0 → 0.4.0

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: c63f568dd1ca8df56852f25c7df28cb462e7684f3681d7b2921905344947a4f0
4
- data.tar.gz: 4e6d293428da814ff5ab29f73a9095a1b4cb3bd8d37a856229b2829012be8462
3
+ metadata.gz: 95dabb73fd9d44328c2c7baed5ffbf4f91996ef2bad026d2f79175b98ba252f9
4
+ data.tar.gz: 4a65dca6d6efef99d01587612451b72293283a30aa0a99f3ada7f192b05added
5
5
  SHA512:
6
- metadata.gz: 6dff31a87fd703933fdcdb92da9428c4908f735b9d3d48a3b5bf06c86fd7ab0e73ebc2728bb009e9e784975a889056393620cb964fb087c78a08ffcd0066f157
7
- data.tar.gz: e1c87696a2ec2984d09152fac3cf5d54897ff59b8cee684a16e19f9e137e21f6f4a989da49add28a8d4381328c138b61ca6844a443b8bbc3e91dd6f814870e05
6
+ metadata.gz: 55b1e75f9bfbf6affb08442adb631c5fb96ab8a7ac3a1a31a433fc44235f5875964a98d4d77038965cd7c20db7cf022c5a8989db3c7a1acdfe80f32a864c792a
7
+ data.tar.gz: c69046a48d695beb1129a005fff5b87163b0cd4935fbb625017a123b1bc2f1ed291ee02d833cf6a2ee4ef0448d14a388a2f5324547cb7fb5fe79bfbe680b3f98
@@ -3,5 +3,5 @@
3
3
  ##
4
4
  # Root module for this gem
5
5
  module CleoCodeowners
6
- VERSION = '0.2.0'
6
+ VERSION = '0.4.0'
7
7
  end
@@ -1,6 +1,8 @@
1
1
  # typed: false
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'pathname'
5
+
4
6
  module Codeowners
5
7
  class Generator
6
8
  DEFAULT_OUTPUT_PATH = '.github/CODEOWNERS'
@@ -105,32 +107,40 @@ module Codeowners
105
107
  calculated_owners.push({ path:, owner: })
106
108
  end
107
109
 
110
+ # GitHub applies the *last* matching CODEOWNERS rule, so rules are listed
111
+ # least-specific first: by number of path segments (`/app/` is broader than
112
+ # `/app/models/user.rb`), then by path so a wildcard such as `/foo/*` sorts
113
+ # ahead of the `/foo/bar/` it would otherwise shadow.
108
114
  def output_in_machine_format
109
- file_rules, folder_rules = calculated_owners.partition { |owner| owner[:path].include?('.') }
110
- group_by_path_segments_and_output(folder_rules)
111
- group_by_path_segments_and_output(file_rules)
115
+ merge_owners_by_path(calculated_owners)
116
+ .sort_by { |path, _owners| [path_depth(path), path] }
117
+ .each { |path, rule_owners| output_file << "#{path} #{rule_owners.join(' ')}\n" }
112
118
  end
113
119
 
114
- def group_by_path_segments_and_output(rules)
115
- grouped_by_path_segments_number = rules
116
- .group_by { |owner| sanitised_path(owner[:path]).split('/').reject(&:empty?).size }
117
- .each_value do |rule|
118
- rule.sort_by! do |owner|
119
- owner[:path]
120
- end
120
+ # Combines the owners of rules that share a path onto one line.
121
+ def merge_owners_by_path(rules)
122
+ rules_by_path = rules.group_by { |owner| sanitised_path(owner[:path]) }
123
+ reject_trailing_slash_conflicts(rules_by_path.keys)
124
+ rules_by_path.map do |path, owners_for_path|
125
+ [path, owners_for_path.map { |owner| owner[:owner] }.uniq.sort]
121
126
  end
122
- .sort
123
- .to_h
124
-
125
- grouped_by_path_segments_number
126
- .each_value
127
- .map { |files_and_owners| files_and_owners.group_by { |owner| owner[:path] } }
128
- .each do |files_and_owners|
129
- files_and_owners.each do |file, owners_list|
130
- rule_owners = owners_list.map { |owner| owner[:owner] }.uniq.sort
131
- output_file << "#{sanitised_path(file)} #{rule_owners.join(' ')}\n"
132
- end
133
- end
127
+ end
128
+
129
+ # A path written both with and without a trailing slash is rejected rather
130
+ # than guessed at: `/foo` and `/foo/` are effectively same rule, but on disk
131
+ # they differ (a directory needs its slash to match recursively, a file must
132
+ # not have one), so the definitions must pick one.
133
+ def reject_trailing_slash_conflicts(paths)
134
+ paths.group_by { |path| path.chomp('/') }.each_value do |variants|
135
+ next if variants.uniq.size == 1
136
+
137
+ raise "#{variants.min} is declared both with and without a trailing slash " \
138
+ "(#{variants.uniq.sort.join(', ')}); pick one so it is clear whether it is a file or a directory."
139
+ end
140
+ end
141
+
142
+ def path_depth(path)
143
+ Pathname.new(path).each_filename.count
134
144
  end
135
145
 
136
146
  def blurb
@@ -25,21 +25,24 @@ module Codeowners
25
25
  # Does this glob match the given filepath?
26
26
  # @param [String] filepath to match against
27
27
  # @note
28
+ # Use `FNM_PATHNAME` so a single `*` stays within one path segment.
29
+ # Use `FNM_DOTMATCH` so `*` still matches dotfiles/dot-dirs e.g.
30
+ # `.github`, as GitHub does.
28
31
  # Don't use `FNM_CASEFOLD`, GitHub uses a case sensitive file system.
29
32
  # Don't use `FNM_EXTGLOB`, GitHub CODEOWNERS files don't support it.
30
- # Don't use `FNM_PATHNAME`, we want to match `*` against '/'.
31
33
  #
32
34
  # @see https://docs.ruby-lang.org/en/3.3/File.html#method-c-fnmatch
33
35
  # @return [Boolean]
34
36
  def match?(filepath)
35
- fnmatch(pattern, filepath, File::FNM_DOTMATCH)
37
+ # Bitwise OR is how we combine flags.
38
+ fnmatch(pattern, filepath, File::FNM_PATHNAME | File::FNM_DOTMATCH)
36
39
  end
37
40
 
38
41
  private
39
42
 
40
43
  def normalize_string(string)
41
44
  string.to_s.delete_prefix('/') # remove leading slashes
42
- .gsub(%r{/\Z}, '/**') # replace trailing slash with a recursive wildcard
45
+ .gsub(%r{/\Z}, '/**/*') # replace trailing slash with a recursive wildcard
43
46
  end
44
47
  end
45
48
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cleo_codeowners
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - "@agentAngelope"
@@ -97,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
97
  - !ruby/object:Gem::Version
98
98
  version: '0'
99
99
  requirements: []
100
- rubygems_version: 4.0.13
100
+ rubygems_version: 4.0.15
101
101
  specification_version: 4
102
102
  summary: Cleo CODEOWNERS tooling
103
103
  test_files: []