dev-cmd/contributions: Stats for all maintainers

- With `brew contributions`, this will output a list of stats
  (across the specified time period, or all time) for people in the
  "maintainers" team on GitHub.
- Add a `--user` flag for getting stats for a specific user (either
  username, name or email address).
- This assumes that their Git committer details are the same as their name is
  set to on GitHub.
- Show an error message if trying to generate a CSV for the full maintainer
  list, since I haven't worked out how to best show all of that info yet (or
  even how best to show only the totals across everything for every user) in
  that format.
This commit is contained in:
Issy Long 2023-02-20 00:14:53 +00:00
parent 5272640cfc
commit 214110f665
No known key found for this signature in database
GPG Key ID: 8247C390DADC67D4
2 changed files with 59 additions and 33 deletions

View File

@ -338,6 +338,9 @@ module Homebrew
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def csv?; end def csv?; end
sig { returns(T.nilable(String)) }
def user; end
end end
end end
end end

View File

@ -19,11 +19,9 @@ module Homebrew
sig { returns(CLI::Parser) } sig { returns(CLI::Parser) }
def contributions_args def contributions_args
Homebrew::CLI::Parser.new do Homebrew::CLI::Parser.new do
usage_banner "`contributions` <email|username> [<--repositories>`=`] [<--csv>]" usage_banner "`contributions` [--user=<email|username>] [<--repositories>`=`] [<--csv>]"
description <<~EOS description <<~EOS
Contributions to Homebrew repos for a user. Contributions to Homebrew repos.
The first argument is a GitHub username (e.g. "BrewTestBot") or an email address (e.g. "brewtestbot@brew.sh").
EOS EOS
comma_array "--repositories", comma_array "--repositories",
@ -37,10 +35,11 @@ module Homebrew
flag "--to=", flag "--to=",
description: "Date (ISO-8601 format) to stop searching contributions." 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", switch "--csv",
description: "Print a CSV of a user's contributions across repositories over the time period." description: "Print a CSV of a user's contributions across repositories over the time period."
named_args number: 1
end end
end end
@ -59,33 +58,26 @@ module Homebrew
args.repositories args.repositories
end end
repos.each do |repo| return ofail "CSVs not yet supported for the full list of maintainers at once." if args.csv? && args.user.nil?
if SUPPORTED_REPOS.exclude?(repo)
return ofail "Unsupported repository: #{repo}. Try one of #{SUPPORTED_REPOS.join(", ")}." maintainers = GitHub.members_by_team("Homebrew", "maintainers")
maintainers.each do |username, _|
puts "Determining contributions for #{username}..." if args.verbose?
# 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)
puts "#{username} contributed #{total(results[username])} times #{time_period(args)}."
end end
repo_path = find_repo_path_for_repo(repo) return unless args.user
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" user = args.user
"homebrew/brew" results[user] = scan_repositories(repos, user, args)
else puts "#{user} contributed #{total(results[user])} times #{time_period(args)}."
tap.full_name puts generate_csv(T.must(user), results[user]) if args.csv?
end
results[repo] = {
commits: GitHub.repo_commit_count_for_user(repo_full_name, args.named.first),
coauthorships: git_log_trailers_cmd(T.must(repo_path), "Co-authored-by", args),
signoffs: git_log_trailers_cmd(T.must(repo_path), "Signed-off-by", args),
}
end
puts "The user #{args.named.first} has made #{total(results)} contributions #{time_period(args)}."
puts generate_csv(args.named.first, results) if args.csv?
end end
sig { params(repo: String).returns(Pathname) } sig { params(repo: String).returns(Pathname) }
@ -126,6 +118,37 @@ module Homebrew
end end
end 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
data[repo] = {
commits: GitHub.repo_commit_count_for_user(repo_full_name, person),
coauthorships: git_log_trailers_cmd(T.must(repo_path), "Co-authored-by", person, args),
signoffs: git_log_trailers_cmd(T.must(repo_path), "Signed-off-by", person, args),
}
end
data
end
sig { params(results: Hash).returns(Integer) } sig { params(results: Hash).returns(Integer) }
def total(results) def total(results)
results results
@ -134,13 +157,13 @@ module Homebrew
.sum(&:sum) # 956 .sum(&:sum) # 956
end end
sig { params(repo_path: Pathname, trailer: String, args: Homebrew::CLI::Args).returns(Integer) } sig { params(repo_path: Pathname, person: String, trailer: String, args: Homebrew::CLI::Args).returns(Integer) }
def git_log_trailers_cmd(repo_path, trailer, args) def git_log_trailers_cmd(repo_path, person, trailer, args)
cmd = ["git", "-C", repo_path, "log", "--oneline"] cmd = ["git", "-C", repo_path, "log", "--oneline"]
cmd << "--format='%(trailers:key=#{trailer}:)'" cmd << "--format='%(trailers:key=#{trailer}:)'"
cmd << "--before=#{args.to}" if args.to cmd << "--before=#{args.to}" if args.to
cmd << "--after=#{args.from}" if args.from cmd << "--after=#{args.from}" if args.from
Utils.safe_popen_read(*cmd).lines.count { |l| l.include?(args.named.first) } Utils.safe_popen_read(*cmd).lines.count { |l| l.include?(person) }
end end
end end