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.

g1t.sh
328 linesCodeBlameRaw
1//! `why`: why a line of a file is the way it is.
2//!
3//! The line's commit comes from the repos service's blame; the merged pull
4//! request it arrived in is the one whose head it is, or else the one whose
5//! stretch of the default branch's history holds it (as the site's blame
6//! view finds it, `apps/web/app/lib/provenance.server.ts`); then that pull
7//! request's issue, its decision records, and the session entries recorded
8//! around the commit.
9
10use g1t_contracts::decisions::{Decision, ListDecisionsArgs, Why, WhyCommit, WhyIssue, WhyPull};
11use g1t_contracts::repos::{Blame, BlameArgs, Commit, LogArgs, RepoPath};
12use g1t_contracts::work::{IssueDetail, ListPullsArgs, Pull, PullStatus, SessionEntry, SessionEntryKind, State, ViewArgs};
13use g1t_contracts::{FailureCode, Outcome, Viewer};
14use serde_json::Value;
15use worker::Result;
16
17use crate::operations::Services;
18
19/// How far back the default branch is read to place a commit.
20const HISTORY_READ: u32 = 300;
21/// How many steps of a session are shown for one commit.
22const MAX_STEPS: usize = 8;
23/// Longest a step's text is shown.
24const MAX_STEP_CHARS: usize = 600;
25/// Longest an issue's description is shown.
26const MAX_BODY_CHARS: usize = 900;
27
28/// The commit that last changed `line`, from a blame of the file.
29pub(crate) fn commit_of_line(blame: &Blame, line: u32) -> Option<&Commit> {
30 let range = blame.ranges.iter().find(|range| range.start <= line && line <= range.end)?;
31 blame.commits.iter().find(|commit| commit.hash == range.commit)
32}
33
34/// The merged pull request `hash` arrived in: the one whose head it is,
35/// or else the one whose stretch of `history` (the default branch, newest
36/// first) holds it, from its head back to the commit it started from.
37pub(crate) fn pull_for_commit<'a>(merged: &'a [Pull], history: &[String], hash: &str) -> Option<&'a Pull> {
38 let merged: Vec<&Pull> = merged
39 .iter()
40 .filter(|pull| pull.status == PullStatus::Merged && pull.head_commit.is_some())
41 .collect();
42 if let Some(direct) = merged.iter().find(|pull| pull.head_commit.as_deref() == Some(hash)) {
43 return Some(direct);
44 }
45 let position = |commit: &str| history.iter().position(|known| known == commit);
46 let at = position(hash)?;
47 let mut best: Option<(&Pull, usize)> = None;
48 for pull in merged {
49 let Some(head) = pull.head_commit.as_deref().and_then(position) else { continue };
50 if head > at {
51 continue;
52 }
53 if let Some(base) = pull.merge_base.as_deref().and_then(position)
54 && base <= at
55 {
56 continue;
57 }
58 if best.is_none_or(|(_, best_head)| head > best_head) {
59 best = Some((pull, head));
60 }
61 }
62 best.map(|(pull, _)| pull)
63}
64
65/// The steps of a session behind `hash`: what was said and done between
66/// the push before it and the push that carried it. Steps that touch
67/// `file` come first; at most [`MAX_STEPS`], each cut to
68/// [`MAX_STEP_CHARS`].
69pub(crate) fn steps_for(session: &[SessionEntry], hash: &str, file: &str) -> Vec<SessionEntry> {
70 // A push is noted "Pushed b1c42490059b."
71 let pushed = |entry: &SessionEntry| -> Option<String> {
72 if entry.kind != SessionEntryKind::Note {
73 return None;
74 }
75 let rest = entry.text.strip_prefix("Pushed ")?;
76 let short: String = rest.chars().take_while(char::is_ascii_hexdigit).collect();
77 (short.len() >= 7).then_some(short)
78 };
79 let end = session.iter().position(|entry| {
80 entry.commit.as_deref() == Some(hash) || pushed(entry).is_some_and(|short| hash.starts_with(&short))
81 });
82 let window: &[SessionEntry] = match end {
83 Some(end) => {
84 let mut start = end;
85 while start > 0 && pushed(&session[start - 1]).is_none() {
86 start -= 1;
87 }
88 &session[start..=end]
89 }
90 None => session,
91 };
92 let name = file.rsplit('/').next().unwrap_or(file);
93 let useful: Vec<&SessionEntry> = window
94 .iter()
95 .filter(|entry| {
96 entry.kind == SessionEntryKind::Message
97 || (entry.kind == SessionEntryKind::ToolCall && (entry.text.contains(file) || entry.text.contains(name)))
98 })
99 .collect();
100 let touching: Vec<&SessionEntry> = useful.iter().copied().filter(|entry| entry.text.contains(name)).collect();
101 let mut chosen: Vec<&SessionEntry> = if touching.is_empty() {
102 useful.clone()
103 } else {
104 touching.into_iter().chain(useful.iter().copied().filter(|entry| entry.kind == SessionEntryKind::Message)).collect()
105 };
106 chosen.sort_by_key(|entry| entry.seq);
107 chosen.dedup_by_key(|entry| entry.seq);
108 let skip = chosen.len().saturating_sub(MAX_STEPS);
109 chosen
110 .into_iter()
111 .skip(skip)
112 .map(|entry| {
113 let mut entry = entry.clone();
114 if entry.text.chars().count() > MAX_STEP_CHARS {
115 entry.text = format!("{}…", entry.text.chars().take(MAX_STEP_CHARS).collect::<String>());
116 }
117 entry
118 })
119 .collect()
120}
121
122/// Answers `why`.
123pub(crate) async fn why(
124 services: &Services,
125 viewer: &Viewer,
126 repo: RepoPath,
127 path: String,
128 line: u32,
129 git_ref: Option<String>,
130) -> Result<Outcome<Value>> {
131 let blame: Outcome<Blame> = g1t_kit::call(
132 &services.repos,
133 "blame",
134 &BlameArgs {
135 path: repo.clone(),
136 viewer: viewer.clone(),
137 git_ref,
138 file_path: path.clone(),
139 },
140 )
141 .await?;
142 let blame = match blame {
143 Outcome::Ok(blame) => blame,
144 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
145 };
146 let Some(found) = commit_of_line(&blame, line).cloned() else {
147 let lines = blame.ranges.last().map_or(0, |range| range.end);
148 return Ok(Outcome::fail(
149 FailureCode::Invalid,
150 format!("{path} has {lines} lines at that ref: give a line from 1 to {lines}."),
151 ));
152 };
153
154 let closed: Outcome<Vec<Pull>> = g1t_kit::call(
155 &services.work,
156 "list_pulls",
157 &ListPullsArgs {
158 repo: repo.clone(),
159 viewer: viewer.clone(),
160 state: Some(State::Closed),
161 label: None,
162 milestone: None,
163 base: None,
164 },
165 )
166 .await?;
167 let merged = match closed {
168 Outcome::Ok(pulls) => pulls,
169 Outcome::Fail(_) => Vec::new(),
170 };
171 // Who wrote it, by account: the address itself is never returned.
172 let authors = crate::commits::read_authors(services, &[&found]).await;
173 let username = crate::commits::username(&found, &authors).map(str::to_owned);
174 let commit = WhyCommit::of(found, username);
175 let mut pull = pull_for_commit(&merged, &[], &commit.hash).cloned();
176 if pull.is_none() && !merged.is_empty() {
177 let log: Outcome<Vec<Commit>> = g1t_kit::call(
178 &services.repos,
179 "log",
180 &LogArgs {
181 path: repo.clone(),
182 viewer: viewer.clone(),
183 git_ref: None,
184 limit: HISTORY_READ,
185 },
186 )
187 .await?;
188 if let Outcome::Ok(log) = log {
189 let history: Vec<String> = log.into_iter().map(|commit| commit.hash).collect();
190 pull = pull_for_commit(&merged, &history, &commit.hash).cloned();
191 }
192 }
193
194 let Some(pull) = pull else {
195 return ok(Why { path, line, commit, pull: None, issue: None, decisions: Vec::new(), sessions: Vec::new() });
196 };
197 let view = |number: u32| ViewArgs { repo: repo.clone(), number, viewer: viewer.clone(), after_seq: 0 };
198 // A few small reads, one after another: the API has no join helper.
199 let issue = match pull.issue {
200 Some(number) => match g1t_kit::call::<_, Outcome<IssueDetail>>(&services.work, "get_issue", &view(number)).await? {
201 Outcome::Ok(detail) => Some(detail.issue),
202 Outcome::Fail(_) => None,
203 },
204 None => None,
205 };
206 let decisions: Outcome<Vec<Decision>> = g1t_kit::call(
207 &services.work,
208 "list_decisions",
209 &ListDecisionsArgs { repo: repo.clone(), viewer: viewer.clone(), number: pull.number },
210 )
211 .await?;
212 let session: Outcome<Vec<SessionEntry>> = g1t_kit::call(&services.work, "read_session", &view(pull.number)).await?;
213 let decisions = match decisions {
214 Outcome::Ok(decisions) => decisions,
215 Outcome::Fail(_) => Vec::new(),
216 };
217 let sessions = match session {
218 Outcome::Ok(entries) => steps_for(&entries, &commit.hash, &path),
219 Outcome::Fail(_) => Vec::new(),
220 };
221 let issue = issue.map(|issue| WhyIssue {
222 number: issue.number,
223 title: issue.title,
224 state: if issue.state == State::Open { "open" } else { "closed" }.to_owned(),
225 body: if issue.body.chars().count() > MAX_BODY_CHARS {
226 format!("{}…", issue.body.chars().take(MAX_BODY_CHARS).collect::<String>())
227 } else {
228 issue.body
229 },
230 });
231 let pull = WhyPull {
232 number: pull.number,
233 title: pull.title.clone(),
234 author: pull.owner().username.clone(),
235 agent: pull.agent.clone(),
236 merged_by: pull.merged_by.clone(),
237 merged_at: pull.merged_at.clone(),
238 };
239 ok(Why { path, line, commit, pull: Some(pull), issue, decisions, sessions })
240}
241
242fn ok(why: Why) -> Result<Outcome<Value>> {
243 Ok(Outcome::Ok(serde_json::to_value(why)?))
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use g1t_contracts::repos::{BlameRange, Signature};
250
251 fn commit(hash: &str) -> Commit {
252 Commit {
253 hash: hash.into(),
254 tree_hash: "t".into(),
255 message: "m".into(),
256 author: Signature { name: "Ana".into(), email: "ana@example.com".into() },
257 parents: Vec::new(),
258 authored_at: "2026-10-12T00:00:00Z".into(),
259 }
260 }
261
262 fn merged(number: u32, head: &str, base: Option<&str>) -> Pull {
263 serde_json::from_value(serde_json::json!({
264 "id": format!("pr_{number}"), "repoId": "rep_1", "number": number, "issue": 9, "title": "t", "body": null,
265 "agent": "claude-code", "runtime": "external", "status": "merged", "fork": null, "forkRepoId": null,
266 "branch": "b", "headCommit": head, "mergeBase": base, "mergedBy": "ana", "mergedAt": "2026-10-12T00:00:00Z",
267 "supersededBy": null, "checkStatus": null,
268 "author": { "id": "usr_1", "username": "ana" },
269 "createdAt": "", "updatedAt": ""
270 }))
271 .unwrap()
272 }
273
274 #[test]
275 fn a_line_is_found_in_its_range() {
276 let blame = Blame {
277 head: "h".into(),
278 ranges: vec![
279 BlameRange { start: 1, end: 3, commit: "aaa".into() },
280 BlameRange { start: 4, end: 4, commit: "bbb".into() },
281 ],
282 commits: vec![commit("aaa"), commit("bbb")],
283 partial: false,
284 };
285 assert_eq!(commit_of_line(&blame, 3).map(|c| c.hash.as_str()), Some("aaa"));
286 assert_eq!(commit_of_line(&blame, 4).map(|c| c.hash.as_str()), Some("bbb"));
287 assert!(commit_of_line(&blame, 5).is_none());
288 }
289
290 #[test]
291 fn a_commit_is_placed_in_the_pull_request_it_arrived_in() {
292 // Newest first: m2 landed #12 on top of b1; m1 landed #11 on top of b0.
293 let history: Vec<String> = ["m2", "c3", "c2", "b1", "m1", "c1", "b0"].map(String::from).to_vec();
294 let pulls = vec![merged(11, "m1", Some("b0")), merged(12, "m2", Some("b1"))];
295 assert_eq!(pull_for_commit(&pulls, &[], "m1").map(|p| p.number), Some(11));
296 assert_eq!(pull_for_commit(&pulls, &history, "c2").map(|p| p.number), Some(12));
297 assert_eq!(pull_for_commit(&pulls, &history, "c1").map(|p| p.number), Some(11));
298 // Before any pull request.
299 assert!(pull_for_commit(&pulls, &history, "b0").is_none());
300 assert!(pull_for_commit(&pulls, &history, "zzz").is_none());
301 }
302
303 fn entry(seq: u32, kind: SessionEntryKind, text: &str, commit: Option<&str>) -> SessionEntry {
304 SessionEntry { seq, kind, text: text.into(), tool: None, commit: commit.map(String::from), at: String::new() }
305 }
306
307 #[test]
308 fn the_session_steps_are_those_behind_the_commit() {
309 use SessionEntryKind::*;
310 let session = vec![
311 entry(1, Message, "Starting on the parser.", None),
312 entry(2, Note, "Pushed 1111111aaaa.", None),
313 entry(3, Message, "The lexer drops the last token.", None),
314 entry(4, ToolCall, "Edit src/lexer.rs", None),
315 entry(5, ToolCall, "Run cargo test", None),
316 entry(6, Note, "Pushed 2222222bbbb.", None),
317 entry(7, Message, "Now the docs.", None),
318 ];
319 let steps = steps_for(&session, "2222222bbbbcccc", "src/lexer.rs");
320 let seqs: Vec<u32> = steps.iter().map(|step| step.seq).collect();
321 assert_eq!(seqs, [3, 4]);
322 // A commit no push names: the whole session is looked through.
323 let all = steps_for(&session, "9999999", "src/lexer.rs");
324 assert_eq!(all.iter().map(|s| s.seq).collect::<Vec<_>>(), [1, 3, 4, 7]);
325 let long = vec![entry(1, Message, &"x".repeat(MAX_STEP_CHARS + 10), None)];
326 assert!(steps_for(&long, "h", "a.rs")[0].text.ends_with('…'));
327 }
328}