Skip to content
167 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1//! Commits people push, for the contribution calendar on their profile.
2//!
3//! When a push lands on a repository's default branch (or `gh-pages`, which
4//! publishes a site), the person who pushed is credited, for that UTC day,
5//! with the push and the new commits it brought: the commits along the
6//! branch's first-parent line from where it is now back to where it was,
7//! at most [`MOST_PER_PUSH`]. A merge commit counts once, not the commits
8//! it merged. A branch's first push counts one commit, so importing a long
9//! history does not fill one day. Other branches count when they are
10//! merged, which is a pull request.
11//!
12//! Only people are credited: pushes with an agent's, a workspace's or a
13//! workflow job's token are not counted. The rows are in `push_commits`
14//! (migrations/0017); `commit_days` reads them for work's `contributions`,
15//! only for repositories the viewer may read (`Registry::readable`).
16
17use g1t_contracts::Viewer;
18use g1t_contracts::repos::Commit;
19use g1t_contracts::work::ContributionDay;
20use serde::Deserialize;
21use worker::{D1Database, Result};
22
23use crate::registry::Registry;
24
25/// The most commits one push is credited with.
26pub const MOST_PER_PUSH: u32 = 50;
27
28/// Whether a push to `branch` counts on the calendar.
29pub fn counts(branch: &str, default_branch: &str) -> bool {
30 branch == default_branch || branch == "gh-pages"
31}
32
33/// How many new commits a push brought, from the branch's log after it
34/// (newest first, first parents, at most `MOST_PER_PUSH + 1`) and where the
35/// branch was before. A new branch is one; a log that ends without reaching
36/// `before` (history rewritten onto an unrelated line) is one; one that
37/// runs past the cap is the cap.
38pub fn new_commits(log: &[Commit], before: Option<&str>) -> u32 {
39 let Some(before) = before.filter(|before| !before.is_empty() && !before.bytes().all(|b| b == b'0')) else {
40 return u32::from(!log.is_empty());
41 };
42 match log.iter().position(|commit| commit.hash == before) {
43 Some(at) => (at as u32).min(MOST_PER_PUSH),
44 None if log.len() as u32 > MOST_PER_PUSH => MOST_PER_PUSH,
45 None => u32::from(!log.is_empty()),
46 }
47}
48
49/// Credits `user_id` with one push of `commits` commits to `repo_id` on `day`.
50pub async fn record(db: &D1Database, user_id: &str, repo_id: &str, day: &str, commits: u32) -> Result<()> {
51 db.prepare(
52 "INSERT INTO push_commits (user_id, repo_id, day, pushes, commits) VALUES (?1, ?2, ?3, 1, ?4)
53 ON CONFLICT (user_id, day, repo_id) DO UPDATE SET
54 pushes = push_commits.pushes + 1, commits = push_commits.commits + excluded.commits",
55 )
56 .bind(&[user_id.into(), repo_id.into(), day.into(), commits.into()])?
57 .run()
58 .await?;
59 Ok(())
60}
61
62#[derive(Deserialize)]
63struct RepoRow {
64 repo_id: String,
65}
66
67#[derive(Deserialize)]
68struct DayRow {
69 day: String,
70 commits: u32,
71}
72
73/// What `commit_days` answers: a person's commits each day since `since`,
74/// in the repositories the viewer may read.
75pub async fn days(registry: &Registry, user_id: &str, since: &str, viewer: &Viewer) -> Result<Vec<ContributionDay>> {
76 let pushed = registry
77 .db
78 .prepare(
79 "SELECT repo_id FROM push_commits WHERE user_id = ?1 AND day >= ?2
80 GROUP BY repo_id ORDER BY SUM(commits) DESC LIMIT ?3",
81 )
82 .bind(&[user_id.into(), since.into(), (g1t_contracts::repos::MAX_READABLE as u32).into()])?
83 .all()
84 .await?
85 .results::<RepoRow>()?;
86 if pushed.is_empty() {
87 return Ok(Vec::new());
88 }
89 let ids: Vec<String> = pushed.into_iter().map(|row| row.repo_id).collect();
90 let readable = registry.readable(&ids, viewer).await?;
91 if readable.is_empty() {
92 return Ok(Vec::new());
93 }
94 let visible: Vec<&str> = readable.iter().map(|repo| repo.id.as_str()).collect();
95 let rows = registry
96 .db
97 .prepare(DAYS_SQL)
98 .bind(&[user_id.into(), since.into(), serde_json::to_string(&visible)?.into()])?
99 .all()
100 .await?
101 .results::<DayRow>()?;
102 Ok(rows
103 .into_iter()
104 .filter(|row| row.commits > 0)
105 .map(|row| ContributionDay { date: row.day, count: row.commits, commits: row.commits })
106 .collect())
107}
108
109/// Commits a day, in the repositories `?3` (a JSON array of ids) only.
110const DAYS_SQL: &str = "SELECT day, SUM(commits) AS commits FROM push_commits
111 WHERE user_id = ?1 AND day >= ?2 AND repo_id IN (SELECT value FROM json_each(?3))
112 GROUP BY day ORDER BY day";
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use g1t_contracts::repos::Signature;
118
119 fn log(hashes: &[&str]) -> Vec<Commit> {
120 hashes
121 .iter()
122 .map(|hash| Commit {
123 hash: (*hash).to_owned(),
124 tree_hash: String::new(),
125 message: String::new(),
126 author: Signature { name: String::new(), email: String::new() },
127 parents: Vec::new(),
128 authored_at: String::new(),
129 })
130 .collect()
131 }
132
133 #[test]
134 fn the_default_branch_and_gh_pages_count() {
135 assert!(counts("main", "main"));
136 assert!(counts("gh-pages", "main"));
137 assert!(!counts("feature", "main"));
138 }
139
140 #[test]
141 fn a_push_brings_the_commits_after_where_the_branch_was() {
142 assert_eq!(new_commits(&log(&["c", "b", "a"]), Some("a")), 2);
143 assert_eq!(new_commits(&log(&["c", "b", "a"]), Some("c")), 0);
144 }
145
146 #[test]
147 fn a_new_branch_or_a_rewrite_is_one() {
148 assert_eq!(new_commits(&log(&["c", "b", "a"]), None), 1);
149 assert_eq!(new_commits(&log(&["c", "b", "a"]), Some(&"0".repeat(40))), 1);
150 assert_eq!(new_commits(&log(&["z", "y"]), Some("a")), 1);
151 assert_eq!(new_commits(&[], Some("a")), 0);
152 }
153
154 #[test]
155 fn a_push_is_credited_with_the_cap_at_most() {
156 let hashes: Vec<String> = (0..=MOST_PER_PUSH).map(|n| format!("h{n}")).collect();
157 let long = log(&hashes.iter().map(String::as_str).collect::<Vec<_>>());
158 assert_eq!(new_commits(&long, Some("old")), MOST_PER_PUSH);
159 assert_eq!(new_commits(&long, Some(&format!("h{MOST_PER_PUSH}"))), MOST_PER_PUSH);
160 }
161
162 #[test]
163 fn days_are_confined_to_the_readable_set() {
164 assert_eq!(DAYS_SQL.matches("json_each(?3)").count(), 1);
165 assert!(DAYS_SQL.contains("user_id = ?1 AND day >= ?2"));
166 }
167}