Skip to content
202 linesCodeBlameRaw
1//! Who made a repository: the authors of the commits on its default branch,
2//! each matched to their account by an address they confirmed (or their
3//! noreply address), g1t as itself, and anyone else by the name on their
4//! commits. Several addresses of one account count as one contributor.
5
6use std::collections::{BTreeMap, HashMap};
7
8use g1t_contracts::about::{Contributor, ContributorKind, MAX_CONTRIBUTOR_WEEKS, MAX_CONTRIBUTORS, WeekCommits, next_week, week_of};
9use g1t_contracts::accounts::EmailOwner;
10use g1t_contracts::coding_agents::coding_agent_for;
11
12/// The address g1t's own commits carry.
13pub const G1T_EMAIL: &str = "g1t@users.noreply.g1t.sh";
14
15/// The addresses g1t's agents and merge queue committed as before they
16/// committed as g1t (2026-10-06): history keeps them, and they are g1t's.
17/// The site matches the same set (apps/web/app/lib/commit-people.ts).
18pub const LEGACY_G1T_EMAILS: [&str; 3] = ["agent@g1t.sh", "queue@g1t.sh", "mergecheck@g1t.sh"];
19
20/// One commit's author, as history gives it.
21#[derive(Clone, Debug)]
22pub struct Authored {
23 pub name: String,
24 pub email: String,
25 /// RFC 3339.
26 pub at: String,
27}
28
29/// Whether a commit's author is g1t itself.
30pub fn is_g1t(author: &Authored) -> bool {
31 let email = author.email.trim();
32 email.eq_ignore_ascii_case(G1T_EMAIL) || LEGACY_G1T_EMAILS.iter().any(|legacy| email.eq_ignore_ascii_case(legacy))
33}
34
35#[derive(Default)]
36struct Tally {
37 kind: Option<ContributorKind>,
38 name: String,
39 username: Option<String>,
40 avatar: Option<String>,
41 agent: Option<String>,
42 commits: u32,
43 first_at: String,
44 last_at: String,
45 weeks: BTreeMap<String, u32>,
46}
47
48/// The contributors, most commits first (then by name), at most
49/// `MAX_CONTRIBUTORS`, the most active `MAX_CONTRIBUTOR_WEEKS` with their
50/// weeks; how many there are; and every commit by week, oldest first, with
51/// the empty weeks between.
52pub fn tally(commits: &[Authored], owners: &HashMap<String, EmailOwner>) -> (Vec<Contributor>, u32, Vec<WeekCommits>) {
53 let mut by_key: HashMap<String, Tally> = HashMap::new();
54 let mut all_weeks: BTreeMap<String, u32> = BTreeMap::new();
55 for commit in commits {
56 let email = commit.email.trim().to_ascii_lowercase();
57 let (key, kind, name, username, avatar, agent) = if is_g1t(commit) {
58 ("g1t".to_owned(), ContributorKind::G1t, "g1t".to_owned(), None, None, None)
59 } else if let Some(owner) = owners.get(&email) {
60 (format!("user:{}", owner.id), ContributorKind::User, owner.username.clone(), Some(owner.username.clone()), owner.avatar.clone(), None)
61 } else if let Some(agent) = coding_agent_for(&email) {
62 // One contributor per agent, whichever of its addresses a commit carries.
63 (format!("agent:{agent}"), ContributorKind::Agent, commit.name.trim().to_owned(), None, None, Some(agent.to_owned()))
64 } else {
65 let key = if email.is_empty() { format!("name:{}", commit.name.trim().to_lowercase()) } else { format!("email:{email}") };
66 (key, ContributorKind::Author, commit.name.trim().to_owned(), None, None, None)
67 };
68 let tally = by_key.entry(key).or_default();
69 if tally.kind.is_none() {
70 *tally = Tally { kind: Some(kind), name, username, avatar, agent, first_at: commit.at.clone(), last_at: commit.at.clone(), ..Tally::default() };
71 }
72 tally.commits += 1;
73 if commit.at < tally.first_at {
74 tally.first_at = commit.at.clone();
75 }
76 if commit.at > tally.last_at {
77 tally.last_at = commit.at.clone();
78 }
79 if let Some(week) = week_of(&commit.at) {
80 *tally.weeks.entry(week.clone()).or_default() += 1;
81 *all_weeks.entry(week).or_default() += 1;
82 }
83 }
84 let total = by_key.len() as u32;
85 let mut tallies: Vec<Tally> = by_key.into_values().collect();
86 tallies.sort_by(|a, b| b.commits.cmp(&a.commits).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())));
87 tallies.truncate(MAX_CONTRIBUTORS);
88 let contributors = tallies
89 .into_iter()
90 .enumerate()
91 .map(|(rank, tally)| Contributor {
92 kind: tally.kind.unwrap_or(ContributorKind::Author),
93 name: tally.name,
94 username: tally.username,
95 avatar: tally.avatar,
96 agent: tally.agent,
97 commits: tally.commits,
98 first_at: tally.first_at,
99 last_at: tally.last_at,
100 weeks: if rank < MAX_CONTRIBUTOR_WEEKS {
101 tally.weeks.into_iter().map(|(week, commits)| WeekCommits { week, commits }).collect()
102 } else {
103 Vec::new()
104 },
105 })
106 .collect();
107 (contributors, total, filled(&all_weeks))
108}
109
110/// Every week from the first to the last, with none for those without.
111fn filled(weeks: &BTreeMap<String, u32>) -> Vec<WeekCommits> {
112 let (Some(first), Some(last)) = (weeks.keys().next(), weeks.keys().next_back()) else {
113 return Vec::new();
114 };
115 let mut out = Vec::new();
116 let mut week = first.clone();
117 // At most twenty years of weeks, whatever the dates say.
118 while week <= *last && out.len() < 1_100 {
119 out.push(WeekCommits { commits: weeks.get(&week).copied().unwrap_or(0), week: week.clone() });
120 match next_week(&week) {
121 Some(next) => week = next,
122 None => break,
123 }
124 }
125 out
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn authored(name: &str, email: &str, at: &str) -> Authored {
133 Authored { name: name.into(), email: email.into(), at: at.into() }
134 }
135
136 #[test]
137 fn authors_are_matched_merged_and_ranked() {
138 let commits = vec![
139 authored("Ada", "ada@example.com", "2026-10-07T10:00:00Z"),
140 authored("Ada L", "ADA@work.example", "2026-09-30T10:00:00Z"),
141 authored("g1t", G1T_EMAIL, "2026-10-06T10:00:00Z"),
142 authored("g1t", G1T_EMAIL, "2026-10-01T10:00:00Z"),
143 authored("g1t", G1T_EMAIL, "2026-09-20T10:00:00Z"),
144 authored("Sam", "sam@example.com", "2026-09-16T10:00:00Z"),
145 ];
146 let ada = EmailOwner { id: "usr_ada".into(), username: "ada".into(), avatar: Some("abc".into()) };
147 let owners = HashMap::from([("ada@example.com".to_owned(), ada.clone()), ("ada@work.example".to_owned(), ada)]);
148 let (contributors, total, weeks) = tally(&commits, &owners);
149 assert_eq!(total, 3);
150 assert_eq!(contributors[0].kind, ContributorKind::G1t);
151 assert_eq!(contributors[0].commits, 3);
152 assert_eq!(contributors[1].username.as_deref(), Some("ada"));
153 assert_eq!(contributors[1].commits, 2, "both of Ada's addresses");
154 assert_eq!(contributors[1].first_at, "2026-09-30T10:00:00Z");
155 assert_eq!(contributors[1].last_at, "2026-10-07T10:00:00Z");
156 assert_eq!(contributors[1].avatar.as_deref(), Some("abc"));
157 assert_eq!(contributors[2].kind, ContributorKind::Author);
158 assert_eq!(contributors[2].name, "Sam");
159 // Weeks of 2026-09-14 through 2026-10-05, none skipped.
160 let named: Vec<(&str, u32)> = weeks.iter().map(|week| (week.week.as_str(), week.commits)).collect();
161 assert_eq!(named, vec![("2026-09-14", 2), ("2026-09-21", 0), ("2026-09-28", 2), ("2026-10-05", 2)]);
162 assert_eq!(contributors[1].weeks.len(), 2);
163 }
164
165 #[test]
166 fn coding_agents_are_agents_by_their_address_whatever_the_name() {
167 let commits = vec![
168 authored("Claude", "noreply@anthropic.com", "2026-10-01T10:00:00Z"),
169 authored("claude[bot]", "209825114+claude[bot]@users.noreply.github.com", "2026-10-02T10:00:00Z"),
170 authored("Claude", "claude@example.com", "2026-10-03T10:00:00Z"),
171 ];
172 let (contributors, total, _) = tally(&commits, &HashMap::new());
173 assert_eq!(total, 2);
174 assert_eq!(contributors[0].kind, ContributorKind::Agent);
175 assert_eq!(contributors[0].agent.as_deref(), Some("claude"));
176 assert_eq!(contributors[0].commits, 2, "both of its addresses");
177 assert_eq!(contributors[1].kind, ContributorKind::Author, "a person who calls themselves Claude stays a person");
178 }
179
180 #[test]
181 fn g1t_older_addresses_are_g1t() {
182 let commits = vec![
183 authored("g1t agent", "agent@g1t.sh", "2026-10-01T10:00:00Z"),
184 authored("g1t merge queue", "Queue@g1t.sh", "2026-10-02T10:00:00Z"),
185 authored("g1t", G1T_EMAIL, "2026-10-07T10:00:00Z"),
186 authored("Not g1t", "someone@g1t.sh", "2026-10-07T10:00:00Z"),
187 ];
188 let (contributors, total, _) = tally(&commits, &HashMap::new());
189 assert_eq!(total, 2);
190 assert_eq!(contributors[0].kind, ContributorKind::G1t);
191 assert_eq!(contributors[0].name, "g1t");
192 assert_eq!(contributors[0].commits, 3);
193 assert_eq!(contributors[1].kind, ContributorKind::Author, "only g1t's own addresses");
194 }
195
196 #[test]
197 fn no_history_is_no_one() {
198 let (contributors, total, weeks) = tally(&[], &HashMap::new());
199 assert!(contributors.is_empty() && weeks.is_empty());
200 assert_eq!(total, 0);
201 }
202}