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 | //! 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 | ||
| 10 | use serde::{Deserialize, Serialize}; | |
| 11 | ||
| 12 | use crate::repos::{Commit, RepoPath}; | |
| 13 | use crate::work::SessionEntry; | |
| 14 | use 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")] | |
| 19 | pub 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 | ||
| 38 | impl 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")] | |
| 65 | pub 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 | ||
| 74 | impl 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")] | |
| 87 | pub 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")] | |
| 113 | pub 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")] | |
| 130 | pub 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")] | |
| 140 | pub 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")] | |
| 157 | pub 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")] | |
| 183 | pub 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")] | |
| 196 | pub 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")] | |
| 207 | pub 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")] | |
| 219 | pub 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")] | |
| 232 | pub 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)] | |
| 243 | mod 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 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.