Skip to content
302 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 issue it is about, when known.
90 pub issue: 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 an issue (and on its pull requests)
128/// or on a pull request, oldest first. Returns `Outcome<Vec<Decision>>`.
129#[derive(Debug, Serialize, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct ListDecisionsArgs {
132 pub repo: RepoPath,
133 pub viewer: Viewer,
134 /// An issue's or a pull request's number: they share a sequence.
135 pub number: u32,
136}
137
138/// The check that failed a combined state.
139#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct FailedCheck {
142 /// The check's name, such as `CI / merge_group`; `merge` for a merge
143 /// conflict, `build` when the state could not be built.
144 pub name: String,
145 /// `failure`, `error`, `missing` (a required check never reported),
146 /// or `conflict`.
147 pub conclusion: String,
148 /// Where to see it, such as the workflow run's page.
149 #[serde(default)]
150 pub url: Option<String>,
151}
152
153/// `integration.failed`: a combined state in the merge queue failed, and
154/// g1t worked out which pull request answers for it and which others it
155/// breaks with.
156#[derive(Clone, Debug, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct IntegrationFailed {
159 pub repo_id: String,
160 pub pull_id: String,
161 /// The responsible pull request, as `pull`.
162 pub number: u32,
163 pub pull: u32,
164 /// Its issue, if it has one.
165 pub issue: Option<u32>,
166 /// The pull requests tested together, in queue order; the last is
167 /// `pull`.
168 pub combination: Vec<u32>,
169 pub check: FailedCheck,
170 /// Whether smaller combinations were built to narrow `suspects` down.
171 pub bisected: bool,
172 /// The pull requests ahead of it that it fails with. Empty when it
173 /// fails by itself.
174 pub suspects: Vec<u32>,
175 /// The other pull requests in `combination`.
176 pub other_pulls: Vec<u32>,
177 /// With nothing ahead of it, though its own checks pass on its head:
178 /// the pull requests merged into the default branch since it branched,
179 /// newest first and at most ten, any of which may be what it now fails
180 /// with. It still answers for the failure. Empty otherwise.
181 #[serde(default)]
182 pub landed: Vec<u32>,
183 /// The combined state that failed.
184 pub commit: Option<String>,
185}
186
187/// `integration.passed`: a combined state in the merge queue passed.
188#[derive(Clone, Debug, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct IntegrationPassed {
191 pub repo_id: String,
192 pub pull_id: String,
193 pub number: u32,
194 pub pull: u32,
195 pub issue: Option<u32>,
196 pub combination: Vec<u32>,
197 pub commit: Option<String>,
198}
199
200/// `decision.recorded`.
201#[derive(Clone, Debug, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct DecisionRecorded {
204 pub repo_id: String,
205 /// The pull request it is about, or else its issue.
206 pub number: u32,
207 pub issue: Option<u32>,
208 pub decision: Decision,
209}
210
211/// The pull request a line arrived in, as `why` shows it.
212#[derive(Clone, Debug, Serialize, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct WhyPull {
215 pub number: u32,
216 pub title: String,
217 pub author: String,
218 pub agent: String,
219 pub merged_by: Option<String>,
220 pub merged_at: Option<String>,
221}
222
223/// The issue that pull request was for.
224#[derive(Clone, Debug, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct WhyIssue {
227 pub number: u32,
228 pub title: String,
229 pub state: String,
230 /// Its description, cut short when long.
231 pub body: String,
232}
233
234/// `why`: why a line is the way it is. The commit that last changed it,
235/// the merged pull request it arrived in, that pull request's issue, the
236/// decisions recorded on them, and the session entries behind the commit.
237#[derive(Clone, Debug, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct Why {
240 pub path: String,
241 pub line: u32,
242 pub commit: Commit,
243 pub pull: Option<WhyPull>,
244 pub issue: Option<WhyIssue>,
245 pub decisions: Vec<Decision>,
246 pub sessions: Vec<SessionEntry>,
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn kinds_read_and_write_as_their_names() {
255 for kind in [
256 DecisionKind::Proposal,
257 DecisionKind::Claim,
258 DecisionKind::IntegrationFailure,
259 DecisionKind::IntegrationPassed,
260 DecisionKind::Fix,
261 DecisionKind::Approval,
262 DecisionKind::Merge,
263 DecisionKind::Note,
264 ] {
265 assert_eq!(DecisionKind::parse(kind.as_str()), Some(kind));
266 assert_eq!(serde_json::to_value(kind).unwrap(), kind.as_str());
267 }
268 assert_eq!(DecisionKind::parse("merged"), None);
269 assert!(!DecisionKind::RECORDABLE.contains(&DecisionKind::IntegrationFailure));
270 assert!(!DecisionKind::RECORDABLE.contains(&DecisionKind::Approval));
271 assert_eq!(serde_json::to_value(DecisionConfidence::Tested).unwrap(), "tested");
272 }
273
274 #[test]
275 fn an_integration_failure_carries_number_and_issue_for_filters() {
276 let event = IntegrationFailed {
277 repo_id: "rep_1".into(),
278 pull_id: "pr_1".into(),
279 number: 14,
280 pull: 14,
281 issue: Some(9),
282 combination: vec![11, 12, 14],
283 check: FailedCheck { name: "CI / merge_group".into(), conclusion: "failure".into(), url: None },
284 bisected: true,
285 suspects: vec![12],
286 other_pulls: vec![11, 12],
287 landed: Vec::new(),
288 commit: Some("abc".into()),
289 };
290 let sent = serde_json::to_value(&event).unwrap();
291 assert_eq!(sent["number"], 14);
292 assert_eq!(sent["issue"], 9);
293 assert_eq!(sent["otherPulls"], serde_json::json!([11, 12]));
294 assert_eq!(sent["check"]["conclusion"], "failure");
295 assert_eq!(sent["landed"], serde_json::json!([]));
296 // An event from before `landed` was sent still reads.
297 let mut older = sent.clone();
298 older.as_object_mut().unwrap().remove("landed");
299 let read: IntegrationFailed = serde_json::from_value(older).unwrap();
300 assert!(read.landed.is_empty());
301 }
302}