mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00

- The usage of this in `brew contributions` wasn't correct for a user with 5 authored commits to homebrew/cask that had been committed by other people, the numbers would turn out as 5 authored, 5 committed. - I decided to do this properly by getting the SHAs for author and committer and determine the differences between the two arrays. This also accounts for when authored commits are 0, or committed commits, or both. - Add tests, because I don't want to fix this a third time!
215 lines
6.4 KiB
Ruby
Executable File
215 lines
6.4 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?
|
|
|
|
data[repo] = {
|
|
author: GitHub.count_repo_commits(repo_full_name, person, "author", args),
|
|
committer: GitHub.count_repo_commits(repo_full_name, person, "committer", args),
|
|
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
|