codeowners 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []