cleo_codeowners 0.3.0 → 0.5.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: 9ab9ff74ae21e83712d2331afa49e4a58fb361afb592489c608662d190adfaac
4
- data.tar.gz: af9b4ec93a30218aef50319329d3594ca97cff39c4b203282abf7ab763ed968b
3
+ metadata.gz: 3da6d91b323d359c5906252cef6cf83727342be78baca1dcf576f5d43ac01be7
4
+ data.tar.gz: d4092679171b3762a76ac338546cf31c02a0b158fb50eecb7d1c8d818ef9b239
5
5
  SHA512:
6
- metadata.gz: bcf8aebdba6312e71130c7ddcd9cf66f39c6e90c7b63d66fb57dd59d0410391938a676e895f9f82245543acfec10fa44acc4db30b49cdc794619c7bfa4cb0ee6
7
- data.tar.gz: 87892f76e750c5ebbbead2dabf13ac1727e6c6fc1104e5ea237b06f4c22e05fb0038f6bbfe66bb73b1ae1a22384c2a756e162927f6fd114f9d405cd678f13601
6
+ metadata.gz: 2c847488207f368367a95244a5cd025449961b1df3f8454658144898d552761463bf5d97c07018c1813ccb89e088a1360fa7d9c3614ac9368aa5aa3decedcf93
7
+ data.tar.gz: e2b9f252b9054559deb915e2f68ed6d526b4953438d5457ae6249cdd189daae688cc329ac3c37b8b52994efc81bffb06a889239f99b94e2981c0310847f556cf
data/README.md CHANGED
@@ -60,27 +60,35 @@ Create YAML files under `.cleo/codeowners/`.
60
60
  # .cleo/codeowners/features.yml
61
61
  features:
62
62
  session management:
63
+ session administration:
63
64
  session expiry:
64
65
  billing:
65
66
  ```
66
67
 
68
+ Child features inherit their parent's owners, or can override with their own.
69
+ An owner can be one or more teams.
70
+
67
71
  ```yaml
68
72
  # .cleo/codeowners/owners.yml
69
73
  owners:
74
+ session administration:
75
+ - operations
70
76
  session management: identity-platform
71
- billing: payments
77
+ billing:
78
+ - payments
79
+ - finance
72
80
  ```
73
81
 
74
- Child features inherit their parent owner unless they define one.
75
-
76
82
  ```yaml
77
83
  # .cleo/codeowners/files/session_management.yml
78
84
  files:
85
+ session administration:
86
+ - /app/controllers/admin/sessions/
87
+ session expiry:
88
+ - /app/services/session_expiry/
79
89
  session management:
80
90
  - /app/controllers/sessions/
81
91
  - /app/models/session.rb
82
- session expiry:
83
- - /app/services/session_expiry/
84
92
 
85
93
  # .cleo/codeowners/files/billing.yml
86
94
  files:
@@ -3,5 +3,5 @@
3
3
  ##
4
4
  # Root module for this gem
5
5
  module CleoCodeowners
6
- VERSION = '0.3.0'
6
+ VERSION = '0.5.0'
7
7
  end
@@ -1,6 +1,9 @@
1
1
  # typed: false
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'active_support/core_ext/array/conversions'
5
+ require 'pathname'
6
+
4
7
  module Codeowners
5
8
  class Generator
6
9
  DEFAULT_OUTPUT_PATH = '.github/CODEOWNERS'
@@ -57,15 +60,18 @@ module Codeowners
57
60
  @files ||= definitions_file.file_config
58
61
  end
59
62
 
60
- def calculate_owners_and_output_in_human_format(features:, current_owner: nil, depth: 0)
63
+ def calculate_owners_and_output_in_human_format(features:, current_owners: [], depth: 0)
61
64
  return unless present?(features)
62
65
 
63
66
  features.sort_by(&:first).each do |feature, children|
64
67
  output(feature.upcase, depth:)
65
- owner = owners.fetch(feature, current_owner)
68
+ feature_owners = Array(
69
+ owners.fetch(feature, current_owners)
70
+ ).reject { |owner| blank?(owner) }
71
+
66
72
  feature_files = files[feature]
67
73
 
68
- if blank?(owner)
74
+ if feature_owners.empty?
69
75
  raise "Please assign an owner for #{feature} in owners.yaml!"
70
76
  elsif blank?(feature_files) && blank?(children)
71
77
  raise "Please assign files or child features to #{feature}, or remove it!"
@@ -73,20 +79,22 @@ module Codeowners
73
79
  all_features << feature
74
80
  end
75
81
 
76
- if owner == 'unowned'
82
+ long_names = feature_owners.map { |owner| Owner.new(owner).long_name }.uniq
83
+
84
+ if feature_owners == ['unowned']
77
85
  output('Currently unowned', depth:)
78
86
  else
79
- output("Currently owned by @meetcleo/#{owner}", depth:)
87
+ output("Currently owned by #{long_names.to_sentence}", depth:)
80
88
  end
81
89
 
82
90
  if present?(feature_files)
83
91
  feature_files.sort.uniq.each do |file|
84
- record_calculated_owner(file, Owner.new(owner).long_name)
85
- output("#{file} #{Owner.new(owner).long_name}", depth:, comment: false)
92
+ long_names.each { |long_name| record_calculated_owner(file, long_name) }
93
+ output("#{file} #{long_names.join(' ')}", depth:, comment: false)
86
94
  end
87
95
  end
88
96
 
89
- calculate_owners_and_output_in_human_format(features: children, current_owner: owner, depth: depth + 1)
97
+ calculate_owners_and_output_in_human_format(features: children, current_owners: feature_owners, depth: depth + 1)
90
98
  end
91
99
  end
92
100
 
@@ -105,32 +113,40 @@ module Codeowners
105
113
  calculated_owners.push({ path:, owner: })
106
114
  end
107
115
 
116
+ # GitHub applies the *last* matching CODEOWNERS rule, so rules are listed
117
+ # least-specific first: by number of path segments (`/app/` is broader than
118
+ # `/app/models/user.rb`), then by path so a wildcard such as `/foo/*` sorts
119
+ # ahead of the `/foo/bar/` it would otherwise shadow.
108
120
  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)
121
+ merge_owners_by_path(calculated_owners)
122
+ .sort_by { |path, _owners| [path_depth(path), path] }
123
+ .each { |path, rule_owners| output_file << "#{path} #{rule_owners.join(' ')}\n" }
112
124
  end
113
125
 
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
126
+ # Combines the owners of rules that share a path onto one line.
127
+ def merge_owners_by_path(rules)
128
+ rules_by_path = rules.group_by { |owner| sanitised_path(owner[:path]) }
129
+ reject_trailing_slash_conflicts(rules_by_path.keys)
130
+ rules_by_path.map do |path, owners_for_path|
131
+ [path, owners_for_path.map { |owner| owner[:owner] }.uniq.sort]
121
132
  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
133
+ end
134
+
135
+ # A path written both with and without a trailing slash is rejected rather
136
+ # than guessed at: `/foo` and `/foo/` are effectively same rule, but on disk
137
+ # they differ (a directory needs its slash to match recursively, a file must
138
+ # not have one), so the definitions must pick one.
139
+ def reject_trailing_slash_conflicts(paths)
140
+ paths.group_by { |path| path.chomp('/') }.each_value do |variants|
141
+ next if variants.uniq.size == 1
142
+
143
+ raise "#{variants.min} is declared both with and without a trailing slash " \
144
+ "(#{variants.uniq.sort.join(', ')}); pick one so it is clear whether it is a file or a directory."
145
+ end
146
+ end
147
+
148
+ def path_depth(path)
149
+ Pathname.new(path).each_filename.count
134
150
  end
135
151
 
136
152
  def blurb
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - "@agentAngelope"