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.

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