Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

224 linesCodeBlameRaw
1//! A person's year, for the contribution calendar on their profile: how
2//! many commits they pushed to a default branch (or `gh-pages`), work items
3//! (filed from a repository) and pull requests they opened (or g1t opened for them, see `requested_by`)
4//! and reviews they gave, day by day (UTC).
5//!
6//! Commits are the repos service's: it credits whoever pushed, as each
7//! push lands (`commit_days`, services/repos/src/push_commits.rs), and
8//! counts only repositories the viewer may read, by the same `readable`.
9//!
10//! As with `by_author` (authored.rs), only repositories the viewer may read
11//! are counted: the repos service decides which (`readable`) over every
12//! repository the person worked in that year, and every count here is then
13//! confined to that set. A day's number never includes work in a private
14//! repository the viewer could not open.
15
16use g1t_contracts::identity::UsernameArgs;
17use g1t_contracts::repos::{CommitDaysArgs, ReadableArgs, Repo, MAX_READABLE};
18use g1t_contracts::time::rfc3339;
19use g1t_contracts::work::*;
20use g1t_contracts::{FailureCode, Outcome, Viewer};
21use g1t_kit::now_ms;
22use serde::Deserialize;
23use worker::Result;
24
25use crate::Work;
26
27/// Every repository the person did something in since `?2`, busiest first.
28const TOUCHED_SQL: &str = "SELECT repo_id, count(*) AS n FROM (
29 SELECT o.repo_id AS repo_id FROM work_items i JOIN work_item_repos o ON o.item_id = i.id AND o.role = 'origin'
30 WHERE COALESCE(i.requested_by_id, i.author_id) = ?1 AND i.created_at >= ?2
31 UNION ALL SELECT repo_id FROM pulls WHERE COALESCE(requested_by_id, author_id) = ?1 AND created_at >= ?2
32 UNION ALL SELECT repo_id FROM comments WHERE author_id = ?1 AND created_at >= ?2 AND verdict IS NOT NULL
33 ) GROUP BY repo_id ORDER BY n DESC LIMIT ?3";
34
35/// What they did each day since `?3`, in the repositories `?2` (a JSON
36/// array of ids) only.
37const DAYS_SQL: &str = "SELECT day, count(*) AS n FROM (
38 SELECT substr(i.created_at, 1, 10) AS day FROM work_items i
39 JOIN work_item_repos o ON o.item_id = i.id AND o.role = 'origin'
40 WHERE COALESCE(i.requested_by_id, i.author_id) = ?1 AND i.created_at >= ?3
41 AND o.repo_id IN (SELECT value FROM json_each(?2))
42 UNION ALL
43 SELECT substr(created_at, 1, 10) AS day FROM pulls
44 WHERE COALESCE(requested_by_id, author_id) = ?1 AND created_at >= ?3
45 AND repo_id IN (SELECT value FROM json_each(?2))
46 UNION ALL
47 SELECT substr(created_at, 1, 10) AS day FROM comments
48 WHERE author_id = ?1 AND created_at >= ?3 AND verdict IS NOT NULL
49 AND repo_id IN (SELECT value FROM json_each(?2))
50 ) GROUP BY day ORDER BY day";
51
52#[derive(Deserialize)]
53struct RepoCount {
54 repo_id: String,
55}
56
57#[derive(Deserialize)]
58struct DayRow {
59 day: String,
60 n: u32,
61}
62
63/// The first day counted, `YYYY-MM-DD`, when it is `now_ms`: today and the
64/// days before it, [`CONTRIBUTION_DAYS`] in all.
65fn first_day(now_ms: u64) -> String {
66 let back = (CONTRIBUTION_DAYS - 1) * 86_400_000;
67 rfc3339(now_ms.saturating_sub(back))[..10].to_owned()
68}
69
70/// The rows and the days' commits as the calendar's days and their total.
71fn tally(rows: Vec<DayRow>, commits: Vec<ContributionDay>, from: String) -> Contributions {
72 let mut by_day = std::collections::BTreeMap::<String, (u32, u32)>::new();
73 for row in rows {
74 by_day.entry(row.day).or_default().0 += row.n;
75 }
76 for day in commits {
77 by_day.entry(day.date).or_default().1 += day.commits;
78 }
79 let days: Vec<ContributionDay> = by_day
80 .into_iter()
81 .filter(|(day, (work, commits))| work + commits > 0 && day.len() == 10 && day.as_str() >= from.as_str())
82 .map(|(date, (work, commits))| ContributionDay {
83 date,
84 count: work + commits,
85 commits,
86 })
87 .collect();
88 let total = days.iter().map(|day| day.count).sum();
89 Contributions { days, total, from }
90}
91
92impl Work {
93 pub(crate) async fn contributions(&self, a: ContributionsArgs) -> Result<Outcome<Contributions>> {
94 let person: Viewer = g1t_kit::call(
95 &self.identity,
96 "user_by_username",
97 &UsernameArgs {
98 username: a.username.trim().to_lowercase(),
99 },
100 )
101 .await?;
102 let Some(person) = person else {
103 return Ok(Outcome::fail(FailureCode::NotFound, "There is no such account."));
104 };
105 let from = first_day(now_ms());
106 // Commits come from the repos service, asked at once; a calendar
107 // without them is better than none.
108 let (commits, rows) = futures_util::future::join(
109 g1t_kit::call::<_, Vec<ContributionDay>>(
110 &self.repos,
111 "commit_days",
112 &CommitDaysArgs {
113 user_id: person.id.clone(),
114 since: from.clone(),
115 viewer: a.viewer.clone(),
116 },
117 ),
118 self.work_days(&person.id, &from, a.viewer),
119 )
120 .await;
121 let commits = commits.unwrap_or_else(|error| {
122 worker::console_error!("commit_days for {} failed: {error}", person.username);
123 Vec::new()
124 });
125 Ok(Outcome::Ok(tally(rows?, commits, from)))
126 }
127
128 /// Work items, pull requests and reviews a day, in repositories the viewer may read.
129 async fn work_days(&self, person: &str, from: &str, viewer: Viewer) -> Result<Vec<DayRow>> {
130 let touched = self
131 .db
132 .prepare(TOUCHED_SQL)
133 .bind(&[person.into(), from.into(), (MAX_READABLE as u32).into()])?
134 .all()
135 .await?
136 .results::<RepoCount>()?;
137 if touched.is_empty() {
138 return Ok(Vec::new());
139 }
140 let readable: Vec<Repo> = g1t_kit::call(
141 &self.repos,
142 "readable",
143 &ReadableArgs {
144 ids: touched.into_iter().map(|row| row.repo_id).collect(),
145 viewer,
146 },
147 )
148 .await?;
149 if readable.is_empty() {
150 return Ok(Vec::new());
151 }
152 let ids: Vec<&str> = readable.iter().map(|repo| repo.id.as_str()).collect();
153 let visible = serde_json::to_string(&ids)?;
154 self.db
155 .prepare(DAYS_SQL)
156 .bind(&[person.into(), visible.as_str().into(), from.into()])?
157 .all()
158 .await?
159 .results::<DayRow>()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn every_count_is_confined_to_the_readable_set() {
169 // One `json_each(?2)` per kind of work: nothing is counted outside it.
170 assert_eq!(DAYS_SQL.matches("json_each(?2)").count(), 3);
171 assert_eq!(DAYS_SQL.matches("created_at >= ?3").count(), 3);
172 // Reviews are comments with a verdict; plain comments are not counted.
173 assert_eq!(DAYS_SQL.matches("verdict IS NOT NULL").count(), 1);
174 assert_eq!(TOUCHED_SQL.matches("created_at >= ?2").count(), 3);
175 }
176
177 #[test]
178 fn the_year_is_today_and_the_364_days_before() {
179 // 2026-10-08T12:00:00Z.
180 let now = 1_791_460_800_000;
181 assert_eq!(&rfc3339(now)[..10], "2026-10-08");
182 assert_eq!(first_day(now), "2025-10-09");
183 assert_eq!(first_day(0), "1970-01-01");
184 }
185
186 #[test]
187 fn tallies_days_and_drops_the_empty_and_the_early() {
188 let rows = vec![
189 DayRow { day: "2025-10-08".into(), n: 4 },
190 DayRow { day: "2025-10-09".into(), n: 2 },
191 DayRow { day: "2026-01-01".into(), n: 0 },
192 DayRow { day: "2026-10-08".into(), n: 3 },
193 ];
194 let year = tally(rows, Vec::new(), "2025-10-09".into());
195 assert_eq!(year.total, 5);
196 assert_eq!(
197 year.days,
198 vec![
199 ContributionDay { date: "2025-10-09".into(), count: 2, commits: 0 },
200 ContributionDay { date: "2026-10-08".into(), count: 3, commits: 0 },
201 ]
202 );
203 assert_eq!(tally(Vec::new(), Vec::new(), "2025-10-09".into()).total, 0);
204 }
205
206 #[test]
207 fn commits_join_the_days_they_were_pushed() {
208 let rows = vec![DayRow { day: "2026-10-01".into(), n: 2 }];
209 let commits = vec![
210 ContributionDay { date: "2026-10-01".into(), count: 3, commits: 3 },
211 ContributionDay { date: "2026-10-02".into(), count: 1, commits: 1 },
212 ContributionDay { date: "2025-01-01".into(), count: 9, commits: 9 },
213 ];
214 let year = tally(rows, commits, "2025-10-09".into());
215 assert_eq!(year.total, 6);
216 assert_eq!(
217 year.days,
218 vec![
219 ContributionDay { date: "2026-10-01".into(), count: 5, commits: 3 },
220 ContributionDay { date: "2026-10-02".into(), count: 1, commits: 1 },
221 ]
222 );
223 }
224}