codeowners 0.0.1 → 0.0.5
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 +4 -4
- data/.github/workflows/ci.yml +42 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +17 -0
- data/README.md +11 -0
- data/Rakefile +10 -1
- data/codeowners.gemspec +3 -1
- data/lib/codeowners/cli.rb +126 -5
- data/lib/codeowners/git/contributor.rb +4 -0
- data/lib/codeowners/git/contributors.rb +21 -11
- data/lib/codeowners/git.rb +10 -2
- data/lib/codeowners/guess.rb +77 -0
- data/lib/codeowners/import/client.rb +136 -0
- data/lib/codeowners/import/organization.rb +32 -0
- data/lib/codeowners/list_contributors.rb +10 -2
- data/lib/codeowners/list_owners.rb +25 -2
- data/lib/codeowners/result.rb +6 -0
- data/lib/codeowners/storage/collection.rb +37 -0
- data/lib/codeowners/storage/data.rb +30 -0
- data/lib/codeowners/storage.rb +79 -0
- data/lib/codeowners/version.rb +1 -1
- data/lib/codeowners.rb +4 -0
- metadata +44 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96fe4773b399d9f3dc27c5b9bf8689b36e93adcb7a3359275624d55dcb84de0a
|
4
|
+
data.tar.gz: e2f6cd844cb20db9f2369c5572bf04972641e64069762977906387861b5d2d1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4583ed271c4eae4803fea24e590309e42782ac8988661b4180799219fee8c72c9467d353f3858ce109e878c6a868015ef44e95d7520d33ded81434f77bd0aec
|
7
|
+
data.tar.gz: 50fd38fea4c0196e57871df6ce6f5132655a8e1abcf814c46a5e7dd87f18262e5f77818bfa9596e214edbfab8a45dedd37abc76a078c93e54b463ddd0b1c95a8
|
@@ -0,0 +1,42 @@
|
|
1
|
+
name: ci
|
2
|
+
|
3
|
+
"on":
|
4
|
+
push:
|
5
|
+
paths:
|
6
|
+
- ".github/workflows/ci.yml"
|
7
|
+
- "lib/**"
|
8
|
+
- "*.gemspec"
|
9
|
+
- "spec/**"
|
10
|
+
- "Rakefile"
|
11
|
+
- "Gemfile"
|
12
|
+
- ".rubocop.yml"
|
13
|
+
pull_request:
|
14
|
+
branches:
|
15
|
+
- master
|
16
|
+
create:
|
17
|
+
|
18
|
+
jobs:
|
19
|
+
tests:
|
20
|
+
runs-on: ubuntu-latest
|
21
|
+
strategy:
|
22
|
+
fail-fast: false
|
23
|
+
matrix:
|
24
|
+
ruby:
|
25
|
+
- "3.0"
|
26
|
+
- "2.7"
|
27
|
+
- "2.6"
|
28
|
+
steps:
|
29
|
+
- uses: actions/checkout@v1
|
30
|
+
- name: Install package dependencies
|
31
|
+
run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS"
|
32
|
+
- name: Set up Ruby
|
33
|
+
uses: ruby/setup-ruby@v1
|
34
|
+
with:
|
35
|
+
ruby-version: ${{matrix.ruby}}
|
36
|
+
- name: Install latest bundler
|
37
|
+
run: |
|
38
|
+
gem install bundler --no-document
|
39
|
+
- name: Bundle install
|
40
|
+
run: bundle install --jobs 4 --retry 3
|
41
|
+
- name: Run all tests
|
42
|
+
run: bundle exec rake
|
data/.gitignore
CHANGED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,23 @@
|
|
1
1
|
# Codeowners
|
2
2
|
Simple CLI to interact with GitHub CODEOWNERS
|
3
3
|
|
4
|
+
## v0.0.5 - 2021-12-18
|
5
|
+
### Fixed
|
6
|
+
- [Luca Guidi] Added missing `require` of `Codeowners::Result` from `lib/codeowners/list_owners.rb`
|
7
|
+
|
8
|
+
## v0.0.4 - 2021-05-21
|
9
|
+
### Fixed
|
10
|
+
- [Luca Guidi] Make `codeowners list` compatible with [CODEOWNERS spec](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-on-github/about-code-owners#codeowners-syntax)
|
11
|
+
|
12
|
+
## v0.0.3 - 2020-07-22
|
13
|
+
### Added
|
14
|
+
- [Luca Guidi] Added `codeowners guess`
|
15
|
+
- [Luca Guidi] Added `codeowners import org`
|
16
|
+
|
17
|
+
## v0.0.2 - 2020-06-27
|
18
|
+
### Added
|
19
|
+
- [Luca Guidi] Added pattern support to `codeowners contributors`
|
20
|
+
|
4
21
|
## v0.0.1 - 2020-06-12
|
5
22
|
### Added
|
6
23
|
- [Luca Guidi] Added `codeowners contributors`
|
data/README.md
CHANGED
@@ -34,6 +34,17 @@ Person One <person.one@company.com> / +106, -0
|
|
34
34
|
Person Two <person.two@company.com> / +12, -2
|
35
35
|
```
|
36
36
|
|
37
|
+
The command accepts also a pattern to match files in bulk.
|
38
|
+
|
39
|
+
```shell
|
40
|
+
$ codeowners contributors 'path/to/**/*.rb'
|
41
|
+
path/to/**/*.rb
|
42
|
+
|
43
|
+
Person One <person.one@company.com> / +243, -438
|
44
|
+
Person Three <person.three@company.com> / +104, -56
|
45
|
+
Person Two <person.two@company.com> / +12, -2
|
46
|
+
```
|
47
|
+
|
37
48
|
### Help
|
38
49
|
|
39
50
|
For a complete set of options, please run:
|
data/Rakefile
CHANGED
@@ -1,4 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "rake"
|
3
4
|
require "bundler/gem_tasks"
|
4
|
-
|
5
|
+
require "rspec/core/rake_task"
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
8
|
+
file_list = FileList["spec/**/*_spec.rb"]
|
9
|
+
|
10
|
+
task.pattern = file_list
|
11
|
+
end
|
12
|
+
|
13
|
+
task default: "spec"
|
data/codeowners.gemspec
CHANGED
@@ -26,7 +26,9 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
27
|
spec.require_paths = ["lib"]
|
28
28
|
|
29
|
-
spec.add_runtime_dependency "dry-cli", "~> 0.
|
29
|
+
spec.add_runtime_dependency "dry-cli", "~> 0.7"
|
30
|
+
spec.add_runtime_dependency "excon", "~> 0.72"
|
31
|
+
spec.add_development_dependency "rspec"
|
30
32
|
spec.add_development_dependency "rubocop"
|
31
33
|
spec.add_development_dependency "byebug"
|
32
34
|
end
|
data/lib/codeowners/cli.rb
CHANGED
@@ -55,23 +55,144 @@ module Codeowners
|
|
55
55
|
DEFAULT_CODEOWNERS_PATH = ::File.join(".github", "CODEOWNERS").freeze
|
56
56
|
private_constant :DEFAULT_CODEOWNERS_PATH
|
57
57
|
|
58
|
-
|
58
|
+
FORMAT_MAPPING = { "string" => "to_s", "csv" => "to_csv" }.freeze
|
59
|
+
private_constant :FORMAT_MAPPING
|
59
60
|
|
60
|
-
|
61
|
+
FORMAT_VALUES = FORMAT_MAPPING.keys.freeze
|
62
|
+
private_constant :FORMAT_VALUES
|
63
|
+
|
64
|
+
DEFAULT_FORMAT = FORMAT_VALUES.first
|
65
|
+
private_constant :DEFAULT_FORMAT
|
66
|
+
|
67
|
+
DEFAULT_DEBUG = false
|
68
|
+
private_constant :DEFAULT_DEBUG
|
69
|
+
|
70
|
+
desc "List code contributors for a file (or a pattern)"
|
71
|
+
|
72
|
+
argument :file, required: true, desc: "File (or pattern) to check"
|
61
73
|
|
62
74
|
option :base_directory, type: :string, default: DEFAULT_BASE_DIRECTORY, desc: "Base directory"
|
75
|
+
option :format, type: :string, default: DEFAULT_FORMAT, values: FORMAT_VALUES, desc: "Output format"
|
76
|
+
option :debug, type: :boolean, default: DEFAULT_DEBUG, desc: "Print debug information to stdout"
|
77
|
+
|
78
|
+
example [
|
79
|
+
"path/to/file.rb # file",
|
80
|
+
"'path/to/**/*.rb' # pattern"
|
81
|
+
]
|
63
82
|
|
64
|
-
def call(file:, base_directory:, **)
|
65
|
-
result = Codeowners::ListContributors.new(base_directory).call(file)
|
83
|
+
def call(file:, base_directory:, format:, debug:, **)
|
84
|
+
result = Codeowners::ListContributors.new(base_directory).call(file, debug)
|
66
85
|
exit(1) unless result.successful?
|
67
86
|
|
68
|
-
out.puts result
|
87
|
+
out.puts output(result, format)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def output(result, format)
|
93
|
+
method_name = FORMAT_MAPPING.fetch(format)
|
94
|
+
result.public_send(method_name.to_sym)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class Guess < Command
|
99
|
+
DEFAULT_BASE_DIRECTORY = Dir.pwd.dup.freeze
|
100
|
+
private_constant :DEFAULT_BASE_DIRECTORY
|
101
|
+
|
102
|
+
DEFAULT_CODEOWNERS_PATH = ::File.join(".github", "CODEOWNERS").freeze
|
103
|
+
private_constant :DEFAULT_CODEOWNERS_PATH
|
104
|
+
|
105
|
+
FORMAT_MAPPING = { "string" => "to_s", "csv" => "to_csv" }.freeze
|
106
|
+
private_constant :FORMAT_MAPPING
|
107
|
+
|
108
|
+
FORMAT_VALUES = FORMAT_MAPPING.keys.freeze
|
109
|
+
private_constant :FORMAT_VALUES
|
110
|
+
|
111
|
+
DEFAULT_FORMAT = FORMAT_VALUES.first
|
112
|
+
private_constant :DEFAULT_FORMAT
|
113
|
+
|
114
|
+
DEFAULT_STORAGE_PATH = ::File.join(Dir.pwd, "codeowners.json").freeze
|
115
|
+
private_constant :DEFAULT_STORAGE_PATH
|
116
|
+
|
117
|
+
DEFAULT_DEBUG = false
|
118
|
+
private_constant :DEFAULT_DEBUG
|
119
|
+
|
120
|
+
desc "Check CODEOWNERS for a file (or a pattern), if missing, it tries to guess, by looking at the git commit history"
|
121
|
+
|
122
|
+
argument :file, required: true, desc: "File (or pattern) to check"
|
123
|
+
|
124
|
+
option :base_directory, type: :string, default: DEFAULT_BASE_DIRECTORY, desc: "Base directory"
|
125
|
+
option :codeowners, type: :string, default: DEFAULT_CODEOWNERS_PATH, desc: "Path to CODEOWNERS file"
|
126
|
+
option :storage, type: :string, default: DEFAULT_STORAGE_PATH, desc: "Storage path (default: #{DEFAULT_STORAGE_PATH})"
|
127
|
+
|
128
|
+
option :format, type: :string, default: DEFAULT_FORMAT, values: FORMAT_VALUES, desc: "Output format"
|
129
|
+
option :debug, type: :boolean, default: DEFAULT_DEBUG, desc: "Print debug information to stdout"
|
130
|
+
|
131
|
+
example [
|
132
|
+
"path/to/file.rb # file",
|
133
|
+
"'path/to/**/*.rb' # pattern"
|
134
|
+
]
|
135
|
+
|
136
|
+
def call(file:, base_directory:, codeowners:, storage:, format:, debug:, **)
|
137
|
+
owners = Codeowners::ListOwners.new(base_directory, codeowners)
|
138
|
+
contributors = Codeowners::ListContributors.new(base_directory)
|
139
|
+
storage = Codeowners::Storage.new(storage)
|
140
|
+
|
141
|
+
result = Codeowners::Guess.new(owners, contributors, storage, base_directory, out).call(file, debug)
|
142
|
+
# exit(1) unless result.successful?
|
143
|
+
|
144
|
+
out.puts output(result, format)
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def output(result, format)
|
150
|
+
# method_name = FORMAT_MAPPING.fetch(format)
|
151
|
+
return unless format == "csv"
|
152
|
+
|
153
|
+
result.map do |file, data|
|
154
|
+
"#{file},#{data.fetch(:teams).join('/')},#{data.fetch(:codeowners)}"
|
155
|
+
end.join("\n")
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
module Import
|
160
|
+
class Org < Command
|
161
|
+
DEFAULT_STORAGE_PATH = ::File.join(Dir.pwd, "codeowners.json").freeze
|
162
|
+
private_constant :DEFAULT_STORAGE_PATH
|
163
|
+
|
164
|
+
DEFAULT_DEBUG = false
|
165
|
+
private_constant :DEFAULT_DEBUG
|
166
|
+
|
167
|
+
desc "Import teams and members for a GitHub organization"
|
168
|
+
|
169
|
+
argument :org, required: true, desc: "GitHub organization login"
|
170
|
+
argument :token, required: true, desc: "GitHub APIv3 token"
|
171
|
+
|
172
|
+
option :storage, type: :string, default: DEFAULT_STORAGE_PATH, desc: "Storage path (default: #{DEFAULT_STORAGE_PATH})"
|
173
|
+
option :debug, type: :boolean, default: DEFAULT_DEBUG, desc: "Print debug information to stdout"
|
174
|
+
|
175
|
+
example [
|
176
|
+
"hanami s3cr374p1t0k3n"
|
177
|
+
]
|
178
|
+
|
179
|
+
def call(org:, token:, storage:, debug:, **)
|
180
|
+
client = Codeowners::Import::Client.new(token, out)
|
181
|
+
storage = Codeowners::Storage.new(storage)
|
182
|
+
|
183
|
+
Codeowners::Import::Organization.new(client, storage).call(org, debug)
|
184
|
+
end
|
69
185
|
end
|
70
186
|
end
|
71
187
|
|
72
188
|
register "version", Version, aliases: ["v", "-v", "--version"]
|
73
189
|
register "list", List
|
74
190
|
register "contributors", Contributors
|
191
|
+
register "guess", Guess
|
192
|
+
|
193
|
+
register "import" do |prefix|
|
194
|
+
prefix.register "org", Import::Org
|
195
|
+
end
|
75
196
|
end
|
76
197
|
end
|
77
198
|
end
|
@@ -36,27 +36,37 @@ module Codeowners
|
|
36
36
|
while lines.any?
|
37
37
|
commit = lines.take_while { |line| line != "" }
|
38
38
|
yield parse(commit.dup) unless commit.empty?
|
39
|
-
lines
|
40
|
-
lines.shift
|
39
|
+
lines.shift(commit.size + 1)
|
41
40
|
end
|
42
41
|
end
|
43
42
|
|
44
43
|
def self.parse(commit)
|
45
|
-
stats = commit.
|
46
|
-
stats = stats.split(", ")
|
44
|
+
authors, stats = commit.partition { |line| line.match?(/author:/) }
|
47
45
|
|
48
|
-
|
49
|
-
|
50
|
-
deletions = deletions.to_i
|
46
|
+
[extract_authors(authors), *calculate_stats(stats)]
|
47
|
+
end
|
51
48
|
|
52
|
-
|
49
|
+
def self.extract_authors(authors)
|
50
|
+
authors.map do |author|
|
53
51
|
{
|
54
|
-
"name" =>
|
55
|
-
"email" =>
|
52
|
+
"name" => scan(author, /author:(.*)email:/).chop,
|
53
|
+
"email" => scan(author, /email:(.*)/)
|
56
54
|
}
|
55
|
+
end.uniq
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.calculate_stats(stats)
|
59
|
+
stats.each_with_object([0, 0]) do |stat, result|
|
60
|
+
stat = stat.split(/[[:space:]]+/)
|
61
|
+
|
62
|
+
insertions, deletions, = *stat
|
63
|
+
result[0] += Integer(insertions)
|
64
|
+
result[1] += Integer(deletions)
|
57
65
|
end
|
66
|
+
end
|
58
67
|
|
59
|
-
|
68
|
+
def self.scan(string, pattern)
|
69
|
+
string.scan(pattern).flatten.first
|
60
70
|
end
|
61
71
|
|
62
72
|
def initialize(data)
|
data/lib/codeowners/git.rb
CHANGED
@@ -9,9 +9,10 @@ module Codeowners
|
|
9
9
|
@base_directory = Pathname.new(::File.expand_path(base_directory))
|
10
10
|
end
|
11
11
|
|
12
|
-
def contributors(file)
|
12
|
+
def contributors(file, debug = false)
|
13
13
|
require "codeowners/git/contributors"
|
14
|
-
output = git(["log", "--
|
14
|
+
output = git(["log", "--numstat", %(--pretty=format:"author:%aN email:%ae"), "--no-color", "--", escape(file)])
|
15
|
+
print_debug(output, debug)
|
15
16
|
|
16
17
|
Contributors.call(file, output)
|
17
18
|
end
|
@@ -42,5 +43,12 @@ module Codeowners
|
|
42
43
|
return stdout.read
|
43
44
|
end
|
44
45
|
end
|
46
|
+
|
47
|
+
def print_debug(output, debug)
|
48
|
+
return unless debug
|
49
|
+
|
50
|
+
puts output
|
51
|
+
puts "\n" * 10
|
52
|
+
end
|
45
53
|
end
|
46
54
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
class Guess
|
5
|
+
def initialize(owners, contributors, storage, base_directory, out)
|
6
|
+
@owners = owners
|
7
|
+
@contributors = contributors
|
8
|
+
@storage = storage
|
9
|
+
@base_directory = ::File.expand_path(base_directory)
|
10
|
+
@out = out
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(file, debug)
|
14
|
+
result = {}
|
15
|
+
|
16
|
+
Dir.chdir(base_directory) do
|
17
|
+
Dir.glob(file).sort.each do |f|
|
18
|
+
*teams, codeowners = list_code_owners(f, debug)
|
19
|
+
*teams, codeowners = guess_code_owners(f, debug) unless codeowners
|
20
|
+
teams ||= []
|
21
|
+
|
22
|
+
result[f] = { teams: teams, codeowners: codeowners }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :owners
|
32
|
+
attr_reader :contributors
|
33
|
+
attr_reader :storage
|
34
|
+
attr_reader :base_directory
|
35
|
+
attr_reader :out
|
36
|
+
|
37
|
+
def list_code_owners(file, _debug)
|
38
|
+
result = owners.call(file)
|
39
|
+
|
40
|
+
if result.successful?
|
41
|
+
teams = result.to_a.find_all do |team|
|
42
|
+
storage.team_exist?(team) &&
|
43
|
+
!storage.blacklisted_team?(team)
|
44
|
+
end
|
45
|
+
|
46
|
+
return [result, true] if teams.any?
|
47
|
+
end
|
48
|
+
|
49
|
+
[nil, false]
|
50
|
+
end
|
51
|
+
|
52
|
+
def guess_code_owners(file, debug)
|
53
|
+
result = contributors.call(file, debug)
|
54
|
+
return [nil, false] unless result.successful?
|
55
|
+
|
56
|
+
contributors = result.to_a
|
57
|
+
result = contributors.each_with_object({}) do |contributor, memo|
|
58
|
+
teams = storage.teams_for(contributor)
|
59
|
+
teams.each do |team|
|
60
|
+
slug = team.fetch("slug")
|
61
|
+
|
62
|
+
memo[slug] ||= {}
|
63
|
+
memo[slug][:insertions] ||= 0
|
64
|
+
memo[slug][:deletions] ||= 0
|
65
|
+
memo[slug][:insertions] += contributor.insertions
|
66
|
+
memo[slug][:deletions] += contributor.deletions
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
team = result.sort do |a, b|
|
71
|
+
-a.last.fetch(:insertions) <=> -b.last.fetch(:insertions)
|
72
|
+
end&.first&.first
|
73
|
+
|
74
|
+
[team, false]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "excon"
|
5
|
+
|
6
|
+
module Codeowners
|
7
|
+
module Import
|
8
|
+
class Client
|
9
|
+
BASE_URL = "https://api.github.com"
|
10
|
+
private_constant :BASE_URL
|
11
|
+
|
12
|
+
USER_AGENT = "codeowners v#{Codeowners::VERSION}"
|
13
|
+
private_constant :USER_AGENT
|
14
|
+
|
15
|
+
def initialize(token, out, base_url = BASE_URL, user_agent = USER_AGENT, client = Excon, sleep_time: 3)
|
16
|
+
@base_url = base_url
|
17
|
+
@user_agent = user_agent
|
18
|
+
@token = token
|
19
|
+
@client = client
|
20
|
+
@out = out
|
21
|
+
@sleep_time = sleep_time
|
22
|
+
end
|
23
|
+
|
24
|
+
def org(login, debug = false)
|
25
|
+
result = get("/orgs/#{login}", debug: debug)
|
26
|
+
|
27
|
+
{
|
28
|
+
id: result.fetch("id"),
|
29
|
+
login: result.fetch("login")
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def org_members(org, debug = false)
|
34
|
+
result = get_paginated("/orgs/#{org.fetch(:login)}/members", debug: debug)
|
35
|
+
result.map do |user|
|
36
|
+
{
|
37
|
+
id: user.fetch("id"),
|
38
|
+
login: user.fetch("login")
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def teams(org, debug = false)
|
44
|
+
result = get_paginated("/orgs/#{org.fetch(:login)}/teams", debug: debug)
|
45
|
+
result.map do |team|
|
46
|
+
{
|
47
|
+
id: team.fetch("id"),
|
48
|
+
org_id: org.fetch(:id),
|
49
|
+
name: team.fetch("name"),
|
50
|
+
slug: team.fetch("slug")
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def team_members(org, teams, debug = false)
|
56
|
+
teams.each_with_object([]) do |team, memo|
|
57
|
+
result = get_paginated("/orgs/#{org.fetch(:login)}/teams/#{team.fetch(:slug)}/members", debug: debug)
|
58
|
+
result.each do |member|
|
59
|
+
team_id = team.fetch(:id)
|
60
|
+
user_id = member.fetch("id")
|
61
|
+
|
62
|
+
memo << {
|
63
|
+
id: [team_id, user_id],
|
64
|
+
team_id: team_id,
|
65
|
+
user_id: user_id
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
sleep_for_a_while
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def users(users, debug)
|
74
|
+
users.each do |user|
|
75
|
+
remote_user = get("/users/#{user.fetch(:login)}", debug: debug)
|
76
|
+
user.merge!(
|
77
|
+
name: remote_user.fetch("name"),
|
78
|
+
email: remote_user.fetch("email")
|
79
|
+
)
|
80
|
+
|
81
|
+
sleep_for_a_while
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
attr_reader :base_url
|
88
|
+
attr_reader :user_agent
|
89
|
+
attr_reader :token
|
90
|
+
attr_reader :client
|
91
|
+
attr_reader :out
|
92
|
+
attr_reader :sleep_time
|
93
|
+
|
94
|
+
def get(path, debug: false)
|
95
|
+
out.puts "requesting GET #{path}" if debug
|
96
|
+
|
97
|
+
response = client.get(base_url + path, query: query, headers: headers)
|
98
|
+
return {} unless response.status == 200
|
99
|
+
|
100
|
+
JSON.parse(response.body)
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_paginated(path, result = [], debug: false, page: 1)
|
104
|
+
out.puts "requesting GET #{path}, page: #{page}" if debug
|
105
|
+
|
106
|
+
response = client.get(base_url + path, query: query(page: page), headers: headers)
|
107
|
+
return [] unless response.status == 200
|
108
|
+
|
109
|
+
parsed = JSON.parse(response.body)
|
110
|
+
result.push(parsed)
|
111
|
+
|
112
|
+
if parsed.any?
|
113
|
+
sleep_for_a_while
|
114
|
+
get_paginated(path, result, debug: debug, page: page + 1)
|
115
|
+
else
|
116
|
+
result.flatten
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def query(options = {})
|
121
|
+
{ page: 1, per_page: 100 }.merge(options)
|
122
|
+
end
|
123
|
+
|
124
|
+
def headers
|
125
|
+
{
|
126
|
+
"Authorization" => "token #{token}",
|
127
|
+
"User-Agent" => user_agent
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
def sleep_for_a_while
|
132
|
+
sleep(sleep_time)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
module Import
|
5
|
+
class Organization
|
6
|
+
def initialize(client, storage)
|
7
|
+
@client = client
|
8
|
+
@storage = storage
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(org, debug)
|
12
|
+
org = client.org(org, debug)
|
13
|
+
users = client.org_members(org, debug)
|
14
|
+
users = client.users(users, debug)
|
15
|
+
teams = client.teams(org, debug)
|
16
|
+
memberships = client.team_members(org, teams, debug)
|
17
|
+
|
18
|
+
storage.transaction do |db|
|
19
|
+
db[:orgs].upsert(org)
|
20
|
+
db[:users].upsert(users)
|
21
|
+
db[:teams].upsert(teams)
|
22
|
+
db[:memberships].upsert(memberships)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :client
|
29
|
+
attr_reader :storage
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -17,14 +17,22 @@ module Codeowners
|
|
17
17
|
def to_s
|
18
18
|
[@file, "", *@contributors.map(&:to_s)].join("\n")
|
19
19
|
end
|
20
|
+
|
21
|
+
def to_a
|
22
|
+
@contributors.dup
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_csv
|
26
|
+
@contributors.map(&:to_csv).join("\n")
|
27
|
+
end
|
20
28
|
end
|
21
29
|
|
22
30
|
def initialize(base_directory, git: Git.new(base_directory))
|
23
31
|
@git = git
|
24
32
|
end
|
25
33
|
|
26
|
-
def call(file)
|
27
|
-
contributors = @git.contributors(file)
|
34
|
+
def call(file, debug = false)
|
35
|
+
contributors = @git.contributors(file, debug)
|
28
36
|
return Result.new if contributors.empty?
|
29
37
|
|
30
38
|
contributors = contributors.each.lazy.sort_by { |c| -c.insertions }
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "pathname"
|
4
|
+
require_relative "./result"
|
4
5
|
|
5
6
|
module Codeowners
|
6
7
|
class ListOwners
|
@@ -10,16 +11,38 @@ module Codeowners
|
|
10
11
|
end
|
11
12
|
|
12
13
|
def call(file)
|
14
|
+
result = Result.new
|
15
|
+
|
13
16
|
::File.open(@codeowners, "r").each_line do |line|
|
14
17
|
line = line.chomp
|
15
18
|
next if line.empty? || line.match?(/[[:space:]]*#/)
|
16
19
|
|
17
20
|
pattern, *owners = line.split(/[[:space:]]+/)
|
18
21
|
|
19
|
-
|
22
|
+
result = Result.new(pattern, owners) if match?(pattern, file)
|
20
23
|
end
|
21
24
|
|
22
|
-
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def match?(pattern, file)
|
31
|
+
pattern = normalize_pattern(pattern)
|
32
|
+
flags = match_flags_for(pattern)
|
33
|
+
|
34
|
+
File.fnmatch(pattern, file, flags)
|
35
|
+
end
|
36
|
+
|
37
|
+
def normalize_pattern(pattern)
|
38
|
+
pattern += "**" if pattern.end_with?(::File::SEPARATOR)
|
39
|
+
pattern
|
40
|
+
end
|
41
|
+
|
42
|
+
def match_flags_for(pattern)
|
43
|
+
return File::FNM_PATHNAME if pattern.end_with?(::File::SEPARATOR + "*")
|
44
|
+
|
45
|
+
0
|
23
46
|
end
|
24
47
|
end
|
25
48
|
end
|
data/lib/codeowners/result.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module Codeowners
|
4
4
|
class Result
|
5
|
+
attr_reader :owners
|
6
|
+
|
5
7
|
def initialize(pattern = nil, owners = [])
|
6
8
|
@pattern = pattern
|
7
9
|
@owners = owners
|
@@ -14,5 +16,9 @@ module Codeowners
|
|
14
16
|
def to_s
|
15
17
|
"#{@pattern}\n\n#{@owners.join('\n')}"
|
16
18
|
end
|
19
|
+
|
20
|
+
def to_a
|
21
|
+
@owners.dup.flatten
|
22
|
+
end
|
17
23
|
end
|
18
24
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
class Storage
|
5
|
+
class Collection
|
6
|
+
def initialize(collection)
|
7
|
+
@collection = collection.each_with_object({}) do |record, memo|
|
8
|
+
memo[record.fetch("id")] = record
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def find(&blk)
|
13
|
+
collection.values.find(&blk)
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_all(&blk)
|
17
|
+
collection.values.find_all(&blk)
|
18
|
+
end
|
19
|
+
|
20
|
+
def upsert(*records)
|
21
|
+
records = Array(records).flatten
|
22
|
+
|
23
|
+
records.each do |record|
|
24
|
+
collection[record.fetch(:id)] = record
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def dump
|
29
|
+
collection.values.dup
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :collection
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "codeowners/storage/collection"
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
class Storage
|
7
|
+
class Data
|
8
|
+
COLLECTIONS = %w[orgs users teams memberships].freeze
|
9
|
+
private_constant :COLLECTIONS
|
10
|
+
|
11
|
+
def initialize(data, collections: COLLECTIONS)
|
12
|
+
@data = collections.each_with_object({}) do |name, memo|
|
13
|
+
memo[name] = Collection.new(data.fetch(name, []))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](name)
|
18
|
+
data.fetch(name.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
def dump
|
22
|
+
data.transform_values(&:dump)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :data
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "tempfile"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module Codeowners
|
8
|
+
class Storage
|
9
|
+
require "codeowners/storage/data"
|
10
|
+
|
11
|
+
def initialize(path)
|
12
|
+
@path = path
|
13
|
+
@data = Data.new(load_data)
|
14
|
+
@mutex = Mutex.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def transaction
|
18
|
+
@mutex.synchronize do
|
19
|
+
tmp_data = data.dup
|
20
|
+
yield tmp_data
|
21
|
+
|
22
|
+
Tempfile.open("codeowners-storage") do |tmp|
|
23
|
+
tmp.binmode
|
24
|
+
tmp.write(JSON.generate(data.dump))
|
25
|
+
tmp.close
|
26
|
+
|
27
|
+
FileUtils.mv(tmp.path, path, force: true)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def team_exist?(team)
|
33
|
+
data[:teams].find do |record|
|
34
|
+
record.fetch("slug") == team
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def blacklisted_team?(team)
|
39
|
+
data[:teams].find do |record|
|
40
|
+
record.fetch("slug") == team &&
|
41
|
+
record.fetch("blacklisted")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def teams_for(user)
|
46
|
+
found = data[:users].find do |record|
|
47
|
+
record.fetch("email") == user.email ||
|
48
|
+
record.fetch("name") == user.name
|
49
|
+
end
|
50
|
+
|
51
|
+
return [] unless found
|
52
|
+
|
53
|
+
memberships = data[:memberships].find_all do |record|
|
54
|
+
record.fetch("user_id") == found.fetch("id")
|
55
|
+
end
|
56
|
+
memberships.map! { |hash| hash.fetch("team_id") }
|
57
|
+
|
58
|
+
return [] if memberships.empty?
|
59
|
+
|
60
|
+
teams = data[:teams].find_all do |record|
|
61
|
+
!record.fetch("blacklisted") &&
|
62
|
+
memberships.include?(record.fetch("id"))
|
63
|
+
end.flatten
|
64
|
+
|
65
|
+
teams
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_reader :path
|
71
|
+
attr_reader :data
|
72
|
+
|
73
|
+
def load_data
|
74
|
+
return {} unless File.exist?(path)
|
75
|
+
|
76
|
+
JSON.parse(File.read(path))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/codeowners/version.rb
CHANGED
data/lib/codeowners.rb
CHANGED
@@ -3,8 +3,12 @@
|
|
3
3
|
module Codeowners
|
4
4
|
require "codeowners/version"
|
5
5
|
require "codeowners/result"
|
6
|
+
require "codeowners/storage"
|
6
7
|
require "codeowners/list_owners"
|
7
8
|
require "codeowners/list_contributors"
|
9
|
+
require "codeowners/guess"
|
10
|
+
require "codeowners/import/client"
|
11
|
+
require "codeowners/import/organization"
|
8
12
|
|
9
13
|
class Error < StandardError
|
10
14
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: codeowners
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luca Guidi
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-cli
|
@@ -16,14 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.7'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
26
|
+
version: '0.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: excon
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.72'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.72'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: rubocop
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -61,7 +89,9 @@ executables:
|
|
61
89
|
extensions: []
|
62
90
|
extra_rdoc_files: []
|
63
91
|
files:
|
92
|
+
- ".github/workflows/ci.yml"
|
64
93
|
- ".gitignore"
|
94
|
+
- ".rspec"
|
65
95
|
- ".rubocop.yml"
|
66
96
|
- CHANGELOG.md
|
67
97
|
- Gemfile
|
@@ -76,9 +106,15 @@ files:
|
|
76
106
|
- lib/codeowners/git.rb
|
77
107
|
- lib/codeowners/git/contributor.rb
|
78
108
|
- lib/codeowners/git/contributors.rb
|
109
|
+
- lib/codeowners/guess.rb
|
110
|
+
- lib/codeowners/import/client.rb
|
111
|
+
- lib/codeowners/import/organization.rb
|
79
112
|
- lib/codeowners/list_contributors.rb
|
80
113
|
- lib/codeowners/list_owners.rb
|
81
114
|
- lib/codeowners/result.rb
|
115
|
+
- lib/codeowners/storage.rb
|
116
|
+
- lib/codeowners/storage/collection.rb
|
117
|
+
- lib/codeowners/storage/data.rb
|
82
118
|
- lib/codeowners/version.rb
|
83
119
|
homepage: https://lucaguidi.com
|
84
120
|
licenses: []
|
@@ -87,7 +123,7 @@ metadata:
|
|
87
123
|
homepage_uri: https://lucaguidi.com
|
88
124
|
source_code_uri: https://github.com/jodosha/codeowners
|
89
125
|
changelog_uri: https://github.com/jodosha/codeowners/blob/master/CHANGELOG.md
|
90
|
-
post_install_message:
|
126
|
+
post_install_message:
|
91
127
|
rdoc_options: []
|
92
128
|
require_paths:
|
93
129
|
- lib
|
@@ -102,8 +138,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
138
|
- !ruby/object:Gem::Version
|
103
139
|
version: '0'
|
104
140
|
requirements: []
|
105
|
-
rubygems_version: 3.
|
106
|
-
signing_key:
|
141
|
+
rubygems_version: 3.2.29
|
142
|
+
signing_key:
|
107
143
|
specification_version: 4
|
108
144
|
summary: GitHub Codeowners check and guess
|
109
145
|
test_files: []
|