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
2951def 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