Skip to content

Commit 16493d8

Browse files
committed
feat: add Co-authored-by parsing for squash merges
- Add --use-commits flag to enumerate commits instead of using cached /contributors endpoint - Parse Co-authored-by trailers from commit messages to handle squash merges - Resolve email addresses to GitHub logins with cache + search API fallback - Add KNOWN_EMAIL_TO_LOGIN for manual email-to-login mappings - Add IGNORED_COAUTHOR_EMAILS to filter non-standard bots - Add LOGIN_ALIASES to merge known duplicate accounts (e.g., joseph-gergaud -> gergaud) - Fix bot filtering for co-authors (check login suffix) - Fix case canonicalization (GitHub logins are case-insensitive) - Update workflow to use --use-commits flag
1 parent 65e4962 commit 16493d8

1 file changed

Lines changed: 51 additions & 6 deletions

File tree

scripts/contributors/github_contributors.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,28 @@
2525
re.IGNORECASE,
2626
)
2727

28+
# Manual mapping from email address to GitHub login.
29+
# Used for contributors whose email is not public on GitHub and who never
30+
# committed directly on a default branch (so we cannot discover their login
31+
# automatically). Keys must be lowercase.
32+
KNOWN_EMAIL_TO_LOGIN: Dict[str, str] = {
33+
'valentin.0111@hotmail.fr': 'vmerc',
34+
}
35+
36+
# Co-author emails to ignore entirely (e.g., bots that don't use a '[bot]'
37+
# suffix in their login and therefore escape automatic filtering).
38+
# Keys must be lowercase.
39+
IGNORED_COAUTHOR_EMAILS = {
40+
'compathelper_noreply@julialang.org',
41+
}
42+
43+
# Merge known duplicate GitHub accounts belonging to the same real-world
44+
# contributor. Keys are aliases (lowercase), values are the canonical login
45+
# that contributions should be attributed to (exact case).
46+
LOGIN_ALIASES: Dict[str, str] = {
47+
'joseph-gergaud': 'gergaud',
48+
}
49+
2850

2951
def parse_coauthors(message: str) -> List[Tuple[str, str]]:
3052
"""Parse Co-authored-by trailers from a commit message.
@@ -147,12 +169,24 @@ def get_all_contributors_from_commits(repo_owner: str, repo_name: str, exclude_b
147169
params = {'per_page': 100}
148170

149171
contributor_counts = Counter()
150-
# Cache email -> login (None means unresolvable). Seeded while iterating
151-
# commits whose primary author is linked to a GitHub account.
152-
email_to_login: Dict[str, Optional[str]] = {}
172+
# Cache email -> login (None means unresolvable). Seeded with manual
173+
# mapping and then updated while iterating commits whose primary author
174+
# is linked to a GitHub account.
175+
email_to_login: Dict[str, Optional[str]] = dict(KNOWN_EMAIL_TO_LOGIN)
176+
# Canonical case for GitHub logins (lowercase -> canonical form).
177+
# GitHub logins are case-insensitive, but noreply emails store them in
178+
# lowercase while the /commits API returns the canonical form.
179+
canonical_logins: Dict[str, str] = {}
153180
unresolved_coauthors: Dict[str, str] = {} # email -> name, for the warning
154181
page = 1
155182

183+
def canonical(login: str) -> str:
184+
"""Return the canonical case for a login if known, else the input."""
185+
return canonical_logins.get(login.lower(), login)
186+
187+
def is_bot(login: str) -> bool:
188+
return login.lower().endswith('[bot]')
189+
156190
print(f" 🔍 Enumerating commits on default branch...")
157191

158192
try:
@@ -179,6 +213,8 @@ def get_all_contributors_from_commits(repo_owner: str, repo_name: str, exclude_b
179213
if exclude_bots and author.get('type') != 'User':
180214
name = None
181215
if name:
216+
# Record canonical case (API returns it correctly cased)
217+
canonical_logins[name.lower()] = name
182218
contributor_counts[name] += 1
183219
# Seed the cache: we now know this email maps to this login
184220
if commit_email:
@@ -187,11 +223,18 @@ def get_all_contributors_from_commits(repo_owner: str, repo_name: str, exclude_b
187223
# Co-authors from "Co-authored-by:" trailers (handles squash merges)
188224
message = commit_data.get('message', '')
189225
for coauthor_name, coauthor_email in parse_coauthors(message):
226+
# Skip explicitly ignored emails (e.g., non-standard bots)
227+
if coauthor_email in IGNORED_COAUTHOR_EMAILS:
228+
continue
190229
login = resolve_email_to_login(coauthor_email, email_to_login, headers)
191-
if login:
192-
contributor_counts[login] += 1
193-
else:
230+
if not login:
194231
unresolved_coauthors[coauthor_email] = coauthor_name
232+
continue
233+
# Filter bots (their logins end with '[bot]')
234+
if exclude_bots and is_bot(login):
235+
continue
236+
# Canonicalize case (noreply emails are lowercase)
237+
contributor_counts[canonical(login)] += 1
195238

196239
print(f" 📄 Fetched {len(commits)} commits (page {page})")
197240

@@ -246,6 +289,8 @@ def aggregate_contributors(packages: List[Tuple[str, str]], exclude_bots: bool =
246289
contributors = get_contributors(owner, repo, exclude_bots, github_token, use_commits)
247290

248291
for name, contributions in contributors:
292+
# Merge known duplicate accounts (e.g., joseph-gergaud -> gergaud)
293+
name = LOGIN_ALIASES.get(name.lower(), name)
249294
# Exclude specified names
250295
if name.lower() not in exclude_names_lower:
251296
aggregated[name] += contributions

0 commit comments

Comments
 (0)