dev-cmd/contributions: Use GitHub APIs for commit author info

- Using `git log` was brittle with name changes and email address changes for
  contributors over the years unless we made a Git `mailmap` file which brings
  with it its own updatedness overhead.
- Let's use the GitHub commits API (importantly _not_ the search API) so that
  we can give it a username and it will return contributions associated with
  every email address on that user's account:
  https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits--parameters.
- This is quite significantly slower, but it's worth it for correctness
  especially when we get to all maintainers' contributions (in a separate PR).
- The commits API does not (yet?) support trailers or commit "committer"s, just
  authors.
This commit is contained in:
Issy Long 2023-02-20 19:18:09 +00:00
parent 6b7ecd18e9
commit d3827b12f2
No known key found for this signature in database
GPG Key ID: 8247C390DADC67D4
4 changed files with 19 additions and 17 deletions

View File

@ -19,11 +19,11 @@ module Homebrew
sig { returns(CLI::Parser) }
def contributions_args
Homebrew::CLI::Parser.new do
usage_banner "`contributions` <email|name> [<--repositories>`=`] [<--csv>]"
usage_banner "`contributions` <email|username> [<--repositories>`=`] [<--csv>]"
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 first argument is a GitHub username (e.g. "BrewTestBot") or an email address (e.g. "brewtestbot@brew.sh").
EOS
comma_array "--repositories",
@ -64,14 +64,15 @@ module Homebrew
return ofail "Unsupported repository: #{repo}. Try one of #{SUPPORTED_REPOS.join(", ")}."
end
tap = Tap.fetch("Homebrew", repo)
repo_path = find_repo_path_for_repo(repo)
unless repo_path.exist?
opoo "Repository #{repo} not yet tapped! Tapping it now..."
Tap.fetch("homebrew", repo).install
tap.install
end
results[repo] = {
commits: git_log_author_cmd(T.must(repo_path), args),
commits: GitHub.repo_commit_count_for_user(tap.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),
}
@ -127,15 +128,6 @@ module Homebrew
.sum(&:sum) # 956
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, trailer: String, args: Homebrew::CLI::Args).returns(Integer) }
def git_log_trailers_cmd(repo_path, trailer, args)
cmd = ["git", "-C", repo_path, "log", "--oneline"]

View File

@ -86,7 +86,7 @@ class Tap
# e.g. `user/repo`
attr_reader :name
# The full name of this {Tap}, including the `homebrew-` prefix.
# The full name of this {Tap}, including the `homebrew-` prefix unless repo == "brew".
# It combines {#user} and 'homebrew-'-prefixed {#repo} with a slash.
# e.g. `user/homebrew-repo`
attr_reader :full_name
@ -100,7 +100,7 @@ class Tap
@user = user
@repo = repo
@name = "#{@user}/#{@repo}".downcase
@full_name = "#{@user}/homebrew-#{@repo}"
@full_name = (@repo == "brew") ? "#{user}/#{repo}" : "#{@user}/homebrew-#{@repo}"
@path = TAP_DIRECTORY/@full_name.downcase
@path.extend(GitRepositoryExtension)
@alias_table = nil

View File

@ -699,4 +699,14 @@ module GitHub # rubocop:disable Metrics/ModuleLength
output[/^Status: (200)/, 1] != "200"
end
def repo_commit_count_for_user(nwo, user)
return if Homebrew::EnvConfig.no_github_api?
commits = 0
API.paginate_rest("#{API_URL}/repos/#{nwo}/commits", query: "&author=#{user}") do |result|
commits += result.length
end
commits
end
end

View File

@ -253,9 +253,9 @@ module GitHub
end
end
def paginate_rest(url, per_page: 100)
def paginate_rest(url, query: nil, per_page: 100)
(1..API_MAX_PAGES).each do |page|
result = API.open_rest("#{url}?per_page=#{per_page}&page=#{page}")
result = API.open_rest("#{url}?per_page=#{per_page}&page=#{page}#{query}")
yield(result, page)
end
end