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