brew/Library/Homebrew/dev-cmd/contributions.rb
Issy Long 0c7825accd
dev-cmd/contributions: Make the first arg either a name or email
- This is easier than mapping GitHub usernames to email addresses, when
  folks don't have email addresses always public on their GitHub
  profiles. Also, the users of this command (PLC members, other
  interested parties) don't have to remember folks' email addresses.
- It also gives better data for people who've changed their name over
  the years, and who commit with multiple email addresses (personal and
  work).

```
❯ brew contributions "Issy Long" all
Person Issy Long directly authored 687 commits and co-authored 33 commits across all Homebrew repos in all time.

❯ brew contributions "Rylan Polster" all
Person Rylan Polster directly authored 1747 commits and co-authored 133 commits across all Homebrew repos in all time.

❯ brew contributions me@issyl0.co.uk all
Person me@issyl0.co.uk directly authored 711 commits and co-authored 25 commits across all Homebrew repos in all time.

❯ brew contributions "Mike McQuaid" all
Person Mike McQuaid directly authored 26879 commits and co-authored 204 commits across all Homebrew repos in all time.
```
2022-08-03 16:53:57 +01:00

111 lines
3.3 KiB
Ruby
Executable File

# typed: true
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
SUPPORTED_REPOS = [
%w[brew core cask],
OFFICIAL_CMD_TAPS.keys.map { |t| t.delete_prefix("homebrew/") },
OFFICIAL_CASK_TAPS,
].flatten.freeze
sig { returns(CLI::Parser) }
def contributions_args
Homebrew::CLI::Parser.new do
usage_banner "`contributions` <email|name> <repo1,repo2|all>"
description <<~EOS
Contributions to Homebrew repos for a user.
The first argument is a name (e.g. "BrewTestBot") or an email address (e.g. "brewtestbot@brew.sh").
The second argument is a comma-separated list of repos to search.
Specify <all> to search all repositories.
Supported repositories: #{SUPPORTED_REPOS.join(", ")}.
EOS
flag "--from=",
description: "Date (ISO-8601 format) to start searching contributions."
flag "--to=",
description: "Date (ISO-8601 format) to stop searching contributions."
named_args [:email, :name, :repositories], min: 2, max: 2
end
end
sig { returns(NilClass) }
def contributions
args = contributions_args.parse
commits = 0
coauthorships = 0
all_repos = args.named.last == "all"
repos = all_repos ? SUPPORTED_REPOS : args.named.last.split(",")
repos.each do |repo|
if SUPPORTED_REPOS.exclude?(repo)
return ofail "Unsupported repository: #{repo}. Try one of #{SUPPORTED_REPOS.join(", ")}."
end
repo_path = find_repo_path_for_repo(repo)
unless repo_path.exist?
next if repo == "versions" # This tap is deprecated, tapping it will error.
opoo "Repository #{repo} not yet tapped! Tapping it now..."
Tap.fetch("homebrew", repo).install
end
commits += git_log_author_cmd(T.must(repo_path), args)
coauthorships += git_log_coauthor_cmd(T.must(repo_path), args)
end
sentence = "Person #{args.named.first} directly authored #{commits} commits " \
"and co-authored #{coauthorships} commits " \
"across #{all_repos ? "all Homebrew repos" : repos.to_sentence}"
sentence += if args.from && args.to
" between #{args.from} and #{args.to}"
elsif args.from
" after #{args.from}"
elsif args.to
" before #{args.to}"
else
" in all time"
end
sentence += "."
puts sentence
end
sig { params(repo: String).returns(Pathname) }
def find_repo_path_for_repo(repo)
return HOMEBREW_REPOSITORY if repo == "brew"
Tap.fetch("homebrew", repo).path
end
sig { params(repo_path: Pathname, args: Homebrew::CLI::Args).returns(Integer) }
def git_log_author_cmd(repo_path, args)
cmd = ["git", "-C", repo_path, "log", "--oneline", "--author=#{args.named.first}"]
cmd << "--before=#{args.to}" if args.to
cmd << "--after=#{args.from}" if args.from
Utils.safe_popen_read(*cmd).lines.count
end
sig { params(repo_path: Pathname, args: Homebrew::CLI::Args).returns(Integer) }
def git_log_coauthor_cmd(repo_path, args)
cmd = ["git", "-C", repo_path, "log", "--oneline"]
cmd << "--format='%(trailers:key=Co-authored-by:)'"
cmd << "--before=#{args.to}" if args.to
cmd << "--after=#{args.from}" if args.from
Utils.safe_popen_read(*cmd).lines.count { |l| l.include?(args.named.first) }
end
end