
- For a situation where `authored = 3`, `committed = 4`, the previous calculation was `3 - 4` which meant that `committed = -1` in the end. - This was incorrect, since a user can't have negative contributions! - Instead, only do the subtraction to get the deduplicated `committed` count if the number of authored commits is higher than the number of committed commits. This approach should achieve the desired "don't double count things that the user authored and committed, but do count things that another person authored that the user committed".
220 lines
6.8 KiB
Ruby
Executable File
220 lines
6.8 KiB
Ruby
Executable File
# typed: true
|
|
# frozen_string_literal: true
|
|
|
|
require "cli/parser"
|
|
require "csv"
|
|
|
|
module Homebrew
|
|
extend T::Sig
|
|
|
|
module_function
|
|
|
|
PRIMARY_REPOS = %w[brew core cask].freeze
|
|
SUPPORTED_REPOS = [
|
|
PRIMARY_REPOS,
|
|
OFFICIAL_CMD_TAPS.keys.map { |t| t.delete_prefix("homebrew/") },
|
|
OFFICIAL_CASK_TAPS.reject { |t| t == "cask" },
|
|
].flatten.freeze
|
|
|
|
sig { returns(CLI::Parser) }
|
|
def contributions_args
|
|
Homebrew::CLI::Parser.new do
|
|
usage_banner "`contributions` [--user=<email|username>] [<--repositories>`=`] [<--csv>]"
|
|
description <<~EOS
|
|
Contributions to Homebrew repos.
|
|
EOS
|
|
|
|
comma_array "--repositories",
|
|
description: "Specify a comma-separated (no spaces) list of repositories to search. " \
|
|
"Supported repositories: #{SUPPORTED_REPOS.map { |t| "`#{t}`" }.to_sentence}. " \
|
|
"Omitting this flag, or specifying `--repositories=all`, searches all repositories. " \
|
|
"Use `--repositories=primary` to search only the main repositories: brew,core,cask."
|
|
flag "--from=",
|
|
description: "Date (ISO-8601 format) to start searching contributions."
|
|
|
|
flag "--to=",
|
|
description: "Date (ISO-8601 format) to stop searching contributions."
|
|
|
|
flag "--user=",
|
|
description: "A GitHub username or email address of a specific person to find contribution data for."
|
|
|
|
switch "--csv",
|
|
description: "Print a CSV of contributions across repositories over the time period."
|
|
end
|
|
end
|
|
|
|
sig { void }
|
|
def contributions
|
|
args = contributions_args.parse
|
|
|
|
results = {}
|
|
grand_totals = {}
|
|
|
|
all_repos = args.repositories.nil? || args.repositories.include?("all")
|
|
repos = if all_repos
|
|
SUPPORTED_REPOS
|
|
elsif args.repositories.include?("primary")
|
|
PRIMARY_REPOS
|
|
else
|
|
args.repositories
|
|
end
|
|
|
|
if args.user
|
|
user = args.user
|
|
results[user] = scan_repositories(repos, user, args)
|
|
grand_totals[user] = total(results[user])
|
|
|
|
puts "#{user} contributed #{grand_totals[user].values.sum} times #{time_period(args)}."
|
|
puts generate_csv(T.must(user), results[user], grand_totals[user]) if args.csv?
|
|
return
|
|
end
|
|
|
|
maintainers = GitHub.members_by_team("Homebrew", "maintainers")
|
|
maintainers.each do |username, _|
|
|
# TODO: Using the GitHub username to scan the `git log` undercounts some
|
|
# contributions as people might not always have configured their Git
|
|
# committer details to match the ones on GitHub.
|
|
# TODO: Switch to using the GitHub APIs instead of `git log` if
|
|
# they ever support trailers.
|
|
results[username] = scan_repositories(repos, username, args)
|
|
grand_totals[username] = total(results[username])
|
|
|
|
puts "#{username} contributed #{grand_totals[username].values.sum} times #{time_period(args)}."
|
|
end
|
|
|
|
puts generate_maintainers_csv(grand_totals) if args.csv?
|
|
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(args: Homebrew::CLI::Args).returns(String) }
|
|
def time_period(args)
|
|
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
|
|
end
|
|
|
|
sig { params(totals: Hash).returns(String) }
|
|
def generate_maintainers_csv(totals)
|
|
CSV.generate do |csv|
|
|
csv << %w[user repo author committer coauthorships reviews total]
|
|
|
|
totals.sort_by { |_, v| -v.values.sum }.each do |user, total|
|
|
csv << grand_total_row(user, total)
|
|
end
|
|
end
|
|
end
|
|
|
|
sig { params(user: String, results: Hash, grand_total: Hash).returns(String) }
|
|
def generate_csv(user, results, grand_total)
|
|
CSV.generate do |csv|
|
|
csv << %w[user repo author committer coauthorships reviews total]
|
|
results.each do |repo, counts|
|
|
csv << [
|
|
user,
|
|
repo,
|
|
counts[:author],
|
|
counts[:committer],
|
|
counts[:coauthorships],
|
|
counts[:reviews],
|
|
counts.values.sum,
|
|
]
|
|
end
|
|
csv << grand_total_row(user, grand_total)
|
|
end
|
|
end
|
|
|
|
sig { params(user: String, grand_total: Hash).returns(Array) }
|
|
def grand_total_row(user, grand_total)
|
|
[
|
|
user,
|
|
"all",
|
|
grand_total[:author],
|
|
grand_total[:committer],
|
|
grand_total[:coauthorships],
|
|
grand_total[:reviews],
|
|
grand_total.values.sum,
|
|
]
|
|
end
|
|
|
|
def scan_repositories(repos, person, args)
|
|
data = {}
|
|
|
|
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)
|
|
tap = Tap.fetch("homebrew", repo)
|
|
unless repo_path.exist?
|
|
opoo "Repository #{repo} not yet tapped! Tapping it now..."
|
|
tap.install
|
|
end
|
|
|
|
repo_full_name = if repo == "brew"
|
|
"homebrew/brew"
|
|
else
|
|
tap.full_name
|
|
end
|
|
|
|
puts "Determining contributions for #{person} on #{repo_full_name}..." if args.verbose?
|
|
|
|
commits_authored = GitHub.repo_commit_count_for_user(repo_full_name, person, "author", args)
|
|
commits_committed = GitHub.repo_commit_count_for_user(repo_full_name, person, "committer", args)
|
|
# Only count committers where the author is not the same person. Avoid negative numbers or subtracting zero.
|
|
commits_committed = commits_authored - commits_committed if commits_authored > commits_committed
|
|
|
|
data[repo] = {
|
|
author: commits_authored,
|
|
committer: commits_committed,
|
|
coauthorships: git_log_trailers_cmd(T.must(repo_path), person, "Co-authored-by", args),
|
|
reviews: GitHub.count_issues(
|
|
"",
|
|
is: "pr",
|
|
repo: repo_full_name,
|
|
reviewed_by: person,
|
|
review: "approved",
|
|
args: args,
|
|
),
|
|
}
|
|
end
|
|
|
|
data
|
|
end
|
|
|
|
sig { params(results: Hash).returns(Hash) }
|
|
def total(results)
|
|
totals = { author: 0, committer: 0, coauthorships: 0, reviews: 0 }
|
|
|
|
results.each_value do |counts|
|
|
counts.each do |kind, count|
|
|
totals[kind] += count
|
|
end
|
|
end
|
|
|
|
totals
|
|
end
|
|
|
|
sig { params(repo_path: Pathname, person: String, trailer: String, args: Homebrew::CLI::Args).returns(Integer) }
|
|
def git_log_trailers_cmd(repo_path, person, trailer, args)
|
|
cmd = ["git", "-C", repo_path, "log", "--oneline"]
|
|
cmd << "--format='%(trailers:key=#{trailer}:)'"
|
|
cmd << "--before=#{args.to}" if args.to
|
|
cmd << "--after=#{args.from}" if args.from
|
|
|
|
Utils.safe_popen_read(*cmd).lines.count { |l| l.include?(person) }
|
|
end
|
|
end
|