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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 162d8c21d1f878a18c07817645f62191d2e4996d58533f819e6ebf102e7409fb
4
- data.tar.gz: 4e06893a39070f1c34178f62e31e9afea773096bb121d98cdefea1620825bd2a
3
+ metadata.gz: 96fe4773b399d9f3dc27c5b9bf8689b36e93adcb7a3359275624d55dcb84de0a
4
+ data.tar.gz: e2f6cd844cb20db9f2369c5572bf04972641e64069762977906387861b5d2d1c
5
5
  SHA512:
6
- metadata.gz: 579bbfb6ce368e0407d6d17f644e352b9f9823fd9ed71850189917d272e1ffe6177dd10650f0604172f7a4318d51cd550530c738397f8d90becff8348fbb701a
7
- data.tar.gz: 250bb637dfa66ef6cd4455c489e980e5475b1e67f5684cd70791dba1fe58b8d0e126d11031b3cd3b1f5eb1b6e0b5b5eb870f87e3bc6d7234d9713c1d299bbcd0
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
@@ -8,3 +8,4 @@
8
8
  /tmp/
9
9
  Gemfile.lock
10
10
  .rubocop-*
11
+ codeowners.json
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
- task default: :spec
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.6"
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
@@ -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
- desc "List code contributors for a file"
58
+ FORMAT_MAPPING = { "string" => "to_s", "csv" => "to_csv" }.freeze
59
+ private_constant :FORMAT_MAPPING
59
60
 
60
- argument :file, required: true, desc: "File to check"
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.to_s
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
@@ -18,6 +18,10 @@ module Codeowners
18
18
  def to_s
19
19
  "#{name} <#{email}> / +#{insertions}, -#{deletions}"
20
20
  end
21
+
22
+ def to_csv
23
+ "#{name}, #{email}, #{insertions}, #{deletions}"
24
+ end
21
25
  end
22
26
  end
23
27
  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 -= commit
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.pop
46
- stats = stats.split(", ")
44
+ authors, stats = commit.partition { |line| line.match?(/author:/) }
47
45
 
48
- _, insertions, deletions = *stats
49
- insertions = insertions.to_i
50
- deletions = deletions.to_i
46
+ [extract_authors(authors), *calculate_stats(stats)]
47
+ end
51
48
 
52
- authors = commit.map do |author|
49
+ def self.extract_authors(authors)
50
+ authors.map do |author|
53
51
  {
54
- "name" => author.scan(/author:(.*)email:/).flatten.first.chop,
55
- "email" => author.scan(/email:(.*)/).flatten.first
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
- [authors.uniq, insertions, deletions]
68
+ def self.scan(string, pattern)
69
+ string.scan(pattern).flatten.first
60
70
  end
61
71
 
62
72
  def initialize(data)
@@ -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", "--max-count=500", "--shortstat", %(--pretty=format:"author:%aN email:%ae"), "--no-color", "--", escape(file)])
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
- return Result.new(pattern, owners) if File.fnmatch(pattern, file)
22
+ result = Result.new(pattern, owners) if match?(pattern, file)
20
23
  end
21
24
 
22
- Result.new
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Codeowners
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.5"
5
5
  end
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.1
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: 2020-06-12 00:00:00.000000000 Z
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.6'
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.6'
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.1.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: []