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
324 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.

Merge branch 'worktree-agent-ab8033ab973f8d99a'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, 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(commit) = 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 let mut pull = pull_for_commit(&merged, &[], &commit.hash).cloned();
172 if pull.is_none() && !merged.is_empty() {
173 let log: Outcome<Vec<Commit>> = g1t_kit::call(
174 &services.repos,
175 "log",
176 &LogArgs {
177 path: repo.clone(),
178 viewer: viewer.clone(),
179 git_ref: None,
180 limit: HISTORY_READ,
181 },
182 )
183 .await?;
184 if let Outcome::Ok(log) = log {
185 let history: Vec<String> = log.into_iter().map(|commit| commit.hash).collect();
186 pull = pull_for_commit(&merged, &history, &commit.hash).cloned();
187 }
188 }
189
190 let Some(pull) = pull else {
191 return ok(Why { path, line, commit, pull: None, issue: None, decisions: Vec::new(), sessions: Vec::new() });
192 };
193 let view = |number: u32| ViewArgs { repo: repo.clone(), number, viewer: viewer.clone(), after_seq: 0 };
194 // A few small reads, one after another: the API has no join helper.
195 let issue = match pull.issue {
196 Some(number) => match g1t_kit::call::<_, Outcome<IssueDetail>>(&services.work, "get_issue", &view(number)).await? {
197 Outcome::Ok(detail) => Some(detail.issue),
198 Outcome::Fail(_) => None,
199 },
200 None => None,
201 };
202 let decisions: Outcome<Vec<Decision>> = g1t_kit::call(
203 &services.work,
204 "list_decisions",
205 &ListDecisionsArgs { repo: repo.clone(), viewer: viewer.clone(), number: pull.number },
206 )
207 .await?;
208 let session: Outcome<Vec<SessionEntry>> = g1t_kit::call(&services.work, "read_session", &view(pull.number)).await?;
209 let decisions = match decisions {
210 Outcome::Ok(decisions) => decisions,
211 Outcome::Fail(_) => Vec::new(),
212 };
213 let sessions = match session {
214 Outcome::Ok(entries) => steps_for(&entries, &commit.hash, &path),
215 Outcome::Fail(_) => Vec::new(),
216 };
217 let issue = issue.map(|issue| WhyIssue {
218 number: issue.number,
219 title: issue.title,
220 state: if issue.state == State::Open { "open" } else { "closed" }.to_owned(),
221 body: if issue.body.chars().count() > MAX_BODY_CHARS {
222 format!("{}…", issue.body.chars().take(MAX_BODY_CHARS).collect::<String>())
223 } else {
224 issue.body
225 },
226 });
227 let pull = WhyPull {
228 number: pull.number,
229 title: pull.title.clone(),
230 author: pull.owner().username.clone(),
231 agent: pull.agent.clone(),
232 merged_by: pull.merged_by.clone(),
233 merged_at: pull.merged_at.clone(),
234 };
235 ok(Why { path, line, commit, pull: Some(pull), issue, decisions, sessions })
236}
237
238fn ok(why: Why) -> Result<Outcome<Value>> {
239 Ok(Outcome::Ok(serde_json::to_value(why)?))
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use g1t_contracts::repos::{BlameRange, Signature};
246
247 fn commit(hash: &str) -> Commit {
248 Commit {
249 hash: hash.into(),
250 tree_hash: "t".into(),
251 message: "m".into(),
252 author: Signature { name: "Ana".into(), email: "ana@example.com".into() },
253 parents: Vec::new(),
254 authored_at: "2026-10-12T00:00:00Z".into(),
255 }
256 }
257
258 fn merged(number: u32, head: &str, base: Option<&str>) -> Pull {
259 serde_json::from_value(serde_json::json!({
260 "id": format!("pr_{number}"), "repoId": "rep_1", "number": number, "issue": 9, "title": "t", "body": null,
261 "agent": "claude-code", "runtime": "external", "status": "merged", "fork": null, "forkRepoId": null,
262 "branch": "b", "headCommit": head, "mergeBase": base, "mergedBy": "ana", "mergedAt": "2026-10-12T00:00:00Z",
263 "supersededBy": null, "checkStatus": null,
264 "author": { "id": "usr_1", "username": "ana" },
265 "createdAt": "", "updatedAt": ""
266 }))
267 .unwrap()
268 }
269
270 #[test]
271 fn a_line_is_found_in_its_range() {
272 let blame = Blame {
273 head: "h".into(),
274 ranges: vec![
275 BlameRange { start: 1, end: 3, commit: "aaa".into() },
276 BlameRange { start: 4, end: 4, commit: "bbb".into() },
277 ],
278 commits: vec![commit("aaa"), commit("bbb")],
279 partial: false,
280 };
281 assert_eq!(commit_of_line(&blame, 3).map(|c| c.hash.as_str()), Some("aaa"));
282 assert_eq!(commit_of_line(&blame, 4).map(|c| c.hash.as_str()), Some("bbb"));
283 assert!(commit_of_line(&blame, 5).is_none());
284 }
285
286 #[test]
287 fn a_commit_is_placed_in_the_pull_request_it_arrived_in() {
288 // Newest first: m2 landed #12 on top of b1; m1 landed #11 on top of b0.
289 let history: Vec<String> = ["m2", "c3", "c2", "b1", "m1", "c1", "b0"].map(String::from).to_vec();
290 let pulls = vec![merged(11, "m1", Some("b0")), merged(12, "m2", Some("b1"))];
291 assert_eq!(pull_for_commit(&pulls, &[], "m1").map(|p| p.number), Some(11));
292 assert_eq!(pull_for_commit(&pulls, &history, "c2").map(|p| p.number), Some(12));
293 assert_eq!(pull_for_commit(&pulls, &history, "c1").map(|p| p.number), Some(11));
294 // Before any pull request.
295 assert!(pull_for_commit(&pulls, &history, "b0").is_none());
296 assert!(pull_for_commit(&pulls, &history, "zzz").is_none());
297 }
298
299 fn entry(seq: u32, kind: SessionEntryKind, text: &str, commit: Option<&str>) -> SessionEntry {
300 SessionEntry { seq, kind, text: text.into(), tool: None, commit: commit.map(String::from), at: String::new() }
301 }
302
303 #[test]
304 fn the_session_steps_are_those_behind_the_commit() {
305 use SessionEntryKind::*;
306 let session = vec![
307 entry(1, Message, "Starting on the parser.", None),
308 entry(2, Note, "Pushed 1111111aaaa.", None),
309 entry(3, Message, "The lexer drops the last token.", None),
310 entry(4, ToolCall, "Edit src/lexer.rs", None),
311 entry(5, ToolCall, "Run cargo test", None),
312 entry(6, Note, "Pushed 2222222bbbb.", None),
313 entry(7, Message, "Now the docs.", None),
314 ];
315 let steps = steps_for(&session, "2222222bbbbcccc", "src/lexer.rs");
316 let seqs: Vec<u32> = steps.iter().map(|step| step.seq).collect();
317 assert_eq!(seqs, [3, 4]);
318 // A commit no push names: the whole session is looked through.
319 let all = steps_for(&session, "9999999", "src/lexer.rs");
320 assert_eq!(all.iter().map(|s| s.seq).collect::<Vec<_>>(), [1, 3, 4, 7]);
321 let long = vec![entry(1, Message, &"x".repeat(MAX_STEP_CHARS + 10), None)];
322 assert!(steps_for(&long, "h", "a.rs")[0].text.ends_with('…'));
323 }
324}

This file's history is long; its oldest lines are credited to the oldest commit read.