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.

363 linesCodeBlameRaw
1//! Decision records, what the merge queue learns when changes are combined,
2//! and why a line of code is the way it is.
3//!
4//! A decision record is a structured reason with a confidence: `claimed`
5//! when an agent or a person said so, `tested` when a check or a combination
6//! of pull requests proved it, `approved` when a person approved it. The
7//! work service records the tested and approved ones itself (integration
8//! results, approvals, merges); `record_decision` records claimed ones.
9
10use serde::{Deserialize, Serialize};
11
12use crate::repos::{Commit, RepoPath};
13use crate::work::SessionEntry;
14use crate::{User, Viewer};
15
16/// What a decision is about.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum DecisionKind {
20 /// Why a change is made the way it is.
21 Proposal,
22 /// Something said to be true about the work, such as what it fixes.
23 Claim,
24 /// The merge queue found that a combination of pull requests fails.
25 IntegrationFailure,
26 /// The merge queue found that a combination of pull requests passes.
27 IntegrationPassed,
28 /// What was changed to fix a failure.
29 Fix,
30 /// A person approved the pull request.
31 Approval,
32 /// It was merged.
33 Merge,
34 /// Anything else worth keeping.
35 Note,
36}
37
38impl DecisionKind {
39 pub fn as_str(self) -> &'static str {
40 match self {
41 DecisionKind::Proposal => "proposal",
42 DecisionKind::Claim => "claim",
43 DecisionKind::IntegrationFailure => "integration_failure",
44 DecisionKind::IntegrationPassed => "integration_passed",
45 DecisionKind::Fix => "fix",
46 DecisionKind::Approval => "approval",
47 DecisionKind::Merge => "merge",
48 DecisionKind::Note => "note",
49 }
50 }
51
52 pub fn parse(kind: &str) -> Option<DecisionKind> {
53 serde_json::from_value(serde_json::Value::String(kind.to_owned())).ok()
54 }
55
56 /// The kinds `record_decision` takes. The others are what g1t saw
57 /// happen, and only g1t records them.
58 pub const RECORDABLE: [DecisionKind; 4] =
59 [DecisionKind::Proposal, DecisionKind::Claim, DecisionKind::Fix, DecisionKind::Note];
60}
61
62/// How sure the record is.
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum DecisionConfidence {
66 /// Someone said so.
67 Claimed,
68 /// A check or a combination of pull requests proved it.
69 Tested,
70 /// A person approved it.
71 Approved,
72}
73
74impl DecisionConfidence {
75 pub fn as_str(self) -> &'static str {
76 match self {
77 DecisionConfidence::Claimed => "claimed",
78 DecisionConfidence::Tested => "tested",
79 DecisionConfidence::Approved => "approved",
80 }
81 }
82}
83
84/// One decision record.
85#[derive(Clone, Debug, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct Decision {
88 pub id: String,
89 /// The work item it is about, when known, by number in its workspace.
90 pub item: Option<u32>,
91 /// The pull request it is about, when known.
92 pub pull: Option<u32>,
93 /// The session it was recorded in, if one was named.
94 #[serde(default)]
95 pub session_id: Option<String>,
96 /// Username of who recorded it; absent when g1t did.
97 pub actor: Option<String>,
98 pub kind: DecisionKind,
99 pub confidence: DecisionConfidence,
100 pub summary: String,
101 /// What it rests on, as an object: the commit, the check (`name`,
102 /// `conclusion`, `url`), the `combination` of pull requests tested
103 /// together, `other_pulls`, `suspects`, `bisected`, and for a failure
104 /// with nothing ahead, `landed`.
105 pub evidence: serde_json::Value,
106 /// RFC 3339.
107 pub created_at: String,
108}
109
110/// `record_decision`: an agent's or a person's decision on a pull request,
111/// recorded as `claimed`. Returns `Outcome<Decision>`.
112#[derive(Debug, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct RecordDecisionArgs {
115 pub actor: User,
116 pub repo: RepoPath,
117 /// The pull request's number.
118 pub number: u32,
119 pub kind: DecisionKind,
120 pub summary: String,
121 #[serde(default)]
122 pub evidence: Option<serde_json::Value>,
123 #[serde(default)]
124 pub session_id: Option<String>,
125}
126
127/// `list_decisions`: the decisions on a thread, oldest first: with `repo`,
128/// on its pull request `number`; without, on the work item `number` of
129/// `workspace` and on its pull requests. Returns `Outcome<Vec<Decision>>`.
130#[derive(Debug, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct ListDecisionsArgs {
133 #[serde(default)]
134 pub repo: Option<RepoPath>,
135 #[serde(default)]
136 pub workspace: String,
137 pub viewer: Viewer,
138 pub number: u32,
139}
140
141/// The check that failed a combined state.
142#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct FailedCheck {
145 /// The check's name, such as `CI / merge_group`; `merge` for a merge
146 /// conflict, `build` when the state could not be built.
147 pub name: String,
148 /// `failure`, `error`, `missing` (a required check never reported),
149 /// or `conflict`.
150 pub conclusion: String,
151 /// Where to see it, such as the workflow run's page.
152 #[serde(default)]
153 pub url: Option<String>,
154}
155
156/// `integration.failed`: a combined state in the merge queue failed, and
157/// g1t worked out which pull request answers for it and which others it
158/// breaks with.
159#[derive(Clone, Debug, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct IntegrationFailed {
162 pub repo_id: String,
163 pub pull_id: String,
164 /// The responsible pull request, as `pull`.
165 pub number: u32,
166 pub pull: u32,
167 /// Its work item, if it has one.
168 pub item: Option<u32>,
169 /// The pull requests tested together, in queue order; the last is
170 /// `pull`.
171 pub combination: Vec<u32>,
172 pub check: FailedCheck,
173 /// Whether smaller combinations were built to narrow `suspects` down.
174 pub bisected: bool,
175 /// The pull requests ahead of it that it fails with. Empty when it
176 /// fails by itself.
177 pub suspects: Vec<u32>,
178 /// The other pull requests in `combination`.
179 pub other_pulls: Vec<u32>,
180 /// With nothing ahead of it, though its own checks pass on its head:
181 /// the pull requests merged into the default branch since it branched,
182 /// newest first and at most ten, any of which may be what it now fails
183 /// with. It still answers for the failure. Empty otherwise.
184 #[serde(default)]
185 pub landed: Vec<u32>,
186 /// The combined state that failed.
187 pub commit: Option<String>,
188}
189
190/// `integration.passed`: a combined state in the merge queue passed.
191#[derive(Clone, Debug, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct IntegrationPassed {
194 pub repo_id: String,
195 pub pull_id: String,
196 pub number: u32,
197 pub pull: u32,
198 pub item: Option<u32>,
199 pub combination: Vec<u32>,
200 pub commit: Option<String>,
201}
202
203/// `decision.recorded`.
204#[derive(Clone, Debug, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct DecisionRecorded {
207 pub repo_id: String,
208 /// The pull request it is about, or else its work item.
209 pub number: u32,
210 pub item: Option<u32>,
211 pub decision: Decision,
212}
213
214/// The pull request a line arrived in, as `why` shows it.
215#[derive(Clone, Debug, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct WhyPull {
218 pub number: u32,
219 pub title: String,
220 pub author: String,
221 pub agent: String,
222 pub merged_by: Option<String>,
223 pub merged_at: Option<String>,
224}
225
226/// The work item that pull request was for.
227#[derive(Clone, Debug, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct WhyItem {
230 pub workspace: String,
231 pub number: u32,
232 pub title: String,
233 pub state: String,
234 /// Its description, cut short when long.
235 pub body: String,
236}
237
238/// The commit `why` names. Its author is named as git wrote them and by the
239/// g1t account their address belongs to, when one does; the address itself
240/// is never shown.
241#[derive(Clone, Debug, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct WhyCommit {
244 pub hash: String,
245 pub tree_hash: String,
246 pub message: String,
247 pub author: WhyAuthor,
248 pub parents: Vec<String>,
249 /// RFC 3339.
250 pub authored_at: String,
251}
252
253/// Who wrote a commit, without their address.
254#[derive(Clone, Debug, Serialize, Deserialize)]
255pub struct WhyAuthor {
256 pub name: String,
257 /// The g1t account the author's address belongs to: a confirmed or
258 /// noreply address of one, or g1t's own.
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub username: Option<String>,
261}
262
263impl WhyCommit {
264 /// `commit` as `why` shows it, its author's account `username` when known.
265 pub fn of(commit: Commit, username: Option<String>) -> WhyCommit {
266 WhyCommit {
267 hash: commit.hash,
268 tree_hash: commit.tree_hash,
269 message: commit.message,
270 author: WhyAuthor { name: commit.author.name, username },
271 parents: commit.parents,
272 authored_at: commit.authored_at,
273 }
274 }
275}
276
277/// `why`: why a line is the way it is. The commit that last changed it,
278/// the merged pull request it arrived in, that pull request's work item, the
279/// decisions recorded on them, and the session entries behind the commit.
280#[derive(Clone, Debug, Serialize, Deserialize)]
281#[serde(rename_all = "camelCase")]
282pub struct Why {
283 pub path: String,
284 pub line: u32,
285 pub commit: WhyCommit,
286 pub pull: Option<WhyPull>,
287 pub item: Option<WhyItem>,
288 pub decisions: Vec<Decision>,
289 pub sessions: Vec<SessionEntry>,
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn why_names_an_author_by_account_never_by_address() {
298 let commit = Commit {
299 hash: "7d3e".into(),
300 tree_hash: "c1d2".into(),
301 message: "Read retry_limit".into(),
302 author: crate::repos::Signature { name: "Chase Pierce".into(), email: "syntaqx@gmail.com".into() },
303 parents: vec![],
304 authored_at: "2026-10-14T00:00:00Z".into(),
305 };
306 let known = serde_json::to_value(WhyCommit::of(commit.clone(), Some("syntaqx".into()))).unwrap();
307 assert_eq!(known["author"], serde_json::json!({ "name": "Chase Pierce", "username": "syntaqx" }));
308 let unknown = serde_json::to_value(WhyCommit::of(commit, None)).unwrap();
309 assert_eq!(unknown["author"], serde_json::json!({ "name": "Chase Pierce" }));
310 assert!(!known.to_string().contains("gmail") && !unknown.to_string().contains("gmail"));
311 }
312
313
314 #[test]
315 fn kinds_read_and_write_as_their_names() {
316 for kind in [
317 DecisionKind::Proposal,
318 DecisionKind::Claim,
319 DecisionKind::IntegrationFailure,
320 DecisionKind::IntegrationPassed,
321 DecisionKind::Fix,
322 DecisionKind::Approval,
323 DecisionKind::Merge,
324 DecisionKind::Note,
325 ] {
326 assert_eq!(DecisionKind::parse(kind.as_str()), Some(kind));
327 assert_eq!(serde_json::to_value(kind).unwrap(), kind.as_str());
328 }
329 assert_eq!(DecisionKind::parse("merged"), None);
330 assert!(!DecisionKind::RECORDABLE.contains(&DecisionKind::IntegrationFailure));
331 assert!(!DecisionKind::RECORDABLE.contains(&DecisionKind::Approval));
332 assert_eq!(serde_json::to_value(DecisionConfidence::Tested).unwrap(), "tested");
333 }
334
335 #[test]
336 fn an_integration_failure_carries_number_and_item_for_filters() {
337 let event = IntegrationFailed {
338 repo_id: "rep_1".into(),
339 pull_id: "pr_1".into(),
340 number: 14,
341 pull: 14,
342 item: Some(9),
343 combination: vec![11, 12, 14],
344 check: FailedCheck { name: "CI / merge_group".into(), conclusion: "failure".into(), url: None },
345 bisected: true,
346 suspects: vec![12],
347 other_pulls: vec![11, 12],
348 landed: Vec::new(),
349 commit: Some("abc".into()),
350 };
351 let sent = serde_json::to_value(&event).unwrap();
352 assert_eq!(sent["number"], 14);
353 assert_eq!(sent["item"], 9);
354 assert_eq!(sent["otherPulls"], serde_json::json!([11, 12]));
355 assert_eq!(sent["check"]["conclusion"], "failure");
356 assert_eq!(sent["landed"], serde_json::json!([]));
357 // An event from before `landed` was sent still reads.
358 let mut older = sent.clone();
359 older.as_object_mut().unwrap().remove("landed");
360 let read: IntegrationFailed = serde_json::from_value(older).unwrap();
361 assert!(read.landed.is_empty());
362 }
363}