codeowners 0.0.3
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 +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +8 -0
- data/README.md +75 -0
- data/Rakefile +4 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/codeowners.gemspec +33 -0
- data/exe/codeowners +6 -0
- data/lib/codeowners.rb +18 -0
- data/lib/codeowners/cli.rb +198 -0
- data/lib/codeowners/git.rb +54 -0
- data/lib/codeowners/git/contributor.rb +27 -0
- data/lib/codeowners/git/contributors.rb +89 -0
- 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 +43 -0
- data/lib/codeowners/list_owners.rb +25 -0
- data/lib/codeowners/result.rb +24 -0
- data/lib/codeowners/storage.rb +79 -0
- data/lib/codeowners/storage/collection.rb +37 -0
- data/lib/codeowners/storage/data.rb +30 -0
- data/lib/codeowners/version.rb +5 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 790787fbbbface03dca93e7ee9fb70818d736b55b5b46feba832bb5d1d1cacde
|
4
|
+
data.tar.gz: 5cf3d0d5b9612527ac8e3d5416442dcb0442e1d69180b85df1b25752871e9d9e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9728df04569e092a74db19feec631cb0f4031da24ab1bf7e127b6a728206c21026fc7a2988ff0c81f66b894a895350521a9f956236859e682eaa8be2f79194a3
|
7
|
+
data.tar.gz: 594590f13fe02ea9c8e7f5831e2d716a48779e7309f063caeb194c884a42d69037fed8ca64b6703ad88db7765f25258fa58d9d45e12b3eabb00875a016b1fb3a
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
inherit_from: https://raw.githubusercontent.com/jodosha/dotfiles/master/rubocop.yml
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# Codeowners
|
2
|
+
Simple CLI to interact with GitHub CODEOWNERS
|
3
|
+
|
4
|
+
## v0.0.3 - 2020-07-22
|
5
|
+
### Added
|
6
|
+
- [Luca Guidi] Added `codeowners guess`
|
7
|
+
- [Luca Guidi] Added `codeowners import org`
|
8
|
+
|
9
|
+
## v0.0.2 - 2020-06-27
|
10
|
+
### Added
|
11
|
+
- [Luca Guidi] Added pattern support to `codeowners contributors`
|
12
|
+
|
13
|
+
## v0.0.1 - 2020-06-12
|
14
|
+
### Added
|
15
|
+
- [Luca Guidi] Added `codeowners contributors`
|
16
|
+
- [Luca Guidi] Added `codeowners list`
|
17
|
+
- [Luca Guidi] Added `codeowners --help`
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# Codeowners
|
2
|
+
|
3
|
+
Simple CLI to interact with GitHub CODEOWNERS.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install as:
|
8
|
+
|
9
|
+
```shell
|
10
|
+
$ gem install codeowners
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
### List
|
16
|
+
|
17
|
+
List code owners for a file, if any.
|
18
|
+
|
19
|
+
```shell
|
20
|
+
$ codeowners list path/to/file
|
21
|
+
@company/team-a @company/team-b
|
22
|
+
```
|
23
|
+
|
24
|
+
### Contributors
|
25
|
+
|
26
|
+
List code contributors for a file.
|
27
|
+
This is useful to guess who can be a candidate to own a file.
|
28
|
+
|
29
|
+
```shell
|
30
|
+
$ codeowners contributors path/to/file
|
31
|
+
path/to/file
|
32
|
+
|
33
|
+
Person One <person.one@company.com> / +106, -0
|
34
|
+
Person Two <person.two@company.com> / +12, -2
|
35
|
+
```
|
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
|
+
|
48
|
+
### Help
|
49
|
+
|
50
|
+
For a complete set of options, please run:
|
51
|
+
|
52
|
+
```shell
|
53
|
+
$ codeowners --help
|
54
|
+
$ codeowners COMMAND --help
|
55
|
+
```
|
56
|
+
|
57
|
+
## Development
|
58
|
+
|
59
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
60
|
+
|
61
|
+
To run `codeowners` executable during development:
|
62
|
+
|
63
|
+
```shell
|
64
|
+
$ bundle exec exe/codeowners contributors path/to/file --base-directory=/path/to/git/repository/to/analyze
|
65
|
+
```
|
66
|
+
|
67
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
68
|
+
|
69
|
+
## Contributing
|
70
|
+
|
71
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jodosha/codeowners.
|
72
|
+
|
73
|
+
## Copyright
|
74
|
+
|
75
|
+
© 2020 - Luca Guidi - https://lucaguidi.com
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "codeowners"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/codeowners.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/codeowners/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "codeowners"
|
7
|
+
spec.version = Codeowners::VERSION
|
8
|
+
spec.authors = ["Luca Guidi"]
|
9
|
+
spec.email = ["me@lucaguidi.com"]
|
10
|
+
|
11
|
+
spec.summary = "GitHub Codeowners check and guess"
|
12
|
+
spec.description = "Check GitHub Codeowners and guess which team should be assigned to a file"
|
13
|
+
spec.homepage = "https://lucaguidi.com"
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
15
|
+
|
16
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/jodosha/codeowners"
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/jodosha/codeowners/blob/master/CHANGELOG.md"
|
21
|
+
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_runtime_dependency "dry-cli", "~> 0.6"
|
30
|
+
spec.add_runtime_dependency "excon", "~> 0.75"
|
31
|
+
spec.add_development_dependency "rubocop"
|
32
|
+
spec.add_development_dependency "byebug"
|
33
|
+
end
|
data/exe/codeowners
ADDED
data/lib/codeowners.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
require "codeowners/version"
|
5
|
+
require "codeowners/result"
|
6
|
+
require "codeowners/storage"
|
7
|
+
require "codeowners/list_owners"
|
8
|
+
require "codeowners/list_contributors"
|
9
|
+
require "codeowners/guess"
|
10
|
+
require "codeowners/import/client"
|
11
|
+
require "codeowners/import/organization"
|
12
|
+
|
13
|
+
class Error < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
class SystemCallError < Error
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "codeowners"
|
4
|
+
require "dry/cli"
|
5
|
+
|
6
|
+
module Codeowners
|
7
|
+
module CLI
|
8
|
+
module Commands
|
9
|
+
extend Dry::CLI::Registry
|
10
|
+
|
11
|
+
class Command < Dry::CLI::Command
|
12
|
+
def initialize(out: $stdout)
|
13
|
+
@out = out
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :out
|
19
|
+
end
|
20
|
+
|
21
|
+
class Version < Command
|
22
|
+
desc "Print version"
|
23
|
+
|
24
|
+
def call(*)
|
25
|
+
out.puts "v#{Codeowners::VERSION}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class List < Command
|
30
|
+
DEFAULT_BASE_DIRECTORY = Dir.pwd.dup.freeze
|
31
|
+
private_constant :DEFAULT_BASE_DIRECTORY
|
32
|
+
|
33
|
+
DEFAULT_CODEOWNERS_PATH = ::File.join(".github", "CODEOWNERS").freeze
|
34
|
+
private_constant :DEFAULT_CODEOWNERS_PATH
|
35
|
+
|
36
|
+
desc "List code owners for a file, if any"
|
37
|
+
|
38
|
+
argument :file, required: true, desc: "File to check"
|
39
|
+
|
40
|
+
option :base_directory, type: :string, default: DEFAULT_BASE_DIRECTORY, desc: "Base directory"
|
41
|
+
option :codeowners, type: :string, default: DEFAULT_CODEOWNERS_PATH, desc: "Path to CODEOWNERS file"
|
42
|
+
|
43
|
+
def call(file:, base_directory:, codeowners:, **)
|
44
|
+
result = Codeowners::ListOwners.new(base_directory, codeowners).call(file)
|
45
|
+
exit(1) unless result.successful?
|
46
|
+
|
47
|
+
out.puts result.to_s
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Contributors < Command
|
52
|
+
DEFAULT_BASE_DIRECTORY = Dir.pwd.dup.freeze
|
53
|
+
private_constant :DEFAULT_BASE_DIRECTORY
|
54
|
+
|
55
|
+
DEFAULT_CODEOWNERS_PATH = ::File.join(".github", "CODEOWNERS").freeze
|
56
|
+
private_constant :DEFAULT_CODEOWNERS_PATH
|
57
|
+
|
58
|
+
FORMAT_MAPPING = { "string" => "to_s", "csv" => "to_csv" }.freeze
|
59
|
+
private_constant :FORMAT_MAPPING
|
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"
|
73
|
+
|
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
|
+
]
|
82
|
+
|
83
|
+
def call(file:, base_directory:, format:, debug:, **)
|
84
|
+
result = Codeowners::ListContributors.new(base_directory).call(file, debug)
|
85
|
+
exit(1) unless result.successful?
|
86
|
+
|
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
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
register "version", Version, aliases: ["v", "-v", "--version"]
|
189
|
+
register "list", List
|
190
|
+
register "contributors", Contributors
|
191
|
+
register "guess", Guess
|
192
|
+
|
193
|
+
register "import" do |prefix|
|
194
|
+
prefix.register "org", Import::Org
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "shellwords"
|
5
|
+
|
6
|
+
module Codeowners
|
7
|
+
class Git
|
8
|
+
def initialize(base_directory)
|
9
|
+
@base_directory = Pathname.new(::File.expand_path(base_directory))
|
10
|
+
end
|
11
|
+
|
12
|
+
def contributors(file, debug = false)
|
13
|
+
require "codeowners/git/contributors"
|
14
|
+
output = git(["log", "--numstat", %(--pretty=format:"author:%aN email:%ae"), "--no-color", "--", escape(file)])
|
15
|
+
print_debug(output, debug)
|
16
|
+
|
17
|
+
Contributors.call(file, output)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def git(command_and_args)
|
23
|
+
execute(["git", "--git-dir=#{git_directory}", "--work-tree=#{work_tree}", "-c", "'color.ui=false'"] + command_and_args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def work_tree
|
27
|
+
escape(@base_directory.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def git_directory
|
31
|
+
escape(@base_directory.join(".git").to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
def escape(string)
|
35
|
+
Shellwords.shellescape(string)
|
36
|
+
end
|
37
|
+
|
38
|
+
def execute(command, env: {}, error: ->(err) { raise Codeowners::SystemCallError.new(err) })
|
39
|
+
require "open3"
|
40
|
+
|
41
|
+
Open3.popen3(env, command.join(" ")) do |_, stdout, stderr, wait_thr|
|
42
|
+
error.call(stderr.read) unless wait_thr.value.success?
|
43
|
+
return stdout.read
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def print_debug(output, debug)
|
48
|
+
return unless debug
|
49
|
+
|
50
|
+
puts output
|
51
|
+
puts "\n" * 10
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
class Git
|
5
|
+
class Contributor
|
6
|
+
attr_reader :email, :name, :file, :insertions, :deletions
|
7
|
+
|
8
|
+
def initialize(email, name, file, insertions, deletions)
|
9
|
+
@email = email
|
10
|
+
@name = name
|
11
|
+
@file = file
|
12
|
+
@insertions = insertions
|
13
|
+
@deletions = deletions
|
14
|
+
|
15
|
+
freeze
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{name} <#{email}> / +#{insertions}, -#{deletions}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_csv
|
23
|
+
"#{name}, #{email}, #{insertions}, #{deletions}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
require "codeowners/git/contributor"
|
5
|
+
|
6
|
+
module Codeowners
|
7
|
+
class Git
|
8
|
+
class Contributors
|
9
|
+
# rubocop:disable Metrics/AbcSize
|
10
|
+
# rubocop:disable Metrics/MethodLength
|
11
|
+
def self.call(file, output)
|
12
|
+
lines = output.split($INPUT_RECORD_SEPARATOR)
|
13
|
+
|
14
|
+
result = {}
|
15
|
+
each_commit(lines) do |authors, insertions, deletions|
|
16
|
+
authors.each do |author|
|
17
|
+
author_email = author.fetch("email")
|
18
|
+
author_name = author.fetch("name")
|
19
|
+
|
20
|
+
result[author_email] ||= {}
|
21
|
+
result[author_email]["name"] = author_name
|
22
|
+
result[author_email]["file"] = file
|
23
|
+
result[author_email]["insertions"] ||= 0
|
24
|
+
result[author_email]["deletions"] ||= 0
|
25
|
+
result[author_email]["insertions"] += insertions
|
26
|
+
result[author_email]["deletions"] += deletions
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
new(result)
|
31
|
+
end
|
32
|
+
# rubocop:enable Metrics/MethodLength
|
33
|
+
# rubocop:enable Metrics/AbcSize
|
34
|
+
|
35
|
+
def self.each_commit(lines)
|
36
|
+
while lines.any?
|
37
|
+
commit = lines.take_while { |line| line != "" }
|
38
|
+
yield parse(commit.dup) unless commit.empty?
|
39
|
+
lines.shift(commit.size + 1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.parse(commit)
|
44
|
+
authors, stats = commit.partition { |line| line.match?(/author:/) }
|
45
|
+
|
46
|
+
[extract_authors(authors), *calculate_stats(stats)]
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.extract_authors(authors)
|
50
|
+
authors.map do |author|
|
51
|
+
{
|
52
|
+
"name" => scan(author, /author:(.*)email:/).chop,
|
53
|
+
"email" => scan(author, /email:(.*)/)
|
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)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.scan(string, pattern)
|
69
|
+
string.scan(pattern).flatten.first
|
70
|
+
end
|
71
|
+
|
72
|
+
def initialize(data)
|
73
|
+
@contributors = data.map do |email, stats|
|
74
|
+
Contributor.new(email, *stats.values)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def each(&blk)
|
79
|
+
return enum_for(:each) unless block_given?
|
80
|
+
|
81
|
+
@contributors.each(&blk)
|
82
|
+
end
|
83
|
+
|
84
|
+
def empty?
|
85
|
+
@contributors.empty?
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
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
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "codeowners/git"
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
class ListContributors
|
7
|
+
class Result < ::Codeowners::Result
|
8
|
+
def initialize(file = nil, contributors = [])
|
9
|
+
@file = file
|
10
|
+
@contributors = contributors
|
11
|
+
end
|
12
|
+
|
13
|
+
def successful?
|
14
|
+
!@file.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
[@file, "", *@contributors.map(&:to_s)].join("\n")
|
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
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(base_directory, git: Git.new(base_directory))
|
31
|
+
@git = git
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(file, debug = false)
|
35
|
+
contributors = @git.contributors(file, debug)
|
36
|
+
return Result.new if contributors.empty?
|
37
|
+
|
38
|
+
contributors = contributors.each.lazy.sort_by { |c| -c.insertions }
|
39
|
+
|
40
|
+
Result.new(file, contributors)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
class ListOwners
|
7
|
+
def initialize(base_directory, codeowners)
|
8
|
+
@base_directory = Pathname.new(::File.expand_path(base_directory))
|
9
|
+
@codeowners = @base_directory.join(codeowners)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(file)
|
13
|
+
::File.open(@codeowners, "r").each_line do |line|
|
14
|
+
line = line.chomp
|
15
|
+
next if line.empty? || line.match?(/[[:space:]]*#/)
|
16
|
+
|
17
|
+
pattern, *owners = line.split(/[[:space:]]+/)
|
18
|
+
|
19
|
+
return Result.new(pattern, owners) if File.fnmatch(pattern, file)
|
20
|
+
end
|
21
|
+
|
22
|
+
Result.new
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
class Result
|
5
|
+
attr_reader :owners
|
6
|
+
|
7
|
+
def initialize(pattern = nil, owners = [])
|
8
|
+
@pattern = pattern
|
9
|
+
@owners = owners
|
10
|
+
end
|
11
|
+
|
12
|
+
def successful?
|
13
|
+
!@pattern.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#{@pattern}\n\n#{@owners.join('\n')}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_a
|
21
|
+
@owners.dup.flatten
|
22
|
+
end
|
23
|
+
end
|
24
|
+
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
|
@@ -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
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: codeowners
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luca Guidi
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-cli
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.6'
|
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.75'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.75'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
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'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Check GitHub Codeowners and guess which team should be assigned to a
|
70
|
+
file
|
71
|
+
email:
|
72
|
+
- me@lucaguidi.com
|
73
|
+
executables:
|
74
|
+
- codeowners
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".gitignore"
|
79
|
+
- ".rubocop.yml"
|
80
|
+
- CHANGELOG.md
|
81
|
+
- Gemfile
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- bin/console
|
85
|
+
- bin/setup
|
86
|
+
- codeowners.gemspec
|
87
|
+
- exe/codeowners
|
88
|
+
- lib/codeowners.rb
|
89
|
+
- lib/codeowners/cli.rb
|
90
|
+
- lib/codeowners/git.rb
|
91
|
+
- lib/codeowners/git/contributor.rb
|
92
|
+
- lib/codeowners/git/contributors.rb
|
93
|
+
- lib/codeowners/guess.rb
|
94
|
+
- lib/codeowners/import/client.rb
|
95
|
+
- lib/codeowners/import/organization.rb
|
96
|
+
- lib/codeowners/list_contributors.rb
|
97
|
+
- lib/codeowners/list_owners.rb
|
98
|
+
- lib/codeowners/result.rb
|
99
|
+
- lib/codeowners/storage.rb
|
100
|
+
- lib/codeowners/storage/collection.rb
|
101
|
+
- lib/codeowners/storage/data.rb
|
102
|
+
- lib/codeowners/version.rb
|
103
|
+
homepage: https://lucaguidi.com
|
104
|
+
licenses: []
|
105
|
+
metadata:
|
106
|
+
allowed_push_host: https://rubygems.org
|
107
|
+
homepage_uri: https://lucaguidi.com
|
108
|
+
source_code_uri: https://github.com/jodosha/codeowners
|
109
|
+
changelog_uri: https://github.com/jodosha/codeowners/blob/master/CHANGELOG.md
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: 2.5.0
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubygems_version: 3.1.3
|
126
|
+
signing_key:
|
127
|
+
specification_version: 4
|
128
|
+
summary: GitHub Codeowners check and guess
|
129
|
+
test_files: []
|