codeowners 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 162d8c21d1f878a18c07817645f62191d2e4996d58533f819e6ebf102e7409fb
4
+ data.tar.gz: 4e06893a39070f1c34178f62e31e9afea773096bb121d98cdefea1620825bd2a
5
+ SHA512:
6
+ metadata.gz: 579bbfb6ce368e0407d6d17f644e352b9f9823fd9ed71850189917d272e1ffe6177dd10650f0604172f7a4318d51cd550530c738397f8d90becff8348fbb701a
7
+ data.tar.gz: 250bb637dfa66ef6cd4455c489e980e5475b1e67f5684cd70791dba1fe58b8d0e126d11031b3cd3b1f5eb1b6e0b5b5eb870f87e3bc6d7234d9713c1d299bbcd0
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+ .rubocop-*
@@ -0,0 +1 @@
1
+ inherit_from: https://raw.githubusercontent.com/jodosha/dotfiles/master/rubocop.yml
@@ -0,0 +1,8 @@
1
+ # Codeowners
2
+ Simple CLI to interact with GitHub CODEOWNERS
3
+
4
+ ## v0.0.1 - 2020-06-12
5
+ ### Added
6
+ - [Luca Guidi] Added `codeowners contributors`
7
+ - [Luca Guidi] Added `codeowners list`
8
+ - [Luca Guidi] Added `codeowners --help`
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in codeowners.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 12.0"
@@ -0,0 +1,64 @@
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
+ ### Help
38
+
39
+ For a complete set of options, please run:
40
+
41
+ ```shell
42
+ $ codeowners --help
43
+ $ codeowners COMMAND --help
44
+ ```
45
+
46
+ ## Development
47
+
48
+ 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.
49
+
50
+ To run `codeowners` executable during development:
51
+
52
+ ```shell
53
+ $ bundle exec exe/codeowners contributors path/to/file --base-directory=/path/to/git/repository/to/analyze
54
+ ```
55
+
56
+ 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).
57
+
58
+ ## Contributing
59
+
60
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jodosha/codeowners.
61
+
62
+ ## Copyright
63
+
64
+ &copy; 2020 - Luca Guidi - https://lucaguidi.com
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
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_development_dependency "rubocop"
31
+ spec.add_development_dependency "byebug"
32
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "codeowners/cli"
5
+
6
+ Dry::CLI.new(Codeowners::CLI::Commands).call
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codeowners
4
+ require "codeowners/version"
5
+ require "codeowners/result"
6
+ require "codeowners/list_owners"
7
+ require "codeowners/list_contributors"
8
+
9
+ class Error < StandardError
10
+ end
11
+
12
+ class SystemCallError < Error
13
+ end
14
+ end
@@ -0,0 +1,77 @@
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
+ desc "List code contributors for a file"
59
+
60
+ argument :file, required: true, desc: "File to check"
61
+
62
+ option :base_directory, type: :string, default: DEFAULT_BASE_DIRECTORY, desc: "Base directory"
63
+
64
+ def call(file:, base_directory:, **)
65
+ result = Codeowners::ListContributors.new(base_directory).call(file)
66
+ exit(1) unless result.successful?
67
+
68
+ out.puts result.to_s
69
+ end
70
+ end
71
+
72
+ register "version", Version, aliases: ["v", "-v", "--version"]
73
+ register "list", List
74
+ register "contributors", Contributors
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,46 @@
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)
13
+ require "codeowners/git/contributors"
14
+ output = git(["log", "--max-count=500", "--shortstat", %(--pretty=format:"author:%aN email:%ae"), "--no-color", "--", escape(file)])
15
+
16
+ Contributors.call(file, output)
17
+ end
18
+
19
+ private
20
+
21
+ def git(command_and_args)
22
+ execute(["git", "--git-dir=#{git_directory}", "--work-tree=#{work_tree}", "-c", "'color.ui=false'"] + command_and_args)
23
+ end
24
+
25
+ def work_tree
26
+ escape(@base_directory.to_s)
27
+ end
28
+
29
+ def git_directory
30
+ escape(@base_directory.join(".git").to_s)
31
+ end
32
+
33
+ def escape(string)
34
+ Shellwords.shellescape(string)
35
+ end
36
+
37
+ def execute(command, env: {}, error: ->(err) { raise Codeowners::SystemCallError.new(err) })
38
+ require "open3"
39
+
40
+ Open3.popen3(env, command.join(" ")) do |_, stdout, stderr, wait_thr|
41
+ error.call(stderr.read) unless wait_thr.value.success?
42
+ return stdout.read
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
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
+ end
22
+ end
23
+ end
@@ -0,0 +1,79 @@
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 -= commit
40
+ lines.shift
41
+ end
42
+ end
43
+
44
+ def self.parse(commit)
45
+ stats = commit.pop
46
+ stats = stats.split(", ")
47
+
48
+ _, insertions, deletions = *stats
49
+ insertions = insertions.to_i
50
+ deletions = deletions.to_i
51
+
52
+ authors = commit.map do |author|
53
+ {
54
+ "name" => author.scan(/author:(.*)email:/).flatten.first.chop,
55
+ "email" => author.scan(/email:(.*)/).flatten.first
56
+ }
57
+ end
58
+
59
+ [authors.uniq, insertions, deletions]
60
+ end
61
+
62
+ def initialize(data)
63
+ @contributors = data.map do |email, stats|
64
+ Contributor.new(email, *stats.values)
65
+ end
66
+ end
67
+
68
+ def each(&blk)
69
+ return enum_for(:each) unless block_given?
70
+
71
+ @contributors.each(&blk)
72
+ end
73
+
74
+ def empty?
75
+ @contributors.empty?
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,35 @@
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
+ end
21
+
22
+ def initialize(base_directory, git: Git.new(base_directory))
23
+ @git = git
24
+ end
25
+
26
+ def call(file)
27
+ contributors = @git.contributors(file)
28
+ return Result.new if contributors.empty?
29
+
30
+ contributors = contributors.each.lazy.sort_by { |c| -c.insertions }
31
+
32
+ Result.new(file, contributors)
33
+ end
34
+ end
35
+ 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,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codeowners
4
+ class Result
5
+ def initialize(pattern = nil, owners = [])
6
+ @pattern = pattern
7
+ @owners = owners
8
+ end
9
+
10
+ def successful?
11
+ !@pattern.nil?
12
+ end
13
+
14
+ def to_s
15
+ "#{@pattern}\n\n#{@owners.join('\n')}"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codeowners
4
+ VERSION = "0.0.1"
5
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: codeowners
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Luca Guidi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-06-12 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: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
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
+ description: Check GitHub Codeowners and guess which team should be assigned to a
56
+ file
57
+ email:
58
+ - me@lucaguidi.com
59
+ executables:
60
+ - codeowners
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - ".rubocop.yml"
66
+ - CHANGELOG.md
67
+ - Gemfile
68
+ - README.md
69
+ - Rakefile
70
+ - bin/console
71
+ - bin/setup
72
+ - codeowners.gemspec
73
+ - exe/codeowners
74
+ - lib/codeowners.rb
75
+ - lib/codeowners/cli.rb
76
+ - lib/codeowners/git.rb
77
+ - lib/codeowners/git/contributor.rb
78
+ - lib/codeowners/git/contributors.rb
79
+ - lib/codeowners/list_contributors.rb
80
+ - lib/codeowners/list_owners.rb
81
+ - lib/codeowners/result.rb
82
+ - lib/codeowners/version.rb
83
+ homepage: https://lucaguidi.com
84
+ licenses: []
85
+ metadata:
86
+ allowed_push_host: https://rubygems.org
87
+ homepage_uri: https://lucaguidi.com
88
+ source_code_uri: https://github.com/jodosha/codeowners
89
+ changelog_uri: https://github.com/jodosha/codeowners/blob/master/CHANGELOG.md
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 2.5.0
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.1.3
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: GitHub Codeowners check and guess
109
+ test_files: []