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.
| API: notifications over REST and MCP, with notifications scopes | 1 | //! The work service: issues, pull requests, comments and sessions. |
| 2 | //! | |
| 3 | //! Each `*Args` struct is the argument of the method of the same name, | |
| 4 | //! served at `POST /rpc/<method>`. | |
| 5 | //! | |
| 6 | //! Issues and pull requests share one sequence of numbers per repository, | |
| 7 | //! so `#12` names exactly one of them. | |
| 8 | ||
| 9 | use serde::{Deserialize, Serialize}; | |
| 10 | ||
| 11 | use crate::repos::{CompareArgs, RepoPath}; | |
| 12 | use crate::{User, Viewer}; | |
| 13 | ||
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 14 | // Sessions, claims and dependencies: work/coordination.rs. |
| 15 | mod coordination; | |
| 16 | pub use coordination::*; | |
| 17 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 18 | /// The labels a new repository starts with, and that "Add the default |
| 19 | /// labels" adds to one that is missing some: `(name, color, description)`. | |
| 20 | /// Colors are six hex digits, without `#`. | |
| 21 | pub const DEFAULT_LABELS: [(&str, &str, &str); 11] = [ | |
| 22 | ("bug", "d73a4a", "Something isn't working"), | |
| 23 | ("documentation", "0075ca", "Improvements or additions to documentation"), | |
| 24 | ("duplicate", "cfd3d7", "This issue or pull request already exists"), | |
| 25 | ("enhancement", "a2eeef", "New feature or request"), | |
| 26 | ("good first issue", "7057ff", "Good for newcomers"), | |
| 27 | ("help wanted", "008672", "Extra attention is needed"), | |
| 28 | ("invalid", "e4e669", "This doesn't seem right"), | |
| 29 | ("question", "d876e3", "Further information is requested"), | |
| 30 | ("wontfix", "ffffff", "This will not be worked on"), | |
| 31 | ("dependencies", "0366d6", "Updates a dependency"), | |
| 32 | ("security", "ee0701", "A security fix or a vulnerability"), | |
| 33 | ]; | |
| 34 | ||
| 35 | /// The color a label gets when none is given: chosen from its name, so | |
| 36 | /// the same name always gets the same color. | |
| 37 | pub fn label_color_for(name: &str) -> String { | |
| 38 | const PALETTE: [&str; 12] = [ | |
| 39 | "b60205", "d93f0b", "fbca04", "0e8a16", "006b75", "1d76db", "0052cc", "5319e7", "e99695", "f9d0c4", | |
| 40 | "c2e0c6", "bfdadc", | |
| 41 | ]; | |
| 42 | if let Some((_, color, _)) = DEFAULT_LABELS.iter().find(|(known, _, _)| *known == name) { | |
| 43 | return (*color).to_owned(); | |
| 44 | } | |
| 45 | let hash = name.bytes().fold(0u32, |hash, byte| hash.wrapping_mul(31).wrapping_add(u32::from(byte))); | |
| 46 | PALETTE[(hash as usize) % PALETTE.len()].to_owned() | |
| 47 | } | |
| 48 | ||
| 49 | /// A color as six lowercase hex digits, from `#A1B2C3`, `a1b2c3` or `abc`. | |
| 50 | pub fn tidy_color(color: &str) -> Option<String> { | |
| 51 | let hex = color.trim().trim_start_matches('#').to_ascii_lowercase(); | |
| 52 | if !hex.chars().all(|c| c.is_ascii_hexdigit()) { | |
| 53 | return None; | |
| 54 | } | |
| 55 | match hex.len() { | |
| 56 | 6 => Some(hex), | |
| 57 | 3 => Some(hex.chars().flat_map(|c| [c, c]).collect()), | |
| 58 | _ => None, | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | /// The most labels one issue or pull request can carry, and the longest | |
| 63 | /// name and description of one. | |
| 64 | pub const MAX_LABELS: usize = 20; | |
| 65 | pub const MAX_LABEL_CHARS: usize = 50; | |
| 66 | pub const MAX_LABEL_DESCRIPTION_CHARS: usize = 100; | |
| API: notifications over REST and MCP, with notifications scopes | 67 | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 68 | /// A label of a repository: a name, a color and what it means. Issues and |
| 69 | /// pull requests carry labels by name. Names are lowercase and unique in | |
| 70 | /// a repository. | |
| 71 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 72 | #[serde(rename_all = "camelCase")] | |
| 73 | pub struct Label { | |
| 74 | pub name: String, | |
| 75 | /// Six hex digits, without `#`. | |
| 76 | pub color: String, | |
| 77 | #[serde(default)] | |
| 78 | pub description: String, | |
| 79 | /// How many issues carry it, open or closed. | |
| 80 | #[serde(default)] | |
| 81 | pub issues: u32, | |
| 82 | /// How many pull requests carry it, in any state. | |
| 83 | #[serde(default)] | |
| 84 | pub pulls: u32, | |
| 85 | } | |
| 86 | ||
| 87 | /// A milestone, as an issue or pull request names it. | |
| 88 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 89 | #[serde(rename_all = "camelCase")] | |
| 90 | pub struct MilestoneRef { | |
| 91 | pub number: u32, | |
| 92 | pub title: String, | |
| 93 | } | |
| 94 | ||
| 95 | /// A milestone: a goal, with an optional due date, that issues and pull | |
| 96 | /// requests are gathered under. Its progress is how many of them are | |
| 97 | /// closed (a merged pull request counts as closed). | |
| 98 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 99 | #[serde(rename_all = "camelCase")] | |
| 100 | pub struct Milestone { | |
| 101 | /// Numbered from 1 in each repository, apart from issues. | |
| 102 | pub number: u32, | |
| 103 | pub title: String, | |
| 104 | /// Markdown. | |
| 105 | #[serde(default)] | |
| 106 | pub description: String, | |
| 107 | /// The day it is due, `YYYY-MM-DD`. | |
| 108 | #[serde(default)] | |
| 109 | pub due_on: Option<String>, | |
| 110 | pub state: State, | |
| 111 | /// Open issues and pull requests in it (drafts count as open). | |
| 112 | #[serde(default)] | |
| 113 | pub open_items: u32, | |
| 114 | /// Closed issues, and merged or closed pull requests, in it. | |
| 115 | #[serde(default)] | |
| 116 | pub closed_items: u32, | |
| 117 | /// RFC 3339. | |
| 118 | pub created_at: String, | |
| 119 | /// RFC 3339. | |
| 120 | pub updated_at: String, | |
| 121 | /// RFC 3339. | |
| 122 | #[serde(default)] | |
| 123 | pub closed_at: Option<String>, | |
| 124 | } | |
| 125 | ||
| 126 | impl Milestone { | |
| 127 | /// How far along it is, from 0 to 100: closed items over all of them. | |
| 128 | pub fn percent(&self) -> u32 { | |
| 129 | let total = self.open_items + self.closed_items; | |
| 130 | (self.closed_items * 100).checked_div(total).unwrap_or(0) | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | /// The most characters a milestone's title and description may have. | |
| 135 | pub const MAX_MILESTONE_TITLE_CHARS: usize = 100; | |
| 136 | pub const MAX_MILESTONE_DESCRIPTION_CHARS: usize = 4000; | |
| 137 | ||
| 138 | /// A due date as `YYYY-MM-DD`, from that or from an RFC 3339 time. | |
| 139 | pub fn tidy_due_on(value: &str) -> Option<String> { | |
| 140 | let day = value.trim().get(..10)?; | |
| 141 | let bytes = day.as_bytes(); | |
| 142 | let digits = |range: std::ops::Range<usize>| bytes[range].iter().all(u8::is_ascii_digit); | |
| 143 | if !(digits(0..4) && bytes[4] == b'-' && digits(5..7) && bytes[7] == b'-' && digits(8..10)) { | |
| 144 | return None; | |
| 145 | } | |
| 146 | let month: u32 = day[5..7].parse().ok()?; | |
| 147 | let date: u32 = day[8..10].parse().ok()?; | |
| 148 | ((1..=12).contains(&month) && (1..=31).contains(&date)).then(|| day.to_owned()) | |
| 149 | } | |
| 150 | ||
| 151 | /// `list_labels`: a repository's labels, by name, each with how many | |
| 152 | /// issues and pull requests carry it. Takes `ViewArgs`; returns | |
| 153 | /// `Outcome<Vec<Label>>`. | |
| 154 | /// | |
| 155 | /// `save_label`: creates a label, or with `name` changes one (renaming it | |
| 156 | /// renames it on every issue and pull request). Needs the Triage role. | |
| 157 | /// Returns `Outcome<Label>`. | |
| 158 | #[derive(Debug, Serialize, Deserialize)] | |
| 159 | #[serde(rename_all = "camelCase")] | |
| 160 | pub struct SaveLabelArgs { | |
| 161 | pub actor: User, | |
| 162 | pub repo: RepoPath, | |
| 163 | /// The label to change; absent to create one. | |
| 164 | #[serde(default)] | |
| 165 | pub name: Option<String>, | |
| 166 | /// The name it should have: required to create one. | |
| 167 | #[serde(default)] | |
| 168 | pub new_name: Option<String>, | |
| 169 | /// Six hex digits; one is chosen from the name when creating without. | |
| 170 | #[serde(default)] | |
| 171 | pub color: Option<String>, | |
| 172 | #[serde(default)] | |
| 173 | pub description: Option<String>, | |
| 174 | } | |
| 175 | ||
| 176 | /// `delete_label`: removes a label from the repository and from every | |
| 177 | /// issue and pull request that carries it. Needs the Triage role. Returns | |
| 178 | /// `Outcome<bool>`. | |
| 179 | #[derive(Debug, Serialize, Deserialize)] | |
| 180 | pub struct DeleteLabelArgs { | |
| 181 | pub actor: User, | |
| 182 | pub repo: RepoPath, | |
| 183 | pub name: String, | |
| 184 | } | |
| 185 | ||
| 186 | /// `add_default_labels`: adds those of [`DEFAULT_LABELS`] the repository | |
| 187 | /// does not have yet. Needs the Triage role. Returns | |
| 188 | /// `Outcome<Vec<Label>>`, every label it has now. | |
| 189 | #[derive(Debug, Serialize, Deserialize)] | |
| 190 | pub struct RepoActorArgs { | |
| 191 | pub actor: User, | |
| 192 | pub repo: RepoPath, | |
| 193 | } | |
| 194 | ||
| 195 | /// How `set_labels` changes an issue's or pull request's labels. | |
| 196 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 197 | #[serde(rename_all = "lowercase")] | |
| 198 | pub enum LabelChange { | |
| 199 | /// Replace them all. | |
| 200 | #[default] | |
| 201 | Set, | |
| 202 | /// Add these to the ones it has. | |
| 203 | Add, | |
| 204 | /// Take these off. | |
| 205 | Remove, | |
| 206 | } | |
| 207 | ||
| 208 | /// `set_labels`: changes the labels of an issue or a pull request (they | |
| 209 | /// share numbers). A label the repository does not have yet is created | |
| 210 | /// when the actor has the Triage role; anyone else may only use existing | |
| 211 | /// ones, on what they opened. Returns `Outcome<Vec<String>>`, its labels | |
| 212 | /// now. | |
| 213 | #[derive(Debug, Serialize, Deserialize)] | |
| 214 | #[serde(rename_all = "camelCase")] | |
| 215 | pub struct SetLabelsArgs { | |
| 216 | pub actor: User, | |
| 217 | pub repo: RepoPath, | |
| 218 | pub number: u32, | |
| 219 | pub labels: Vec<String>, | |
| 220 | #[serde(default)] | |
| 221 | pub change: LabelChange, | |
| 222 | } | |
| 223 | ||
| 224 | /// `list_milestones`: a repository's milestones, open ones by due date | |
| 225 | /// (soonest first, those without one last), then closed ones most recently | |
| 226 | /// closed first. Returns `Outcome<Vec<Milestone>>`. | |
| 227 | #[derive(Debug, Serialize, Deserialize)] | |
| 228 | pub struct ListMilestonesArgs { | |
| 229 | pub repo: RepoPath, | |
| 230 | pub viewer: Viewer, | |
| 231 | /// Both when absent. | |
| 232 | #[serde(default)] | |
| 233 | pub state: Option<State>, | |
| 234 | } | |
| 235 | ||
| 236 | /// `get_milestone`: one milestone and what is in it. Takes `ViewArgs`, | |
| 237 | /// whose `number` is the milestone's. Returns `Outcome<MilestoneDetail>`. | |
| 238 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 239 | pub struct MilestoneDetail { | |
| 240 | pub milestone: Milestone, | |
| 241 | /// Newest first, open and closed. | |
| 242 | pub issues: Vec<Issue>, | |
| 243 | /// Newest first, in any state. | |
| 244 | pub pulls: Vec<Pull>, | |
| 245 | } | |
| 246 | ||
| 247 | /// `save_milestone`: creates a milestone, or with `number` changes the | |
| 248 | /// fields given. Needs the Triage role. Returns `Outcome<Milestone>`. | |
| 249 | #[derive(Debug, Serialize, Deserialize)] | |
| 250 | #[serde(rename_all = "camelCase")] | |
| 251 | pub struct SaveMilestoneArgs { | |
| 252 | pub actor: User, | |
| 253 | pub repo: RepoPath, | |
| 254 | /// The milestone to change; absent to create one. | |
| 255 | #[serde(default)] | |
| 256 | pub number: Option<u32>, | |
| 257 | /// Required to create one. | |
| 258 | #[serde(default)] | |
| 259 | pub title: Option<String>, | |
| 260 | #[serde(default)] | |
| 261 | pub description: Option<String>, | |
| 262 | /// `YYYY-MM-DD`; an empty string clears it. | |
| 263 | #[serde(default)] | |
| 264 | pub due_on: Option<String>, | |
| 265 | #[serde(default)] | |
| 266 | pub state: Option<State>, | |
| 267 | } | |
| 268 | ||
| 269 | /// `delete_milestone`: removes a milestone; what was in it is in none. | |
| 270 | /// Needs the Triage role. Returns `Outcome<bool>`. | |
| 271 | #[derive(Debug, Serialize, Deserialize)] | |
| 272 | pub struct DeleteMilestoneArgs { | |
| 273 | pub actor: User, | |
| 274 | pub repo: RepoPath, | |
| 275 | pub number: u32, | |
| 276 | } | |
| 277 | ||
| API: notifications over REST and MCP, with notifications scopes | 278 | /// `open` or `closed`: the filter on lists of issues and pull requests. An |
| 279 | /// open pull request is a draft or one ready for review; a closed one was | |
| 280 | /// merged or closed without merging. | |
| 281 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 282 | #[serde(rename_all = "lowercase")] | |
| 283 | pub enum State { | |
| 284 | Open, | |
| 285 | Closed, | |
| 286 | } | |
| 287 | ||
| 288 | /// Why an issue was closed. | |
| 289 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 290 | #[serde(rename_all = "snake_case")] | |
| 291 | pub enum IssueReason { | |
| 292 | /// The work was done. If a pull request did it, `resolved_by` names it. | |
| 293 | Completed, | |
| 294 | NotPlanned, | |
| 295 | } | |
| 296 | ||
| 297 | impl IssueReason { | |
| 298 | pub fn as_str(self) -> &'static str { | |
| 299 | match self { | |
| 300 | IssueReason::Completed => "completed", | |
| 301 | IssueReason::NotPlanned => "not_planned", | |
| 302 | } | |
| 303 | } | |
| 304 | } | |
| 305 | ||
| 306 | /// Something that should change in a repository: a bug, a feature, a | |
| 307 | /// question. Opened by a person, an agent or an integration. Pull requests | |
| 308 | /// are made against it; the one that is merged resolves it. | |
| 309 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 310 | #[serde(rename_all = "camelCase")] | |
| 311 | pub struct Issue { | |
| 312 | pub id: String, | |
| 313 | pub repo_id: String, | |
| 314 | /// Shown as `#12`. | |
| 315 | pub number: u32, | |
| 316 | pub title: String, | |
| 317 | /// Markdown. Also what an agent is given to work from. | |
| 318 | pub body: String, | |
| 319 | pub labels: Vec<String>, | |
| 320 | pub state: State, | |
| 321 | /// Set when closed. | |
| 322 | pub reason: Option<IssueReason>, | |
| 323 | /// The number of the pull request whose merge closed this issue. | |
| 324 | pub resolved_by: Option<u32>, | |
| 325 | /// Who opened it: a person, an integration, or g1t (`kind` `agent`) | |
| 326 | /// for one its agent filed while at work. | |
| 327 | pub author: User, | |
| 328 | /// For an issue g1t's agent filed: the person it was working for. They | |
| 329 | /// may manage it as its author could. See [`owner`](Issue::owner). | |
| 330 | #[serde(default)] | |
| 331 | pub requested_by: Option<User>, | |
| 332 | /// RFC 3339. | |
| 333 | pub created_at: String, | |
| 334 | /// RFC 3339. | |
| 335 | pub updated_at: String, | |
| 336 | /// RFC 3339. | |
| 337 | pub closed_at: Option<String>, | |
| 338 | /// Pull requests made against this issue, in any state. | |
| 339 | pub pull_count: u32, | |
| 340 | pub comment_count: u32, | |
| 341 | /// Usernames of the people it is assigned to. | |
| 342 | #[serde(default)] | |
| 343 | pub assignees: Vec<String>, | |
| 344 | /// The numbers of the issues that have to be merged before this one is | |
| 345 | /// worked on. | |
| 346 | #[serde(default)] | |
| 347 | pub blocked_by: Vec<u32>, | |
| 348 | /// Whether a g1t agent takes it as soon as it can: at once, or when | |
| 349 | /// what it is blocked by has merged. | |
| 350 | #[serde(default)] | |
| 351 | pub queued: bool, | |
| 352 | /// The agent working on it now: the one behind its newest pull request | |
| 353 | /// that is still in progress in a fork, such as `g1t`. | |
| 354 | #[serde(default)] | |
| 355 | pub agent: Option<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 356 | /// The milestone it is in, if any. |
| 357 | #[serde(default)] | |
| 358 | pub milestone: Option<MilestoneRef>, | |
| API: notifications over REST and MCP, with notifications scopes | 359 | } |
| 360 | ||
| 361 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 362 | #[serde(rename_all = "lowercase")] | |
| 363 | pub enum PullStatus { | |
| 364 | /// Still being worked on. | |
| 365 | Draft, | |
| 366 | /// Ready for review. | |
| 367 | Open, | |
| 368 | Merged, | |
| 369 | /// Closed without merging. | |
| 370 | Closed, | |
| 371 | } | |
| 372 | ||
| 373 | impl PullStatus { | |
| 374 | pub fn as_str(self) -> &'static str { | |
| 375 | match self { | |
| 376 | PullStatus::Draft => "draft", | |
| 377 | PullStatus::Open => "open", | |
| 378 | PullStatus::Merged => "merged", | |
| 379 | PullStatus::Closed => "closed", | |
| 380 | } | |
| 381 | } | |
| 382 | ||
| 383 | /// Whether the pull request can still be changed or merged. | |
| 384 | pub fn is_active(self) -> bool { | |
| 385 | matches!(self, PullStatus::Draft | PullStatus::Open) | |
| 386 | } | |
| 387 | } | |
| 388 | ||
| 389 | /// Where the agent runs: on g1t's sandboxes, or in someone's own session. | |
| 390 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 391 | #[serde(rename_all = "lowercase")] | |
| 392 | pub enum Runtime { | |
| 393 | Hosted, | |
| 394 | External, | |
| 395 | } | |
| 396 | ||
| 397 | /// A proposed change. It is made either in a fork created for it, which is | |
| 398 | /// how agents work, or on a branch pushed to the repository itself. | |
| 399 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 400 | #[serde(rename_all = "camelCase")] | |
| 401 | pub struct Pull { | |
| 402 | pub id: String, | |
| 403 | pub repo_id: String, | |
| 404 | /// Shown as `#12`. | |
| 405 | pub number: u32, | |
| 406 | /// The number of the issue this is for, if any. | |
| 407 | pub issue: Option<u32>, | |
| 408 | pub title: String, | |
| 409 | /// Markdown: what changed and why. Set when marked ready. | |
| 410 | pub body: Option<String>, | |
| 411 | /// A label for the agent doing the work, e.g. `claude-code`. | |
| 412 | pub agent: String, | |
| 413 | pub runtime: Runtime, | |
| 414 | pub status: PullStatus, | |
| 415 | /// The fork holding the change, unless it is on a branch. | |
| 416 | pub fork: Option<RepoPath>, | |
| 417 | /// The fork's repository id. | |
| 418 | pub fork_repo_id: Option<String>, | |
| 419 | /// The branch of the repository holding the change, unless it is in a | |
| 420 | /// fork. | |
| 421 | pub branch: Option<String>, | |
| 422 | pub head_commit: Option<String>, | |
| 423 | /// For a merged pull request, what the branch pointed to before the | |
| 424 | /// merge. Comparing against it shows what the pull request changed. | |
| 425 | pub merge_base: Option<String>, | |
| 426 | /// Username of whoever merged it. | |
| 427 | pub merged_by: Option<String>, | |
| 428 | /// RFC 3339. | |
| 429 | pub merged_at: Option<String>, | |
| 430 | /// Set on a pull request closed because another one for the same issue | |
| 431 | /// was merged: that one's number. | |
| 432 | pub superseded_by: Option<u32>, | |
| 433 | /// `failed` when the merge queue took it out because its combined | |
| 434 | /// state failed, until its head moves. Its checks are the statuses | |
| 435 | /// workflows report on its head: see `PullDetail::statuses` and | |
| 436 | /// `PullDetail::required_checks`. | |
| 437 | pub check_status: Option<CheckStatus>, | |
| 438 | /// The files it changes, as of its latest push. | |
| 439 | #[serde(default)] | |
| 440 | pub files: Vec<ChangedFile>, | |
| 441 | /// Usernames of the people it is assigned to. | |
| 442 | #[serde(default)] | |
| 443 | pub assignees: Vec<String>, | |
| 444 | /// Those whose review was asked for: usernames, and `g1t` when a | |
| 445 | /// g1t agent was asked. | |
| 446 | #[serde(default)] | |
| 447 | pub reviewers: Vec<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 448 | /// Teams whose review was asked for, as `workspace/slug`. A team stays |
| 449 | /// here after review assignment picks people from it, who are listed | |
| 450 | /// in `reviewers`. | |
| 451 | #[serde(default)] | |
| 452 | pub team_reviewers: Vec<String>, | |
| 453 | /// The labels it carries, by name. | |
| 454 | #[serde(default)] | |
| 455 | pub labels: Vec<String>, | |
| 456 | /// The milestone it is in, if any. | |
| 457 | #[serde(default)] | |
| 458 | pub milestone: Option<MilestoneRef>, | |
| 459 | /// The branch it merges into. Stored as null for the repository's | |
| 460 | /// default branch, so that it follows a change of default; lists and | |
| 461 | /// `get_pull` fill in the name. See [`base_branch`](Pull::base_branch). | |
| 462 | #[serde(default)] | |
| 463 | pub base: Option<String>, | |
| API: notifications over REST and MCP, with notifications scopes | 464 | /// Who opened it: a person, or g1t (`kind` `agent`, username `g1t`) |
| 465 | /// for a change g1t made. | |
| 466 | pub author: User, | |
| 467 | /// For a change g1t made: the person who asked for it, by assigning | |
| 468 | /// an issue or handing g1t the work. They answer for it as its author | |
| 469 | /// would. See [`owner`](Pull::owner). | |
| 470 | #[serde(default)] | |
| 471 | pub requested_by: Option<User>, | |
| 472 | /// RFC 3339. | |
| 473 | pub created_at: String, | |
| 474 | /// RFC 3339. | |
| 475 | pub updated_at: String, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 476 | /// Who last moved its head (a user id), and when, RFC 3339: for rules |
| 477 | /// about the most recent push. Absent until a push after rulesets | |
| 478 | /// arrived. | |
| 479 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 480 | pub head_pushed_by: Option<String>, | |
| 481 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 482 | pub head_pushed_at: Option<String>, | |
| API: notifications over REST and MCP, with notifications scopes | 483 | } |
| 484 | ||
| 485 | /// One file a pull request changes, and by how much. | |
| 486 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 487 | pub struct ChangedFile { | |
| 488 | pub path: String, | |
| 489 | pub additions: u32, | |
| 490 | pub deletions: u32, | |
| 491 | } | |
| 492 | ||
| 493 | /// Another pull request in progress that changes some of the same files. | |
| 494 | /// Two for the same issue are alternatives; two for different issues are | |
| 495 | /// heading for a conflict. | |
| 496 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 497 | pub struct Overlap { | |
| 498 | pub number: u32, | |
| 499 | pub title: String, | |
| 500 | /// The number of the issue the other pull request is for. | |
| 501 | pub issue: Option<u32>, | |
| 502 | /// The files both change. | |
| 503 | pub paths: Vec<String>, | |
| 504 | } | |
| 505 | ||
| 506 | impl Issue { | |
| 507 | /// Who the issue is theirs to manage: the person g1t's agent filed it | |
| 508 | /// for, or its author. They may edit, close and reopen it without the | |
| 509 | /// Triage role. | |
| 510 | pub fn owner(&self) -> &User { | |
| 511 | self.requested_by.as_ref().unwrap_or(&self.author) | |
| 512 | } | |
| 513 | } | |
| 514 | ||
| 515 | impl Pull { | |
| 516 | /// Who the pull request is theirs to answer for: whoever asked g1t to | |
| 517 | /// make it, or its author. Every rule that once read "its author" reads | |
| 518 | /// this: they may update, close and steer it, are never asked to review | |
| 519 | /// it and cannot approve it, see it as theirs, and a sandbox at work on | |
| 520 | /// it acts as them. | |
| 521 | pub fn owner(&self) -> &User { | |
| 522 | self.requested_by.as_ref().unwrap_or(&self.author) | |
| 523 | } | |
| 524 | ||
| 525 | /// Whether `id` is its [`owner`](Pull::owner)'s. | |
| 526 | pub fn is_owned_by(&self, id: &str) -> bool { | |
| 527 | self.owner().id == id | |
| 528 | } | |
| 529 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 530 | /// The branch it merges into: its own base, or the repository's |
| 531 | /// default branch when it has none. | |
| 532 | pub fn base_branch<'a>(&'a self, default_branch: &'a str) -> &'a str { | |
| 533 | self.base.as_deref().filter(|base| !base.is_empty()).unwrap_or(default_branch) | |
| 534 | } | |
| 535 | ||
| 536 | /// Whether it merges into the repository's default branch. | |
| 537 | pub fn targets_default(&self, default_branch: &str) -> bool { | |
| 538 | self.base_branch(default_branch) == default_branch | |
| 539 | } | |
| 540 | ||
| API: notifications over REST and MCP, with notifications scopes | 541 | /// What to ask the repos service to see what this pull request changes. |
| 542 | /// | |
| 543 | /// A fork is compared as a whole. A branch is compared by name while | |
| 544 | /// the pull request is open, and by the commit it was merged or closed | |
| 545 | /// at afterwards, so later pushes to the branch do not change the record. | |
| 546 | pub fn comparison(&self, viewer: &Viewer) -> CompareArgs { | |
| 547 | let settled = matches!(self.status, PullStatus::Merged | PullStatus::Closed); | |
| 548 | let (repo_id, head) = match &self.fork_repo_id { | |
| 549 | Some(fork) => (fork.clone(), None), | |
| 550 | None => ( | |
| 551 | self.repo_id.clone(), | |
| 552 | self.head_commit | |
| 553 | .clone() | |
| 554 | .filter(|_| settled) | |
| 555 | .or_else(|| self.branch.clone()), | |
| 556 | ), | |
| 557 | }; | |
| 558 | CompareArgs { | |
| 559 | repo_id, | |
| 560 | viewer: viewer.clone(), | |
| 561 | base: self.merge_base.clone(), | |
| 562 | head, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 563 | base_branch: self.base.clone().filter(|base| !base.is_empty()), |
| Merge branch 'worktree-agent-a432c4ed9deb834b1' | 564 | paths: None, |
| API: notifications over REST and MCP, with notifications scopes | 565 | } |
| 566 | } | |
| 567 | } | |
| 568 | ||
| 569 | /// g1t's agent, as the author of what it opens: stored by | |
| 570 | /// [`AGENT_ID`](crate::identity::AGENT_ID), shown as `g1t`. | |
| 571 | pub fn g1t_author() -> User { | |
| 572 | User { | |
| 573 | id: crate::identity::AGENT_ID.to_owned(), | |
| 574 | username: crate::identity::AGENT_NAME.to_owned(), | |
| 575 | kind: crate::PrincipalKind::Agent, | |
| 576 | ..User::default() | |
| 577 | } | |
| 578 | } | |
| 579 | ||
| 580 | /// Who is recorded as opening an issue or a pull request that `actor` | |
| 581 | /// opens, and who asked for it: `(author, requested_by)`. | |
| 582 | /// | |
| 583 | /// - g1t's agent, acting for someone through its token (an issue it files | |
| 584 | /// while at work): g1t, requested by that person. | |
| 585 | /// - A person having g1t make the change (`by_g1t`: a hosted g1t agent in | |
| 586 | /// a fork of its own): g1t, requested by them. | |
| 587 | /// - Anyone else, and g1t's own work that nobody asked for: the actor, and | |
| 588 | /// nobody asking. | |
| 589 | pub fn authorship(actor: &User, by_g1t: bool) -> (User, Option<User>) { | |
| 590 | let person = |id: &str, username: &str| User { | |
| 591 | id: id.to_owned(), | |
| 592 | username: username.to_owned(), | |
| 593 | kind: crate::PrincipalKind::User, | |
| 594 | ..User::default() | |
| 595 | }; | |
| 596 | if actor.kind == crate::PrincipalKind::Agent { | |
| 597 | let asked = actor | |
| 598 | .acting | |
| 599 | .as_ref() | |
| 600 | .map(|acting| &acting.on_behalf_of) | |
| 601 | .filter(|on_behalf_of| !crate::system::is_system_id(&on_behalf_of.id)) | |
| 602 | .map(|on_behalf_of| person(&on_behalf_of.id, &on_behalf_of.username)); | |
| 603 | return (g1t_author(), asked); | |
| 604 | } | |
| 605 | if by_g1t && actor.kind != crate::PrincipalKind::System { | |
| 606 | return (g1t_author(), Some(person(&actor.id, &actor.username))); | |
| 607 | } | |
| 608 | let author = User { | |
| 609 | id: actor.id.clone(), | |
| 610 | username: actor.username.clone(), | |
| 611 | kind: actor.kind, | |
| 612 | ..User::default() | |
| 613 | }; | |
| 614 | (author, None) | |
| 615 | } | |
| 616 | ||
| 617 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 618 | #[serde(rename_all = "lowercase")] | |
| 619 | pub enum CheckStatus { | |
| 620 | /// Waiting for a sandbox. | |
| 621 | Queued, | |
| 622 | Running, | |
| 623 | Passed, | |
| 624 | Failed, | |
| 625 | /// The checks could not be run at all. | |
| 626 | Errored, | |
| 627 | } | |
| 628 | ||
| 629 | impl CheckStatus { | |
| 630 | pub fn as_str(self) -> &'static str { | |
| 631 | match self { | |
| 632 | CheckStatus::Queued => "queued", | |
| 633 | CheckStatus::Running => "running", | |
| 634 | CheckStatus::Passed => "passed", | |
| 635 | CheckStatus::Failed => "failed", | |
| 636 | CheckStatus::Errored => "errored", | |
| 637 | } | |
| 638 | } | |
| 639 | } | |
| 640 | ||
| 641 | /// How one command went. Recorded by earlier runs of commands written on | |
| 642 | /// issues, which g1t no longer runs; kept so their history still reads. | |
| 643 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 644 | #[serde(rename_all = "camelCase")] | |
| 645 | pub struct CheckResult { | |
| 646 | pub command: String, | |
| 647 | pub passed: bool, | |
| 648 | /// Absent when the command was stopped for taking too long. | |
| 649 | #[serde(default)] | |
| 650 | pub exit_code: Option<i32>, | |
| 651 | /// What the command printed, standard output and error together. The | |
| 652 | /// end of it, when there was a lot. | |
| 653 | #[serde(default)] | |
| 654 | pub output: String, | |
| 655 | #[serde(default)] | |
| 656 | pub duration_ms: u64, | |
| 657 | } | |
| 658 | ||
| 659 | /// A record against a pull request's head: the merge queue taking it out, | |
| 660 | /// with why, or an earlier run of commands written on its issue. | |
| 661 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 662 | #[serde(rename_all = "camelCase")] | |
| 663 | pub struct CheckRun { | |
| 664 | pub id: String, | |
| 665 | /// The commit that was checked. | |
| 666 | pub head_commit: String, | |
| 667 | pub status: CheckStatus, | |
| 668 | pub results: Vec<CheckResult>, | |
| 669 | /// Why the checks could not be run, when `status` is `errored`. | |
| 670 | pub error: Option<String>, | |
| 671 | /// RFC 3339. | |
| 672 | pub created_at: String, | |
| 673 | /// RFC 3339. | |
| 674 | pub finished_at: Option<String>, | |
| 675 | } | |
| 676 | ||
| 677 | /// A reviewer's decision on a pull request. | |
| 678 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 679 | #[serde(rename_all = "snake_case")] | |
| 680 | pub enum Verdict { | |
| 681 | Approve, | |
| 682 | RequestChanges, | |
| 683 | } | |
| 684 | ||
| 685 | impl Verdict { | |
| 686 | pub fn as_str(self) -> &'static str { | |
| 687 | match self { | |
| 688 | Verdict::Approve => "approve", | |
| 689 | Verdict::RequestChanges => "request_changes", | |
| 690 | } | |
| 691 | } | |
| 692 | } | |
| 693 | ||
| 694 | /// What an entry in a conversation is. | |
| 695 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 696 | #[serde(rename_all = "lowercase")] | |
| 697 | pub enum CommentKind { | |
| 698 | /// Something a person or an agent wrote. | |
| 699 | #[default] | |
| 700 | Comment, | |
| 701 | /// Something that happened: an assignment, a review asked for, a close. | |
| 702 | Event, | |
| 703 | } | |
| 704 | ||
| 705 | /// A comment on an issue or a pull request. On a pull request it can sit | |
| 706 | /// on one line of the change, and it can carry a reviewer's verdict. | |
| 707 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 708 | #[serde(rename_all = "camelCase")] | |
| 709 | pub struct Comment { | |
| 710 | pub id: String, | |
| 711 | /// Something a person wrote, or something that happened. | |
| 712 | #[serde(default)] | |
| 713 | pub kind: CommentKind, | |
| 714 | pub author: User, | |
| 715 | /// Markdown. For an event, what its author did, as the rest of a | |
| 716 | /// sentence that starts with their name: "assigned ana". | |
| 717 | pub body: String, | |
| 718 | /// The file commented on, for a comment on a line. | |
| 719 | pub path: Option<String>, | |
| 720 | /// The line of that file, as numbered after the change. | |
| 721 | pub line: Option<u32>, | |
| 722 | pub verdict: Option<Verdict>, | |
| 723 | /// RFC 3339. | |
| 724 | pub created_at: String, | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 725 | /// When its text was last edited, RFC 3339; absent if it never was. |
| 726 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 727 | pub edited_at: Option<String>, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 728 | /// Set when one of the workspace's agents wrote it, as itself |
| 729 | /// (`author` is then the agent: its id, its handle as `username`, | |
| 730 | /// kind `agent`). Absent for people and for g1t. | |
| 731 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 732 | pub agent: Option<AgentRef>, | |
| 733 | /// Who the agent acted for: the person whose access it had, who | |
| 734 | /// answers for it as its author would. Set with `agent`. | |
| 735 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 736 | pub acting_for: Option<User>, | |
| 737 | /// An agent's review: shown with its verdict (or none, for a review | |
| 738 | /// that only comments) but advisory. It never counts toward required | |
| 739 | /// approvals or code owners, and never blocks a merge. | |
| 740 | #[serde(default, skip_serializing_if = "std::ops::Not::not")] | |
| 741 | pub advisory: bool, | |
| 742 | } | |
| 743 | ||
| 744 | impl Comment { | |
| 745 | /// The person who answers for it: whoever an agent acted for, or its author. | |
| 746 | pub fn answerable(&self) -> &User { | |
| 747 | self.acting_for.as_ref().unwrap_or(&self.author) | |
| 748 | } | |
| 749 | } | |
| 750 | ||
| 751 | /// One of a workspace's own agents, as a comment or review it wrote shows | |
| 752 | /// it: Margo (@margo), with the pixel face drawn from `avatar_seed`. Its | |
| 753 | /// page is `/<workspace>/-/agents/<handle>`. Read in camelCase, like the | |
| 754 | /// comment it sits on; the snake_case spelling is accepted too. | |
| 755 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 756 | #[serde(rename_all = "camelCase")] | |
| 757 | pub struct AgentRef { | |
| 758 | pub id: String, | |
| 759 | pub handle: String, | |
| 760 | #[serde(alias = "display_name")] | |
| 761 | pub display_name: String, | |
| 762 | #[serde(alias = "avatar_seed")] | |
| 763 | pub avatar_seed: String, | |
| 764 | } | |
| 765 | ||
| 766 | /// An agent's review: a comment, an approval or a request for changes, | |
| 767 | /// all of them advisory. | |
| 768 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 769 | #[serde(rename_all = "snake_case")] | |
| 770 | pub enum AgentVerdict { | |
| 771 | Comment, | |
| 772 | Approve, | |
| 773 | RequestChanges, | |
| 774 | } | |
| 775 | ||
| 776 | impl AgentVerdict { | |
| 777 | pub fn as_str(self) -> &'static str { | |
| 778 | match self { | |
| 779 | AgentVerdict::Comment => "comment", | |
| 780 | AgentVerdict::Approve => "approve", | |
| 781 | AgentVerdict::RequestChanges => "request_changes", | |
| 782 | } | |
| 783 | } | |
| 784 | ||
| 785 | pub fn parse(text: &str) -> Option<Self> { | |
| 786 | match text { | |
| 787 | "comment" => Some(AgentVerdict::Comment), | |
| 788 | "approve" => Some(AgentVerdict::Approve), | |
| 789 | "request_changes" => Some(AgentVerdict::RequestChanges), | |
| 790 | _ => None, | |
| 791 | } | |
| 792 | } | |
| 793 | ||
| 794 | /// As a person's verdict reads, for showing it; none for a comment. | |
| 795 | pub fn verdict(self) -> Option<Verdict> { | |
| 796 | match self { | |
| 797 | AgentVerdict::Comment => None, | |
| 798 | AgentVerdict::Approve => Some(Verdict::Approve), | |
| 799 | AgentVerdict::RequestChanges => Some(Verdict::RequestChanges), | |
| 800 | } | |
| 801 | } | |
| 802 | } | |
| 803 | ||
| API: notifications over REST and MCP, with notifications scopes | 804 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 805 | #[serde(rename_all = "snake_case")] | |
| 806 | pub enum SessionEntryKind { | |
| 807 | Prompt, | |
| 808 | Message, | |
| 809 | ToolCall, | |
| 810 | ToolResult, | |
| 811 | Note, | |
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 812 | /// A skill the session used: its name only. |
| 813 | Skill, | |
| API: notifications over REST and MCP, with notifications scopes | 814 | } |
| 815 | ||
| 816 | /// One step of an agent's session: the "why" behind a pull request's commits. | |
| 817 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 818 | pub struct SessionEntry { | |
| 819 | pub seq: u32, | |
| 820 | pub kind: SessionEntryKind, | |
| 821 | pub text: String, | |
| 822 | /// For tool calls and results. | |
| 823 | pub tool: Option<String>, | |
| 824 | /// The fork's head commit when this entry was recorded, if known. | |
| 825 | pub commit: Option<String>, | |
| 826 | /// RFC 3339. | |
| 827 | pub at: String, | |
| 828 | } | |
| 829 | ||
| 830 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 831 | pub struct NewSessionEntry { | |
| 832 | pub kind: SessionEntryKind, | |
| 833 | pub text: String, | |
| 834 | #[serde(default)] | |
| 835 | pub tool: Option<String>, | |
| 836 | #[serde(default)] | |
| 837 | pub commit: Option<String>, | |
| 838 | } | |
| 839 | ||
| 840 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 841 | pub struct IssueDetail { | |
| 842 | pub issue: Issue, | |
| 843 | /// Every pull request made against it, oldest first. | |
| 844 | pub pulls: Vec<Pull>, | |
| 845 | pub comments: Vec<Comment>, | |
| 846 | } | |
| 847 | ||
| 848 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 849 | pub struct PullDetail { | |
| 850 | pub pull: Pull, | |
| 851 | /// The issue it is for, if any. | |
| 852 | pub issue: Option<Issue>, | |
| 853 | pub comments: Vec<Comment>, | |
| 854 | /// The latest record against its head: the merge queue taking it out, | |
| 855 | /// or (from before checks were workflows) a run of its issue's commands. | |
| 856 | pub checks: Option<CheckRun>, | |
| 857 | /// Other pull requests in progress that change the same files. | |
| 858 | #[serde(default)] | |
| 859 | pub overlaps: Vec<Overlap>, | |
| 860 | /// Whether the branch it would merge into has moved on without it, so | |
| 861 | /// that it has to catch up before it can merge. | |
| 862 | #[serde(default)] | |
| 863 | pub behind: bool, | |
| 864 | /// Where it stands on its way to being merged, for a pull request g1t | |
| 865 | /// is seeing through. Absent on anyone else's. | |
| 866 | #[serde(default)] | |
| 867 | pub lifecycle: Option<Lifecycle>, | |
| 868 | /// A merge was asked for while it was behind: g1t is bringing it up to | |
| 869 | /// date and will then land it. | |
| 870 | #[serde(default)] | |
| 871 | pub landing: bool, | |
| 872 | /// Why g1t stopped working on it, if it did: a catch-up that could not | |
| 873 | /// be completed, for example. | |
| 874 | #[serde(default)] | |
| 875 | pub stalled: Option<String>, | |
| 876 | /// What workflow runs said about its head commit, one per workflow. | |
| 877 | #[serde(default)] | |
| 878 | pub statuses: Vec<CommitStatus>, | |
| 879 | /// Whether it merges cleanly into the branch it targets, worked out | |
| 880 | /// ahead of time whenever either side moves. | |
| 881 | #[serde(default)] | |
| 882 | pub mergeable: Mergeable, | |
| 883 | /// When `mergeable` is `conflicting`: the files that conflict. | |
| 884 | #[serde(default)] | |
| 885 | pub conflicts: Vec<String>, | |
| 886 | /// Earlier records like `checks`, newest first, without their output. | |
| 887 | #[serde(default)] | |
| 888 | pub earlier_checks: Vec<CheckRun>, | |
| 889 | /// The checks the default branch's protection requires, each as it | |
| 890 | /// stands on the head commit. Empty when none are required. | |
| 891 | #[serde(default, alias = "requiredChecks")] | |
| 892 | pub required_checks: Vec<RequiredCheck>, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 893 | /// The rules of the branch it merges into that it does not meet yet, |
| 894 | /// for whoever is looking. Absent while it is not open. | |
| 895 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 896 | pub rules: Option<crate::rules::MergeRules>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 897 | /// Who owns the files it changes, from the CODEOWNERS file of the |
| 898 | /// branch it merges into, and whose approval is still needed. Absent | |
| 899 | /// when that branch has no CODEOWNERS file. | |
| 900 | #[serde(default, alias = "codeOwners")] | |
| 901 | pub code_owners: Option<crate::codeowners::PullCodeOwners>, | |
| API: notifications over REST and MCP, with notifications scopes | 902 | } |
| 903 | ||
| 904 | /// Where a required check stands on a commit. | |
| 905 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 906 | #[serde(rename_all = "lowercase")] | |
| 907 | pub enum RequiredState { | |
| 908 | Success, | |
| 909 | Failure, | |
| 910 | /// Reported and still running. | |
| 911 | Pending, | |
| 912 | /// Nothing has reported it on this commit yet. | |
| 913 | Expected, | |
| 914 | } | |
| 915 | ||
| 916 | impl RequiredState { | |
| 917 | pub fn as_str(self) -> &'static str { | |
| 918 | match self { | |
| 919 | RequiredState::Success => "success", | |
| 920 | RequiredState::Failure => "failure", | |
| 921 | RequiredState::Pending => "pending", | |
| 922 | RequiredState::Expected => "expected", | |
| 923 | } | |
| 924 | } | |
| 925 | } | |
| 926 | ||
| 927 | /// One check a branch's protection requires, as it stands on a commit. | |
| 928 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 929 | #[serde(rename_all = "camelCase")] | |
| 930 | pub struct RequiredCheck { | |
| 931 | /// The check's name, such as `CI` or `g1t / deploy`. | |
| 932 | pub name: String, | |
| 933 | pub state: RequiredState, | |
| 934 | /// What the status that decided it says, if one did. | |
| 935 | #[serde(default)] | |
| 936 | pub description: Option<String>, | |
| 937 | /// Where to see more: the workflow run, for one a workflow reported. | |
| 938 | #[serde(default)] | |
| 939 | pub target_url: Option<String>, | |
| 940 | } | |
| 941 | ||
| 942 | /// The events a workflow status's context can end in: `CI / pull_request` | |
| 943 | /// is the `CI` check, reported by a run for a `pull_request` event. | |
| 944 | const STATUS_EVENTS: &[&str] = &[ | |
| 945 | "push", | |
| 946 | "pull_request", | |
| 947 | "pull_request_target", | |
| 948 | "pull_request_review", | |
| 949 | "merge_group", | |
| 950 | "workflow_dispatch", | |
| 951 | "workflow_run", | |
| 952 | "workflow_call", | |
| 953 | "schedule", | |
| 954 | "release", | |
| 955 | "issues", | |
| 956 | "issue_comment", | |
| 957 | "repository_dispatch", | |
| 958 | ]; | |
| 959 | ||
| 960 | /// A status context's check name and the event it was reported for: | |
| 961 | /// `CI / pull_request` is `("CI", Some("pull_request"))`. A context that | |
| 962 | /// does not end in an event, such as `g1t / deploy`, is its own name. | |
| 963 | pub fn check_name(context: &str) -> (&str, Option<&str>) { | |
| 964 | match context.rsplit_once(" / ") { | |
| 965 | Some((name, event)) if STATUS_EVENTS.contains(&event) && !name.trim().is_empty() => (name, Some(event)), | |
| 966 | _ => (context, None), | |
| 967 | } | |
| 968 | } | |
| 969 | ||
| 970 | /// Where each required check stands among a commit's statuses. A check is | |
| 971 | /// met by any status of that name, for any event: one that failed fails | |
| 972 | /// it, one still running holds it, and with neither, one that passed | |
| 973 | /// passes it. Names compare without regard to case. | |
| 974 | pub fn required_checks(required: &[String], statuses: &[CommitStatus]) -> Vec<RequiredCheck> { | |
| 975 | required | |
| 976 | .iter() | |
| 977 | .map(|name| { | |
| 978 | let matching: Vec<&CommitStatus> = statuses | |
| 979 | .iter() | |
| 980 | .filter(|status| check_name(&status.context).0.eq_ignore_ascii_case(name.trim())) | |
| 981 | .collect(); | |
| 982 | let failed = matching.iter().find(|s| s.state == "failure" || s.state == "error"); | |
| 983 | let pending = matching.iter().find(|s| s.state == "pending"); | |
| 984 | let passed = matching.iter().find(|s| s.state == "success"); | |
| 985 | let (state, decided) = match (failed, pending, passed) { | |
| 986 | (Some(status), _, _) => (RequiredState::Failure, Some(*status)), | |
| 987 | (None, Some(status), _) => (RequiredState::Pending, Some(*status)), | |
| 988 | (None, None, Some(status)) => (RequiredState::Success, Some(*status)), | |
| 989 | _ => (RequiredState::Expected, None), | |
| 990 | }; | |
| 991 | RequiredCheck { | |
| 992 | name: name.trim().to_owned(), | |
| 993 | state, | |
| 994 | description: decided.and_then(|status| status.description.clone()), | |
| 995 | target_url: decided.and_then(|status| status.target_url.clone()), | |
| 996 | } | |
| 997 | }) | |
| 998 | .collect() | |
| 999 | } | |
| 1000 | ||
| 1001 | /// A repository's required check names, tidied: trimmed, without blanks | |
| 1002 | /// or repeats (ignoring case), at most [`MAX_REQUIRED_CHECKS`]. | |
| 1003 | pub fn tidy_required(names: &[String]) -> Vec<String> { | |
| 1004 | let mut out: Vec<String> = Vec::new(); | |
| 1005 | for name in names { | |
| 1006 | let name: String = name.trim().chars().take(MAX_CHECK_NAME_CHARS).collect(); | |
| 1007 | if !name.is_empty() && !out.iter().any(|kept| kept.eq_ignore_ascii_case(&name)) { | |
| 1008 | out.push(name); | |
| 1009 | } | |
| 1010 | } | |
| 1011 | out.truncate(MAX_REQUIRED_CHECKS); | |
| 1012 | out | |
| 1013 | } | |
| 1014 | ||
| 1015 | /// The most checks a branch can require, and the longest name of one. | |
| 1016 | pub const MAX_REQUIRED_CHECKS: usize = 20; | |
| 1017 | pub const MAX_CHECK_NAME_CHARS: usize = 100; | |
| 1018 | ||
| 1019 | /// A check name seen on the repository's commits recently, for choosing | |
| 1020 | /// required checks: what reported it, and for which events. | |
| 1021 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 1022 | #[serde(rename_all = "camelCase")] | |
| 1023 | pub struct SeenCheck { | |
| 1024 | pub name: String, | |
| 1025 | /// The events it was reported for, such as `pull_request`; empty for a | |
| 1026 | /// status that names none, such as a deployment's. | |
| 1027 | pub events: Vec<String>, | |
| 1028 | /// RFC 3339. The latest report. | |
| 1029 | pub last_seen: String, | |
| 1030 | } | |
| 1031 | ||
| 1032 | /// `seen_checks`: the check names reported on a repository's commits in | |
| 1033 | /// the last 30 days, most recent first. Returns `Outcome<Vec<SeenCheck>>`. | |
| 1034 | #[derive(Debug, Serialize, Deserialize)] | |
| 1035 | pub struct SeenChecksArgs { | |
| 1036 | pub repo: RepoPath, | |
| 1037 | pub viewer: Viewer, | |
| 1038 | } | |
| 1039 | ||
| 1040 | /// The heading an issue's plain-words description of done goes under. | |
| 1041 | pub const DEFINITION_OF_DONE: &str = "## Definition of done"; | |
| 1042 | ||
| 1043 | /// An issue's body with `items` added as a bulleted "Definition of done" | |
| 1044 | /// section, for the agent and reviewers to read. Nothing is added when | |
| 1045 | /// `items` is empty, or when the body already has the section, so folding | |
| 1046 | /// the same items twice changes nothing. | |
| 1047 | pub fn with_definition_of_done(body: &str, items: &[String]) -> String { | |
| 1048 | let items: Vec<&str> = items.iter().map(|item| item.trim()).filter(|item| !item.is_empty()).collect(); | |
| 1049 | let body = body.trim(); | |
| 1050 | if items.is_empty() || body.contains(DEFINITION_OF_DONE) { | |
| 1051 | return body.to_owned(); | |
| 1052 | } | |
| 1053 | let list = items.iter().map(|item| format!("- {item}")).collect::<Vec<_>>().join("\n"); | |
| 1054 | if body.is_empty() { | |
| 1055 | format!("{DEFINITION_OF_DONE}\n\n{list}") | |
| 1056 | } else { | |
| 1057 | format!("{body}\n\n{DEFINITION_OF_DONE}\n\n{list}") | |
| 1058 | } | |
| 1059 | } | |
| 1060 | ||
| 1061 | /// Commands, as items of a definition of done: "`npm test` passes." | |
| 1062 | pub fn commands_pass(commands: &[String]) -> Vec<String> { | |
| 1063 | commands | |
| 1064 | .iter() | |
| 1065 | .map(|command| command.trim()) | |
| 1066 | .filter(|command| !command.is_empty()) | |
| 1067 | .map(|command| format!("`{}` passes.", command.replace('`', "'"))) | |
| 1068 | .collect() | |
| 1069 | } | |
| 1070 | ||
| 1071 | /// Whether a pull request's change merges cleanly into the branch it | |
| 1072 | /// targets. | |
| 1073 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 1074 | #[serde(rename_all = "lowercase")] | |
| 1075 | pub enum Mergeable { | |
| 1076 | /// It merges without conflicts. | |
| 1077 | Clean, | |
| 1078 | /// Some files conflict: see `PullDetail::conflicts`. | |
| 1079 | Conflicting, | |
| 1080 | /// Not known: never worked out, or it could not be. | |
| 1081 | #[default] | |
| 1082 | Unknown, | |
| 1083 | /// Being worked out now. | |
| 1084 | Checking, | |
| 1085 | } | |
| 1086 | ||
| 1087 | impl Mergeable { | |
| 1088 | pub fn as_str(self) -> &'static str { | |
| 1089 | match self { | |
| 1090 | Mergeable::Clean => "clean", | |
| 1091 | Mergeable::Conflicting => "conflicting", | |
| 1092 | Mergeable::Unknown => "unknown", | |
| 1093 | Mergeable::Checking => "checking", | |
| 1094 | } | |
| 1095 | } | |
| 1096 | ||
| 1097 | pub fn parse(value: Option<&str>) -> Mergeable { | |
| 1098 | match value { | |
| 1099 | Some("clean") => Mergeable::Clean, | |
| 1100 | Some("conflicting") => Mergeable::Conflicting, | |
| 1101 | Some("checking") => Mergeable::Checking, | |
| 1102 | _ => Mergeable::Unknown, | |
| 1103 | } | |
| 1104 | } | |
| 1105 | } | |
| 1106 | ||
| 1107 | /// `start_mergecheck`: claims the probe of whether a pull request merges | |
| 1108 | /// cleanly, which the work service asked for with a `pull.mergecheck` | |
| 1109 | /// event. Called by the runner service, which starts the sandbox. Refused | |
| 1110 | /// when it is no longer wanted, or when the repository already has as many | |
| 1111 | /// probes running as it may. Returns `Outcome<MergecheckJob>`. | |
| 1112 | #[derive(Debug, Serialize, Deserialize)] | |
| 1113 | #[serde(rename_all = "camelCase")] | |
| 1114 | pub struct StartMergecheckArgs { | |
| 1115 | pub pull_id: String, | |
| 1116 | } | |
| 1117 | ||
| 1118 | /// What a sandbox needs to find out whether a pull request merges cleanly. | |
| 1119 | #[derive(Debug, Serialize, Deserialize)] | |
| 1120 | #[serde(rename_all = "camelCase")] | |
| 1121 | pub struct MergecheckJob { | |
| 1122 | pub pull_id: String, | |
| 1123 | /// Lets the sandbox, and nothing else, report this probe. | |
| 1124 | pub token: String, | |
| 1125 | pub repo: RepoPath, | |
| 1126 | pub number: u32, | |
| 1127 | pub default_branch: String, | |
| 1128 | /// The default branch's commit to merge into. | |
| 1129 | pub base: String, | |
| 1130 | /// The repository holding the change: its fork, or the repository. | |
| 1131 | pub source: RepoPath, | |
| 1132 | /// The branch of `source` holding it. | |
| 1133 | pub branch: String, | |
| 1134 | /// The change's commit. | |
| 1135 | pub head: String, | |
| 1136 | /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for | |
| 1137 | /// it, or its author), and so can read its source. | |
| 1138 | pub author: User, | |
| 1139 | } | |
| 1140 | ||
| 1141 | /// `report_mergecheck`: what a sandbox found. Returns `Outcome<Mergeable>`. | |
| 1142 | #[derive(Debug, Serialize, Deserialize)] | |
| 1143 | #[serde(rename_all = "camelCase")] | |
| 1144 | pub struct ReportMergecheckArgs { | |
| 1145 | pub pull_id: String, | |
| 1146 | pub token: String, | |
| 1147 | /// The files that conflict; empty when it merges cleanly. | |
| 1148 | #[serde(default)] | |
| 1149 | pub conflicts: Vec<String>, | |
| 1150 | /// Why it could not be found out. | |
| 1151 | #[serde(default)] | |
| 1152 | pub error: Option<String>, | |
| 1153 | } | |
| 1154 | ||
| 1155 | /// What a workflow run (or another tool) says about a commit. | |
| Merge checks: statuses and check runs on every commit | 1156 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] |
| API: notifications over REST and MCP, with notifications scopes | 1157 | #[serde(rename_all = "camelCase")] |
| 1158 | pub struct CommitStatus { | |
| 1159 | /// What reported it, such as `CI / push`. | |
| 1160 | pub context: String, | |
| 1161 | /// `pending`, `success`, `failure` or `error`. | |
| 1162 | pub state: String, | |
| 1163 | pub description: Option<String>, | |
| 1164 | /// Where to see more, such as the run's page. | |
| 1165 | pub target_url: Option<String>, | |
| 1166 | pub updated_at: String, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 1167 | /// The integration that reported it (`actions`, `deployments`, |
| 1168 | /// `security`, `g1t`), when recorded. A required check can insist on | |
| 1169 | /// one (`rules::RequiredCheck::integration`). | |
| 1170 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 1171 | pub source: Option<String>, | |
| Merge checks: statuses and check runs on every commit | 1172 | /// Set on the status a check run stands as (see `checks`): the check |
| 1173 | /// run's id. Such a status is listed as its check run, not again. | |
| 1174 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 1175 | pub check_run_id: Option<String>, | |
| API: notifications over REST and MCP, with notifications scopes | 1176 | } |
| 1177 | ||
| 1178 | /// `set_commit_status`: for services only. Returns `Outcome<bool>`. | |
| 1179 | #[derive(Debug, Serialize, Deserialize)] | |
| 1180 | #[serde(rename_all = "camelCase")] | |
| 1181 | pub struct SetCommitStatusArgs { | |
| 1182 | pub repo_id: String, | |
| 1183 | pub sha: String, | |
| 1184 | pub context: String, | |
| 1185 | pub state: String, | |
| 1186 | #[serde(default)] | |
| 1187 | pub description: Option<String>, | |
| 1188 | #[serde(default)] | |
| 1189 | pub target_url: Option<String>, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 1190 | /// The integration reporting it: `actions`, `deployments`, `security` |
| 1191 | /// or `g1t`. | |
| 1192 | #[serde(default)] | |
| 1193 | pub source: Option<String>, | |
| API: notifications over REST and MCP, with notifications scopes | 1194 | } |
| 1195 | ||
| 1196 | /// A step on the way from an assigned issue to a pull request that is ready | |
| 1197 | /// to merge. g1t takes each one without being asked. | |
| 1198 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1199 | #[serde(rename_all = "snake_case")] | |
| 1200 | pub enum Stage { | |
| 1201 | /// The agent is making the change. | |
| 1202 | Working, | |
| 1203 | /// Waiting for the checks workflows report on its head. | |
| 1204 | Checking, | |
| 1205 | /// g1t is reviewing it. | |
| 1206 | Reviewing, | |
| 1207 | /// The agent is addressing failed checks or a review. | |
| 1208 | Revising, | |
| 1209 | /// The agent is merging in the branch it would land on, which moved. | |
| 1210 | CatchingUp, | |
| 1211 | /// Woken to answer a question another agent asked it, or a handoff. | |
| 1212 | Answering, | |
| 1213 | /// In the repository's merge queue, being tested with what is ahead of | |
| 1214 | /// it before it lands. | |
| 1215 | Queued, | |
| 1216 | /// Required checks passed, reviewed and approved, up to date. A | |
| 1217 | /// person merges. | |
| 1218 | Ready, | |
| 1219 | /// g1t has stopped and a person has to decide what happens next. | |
| 1220 | NeedsYou, | |
| 1221 | } | |
| 1222 | ||
| 1223 | /// Where a pull request made by a g1t agent stands. See [`Stage`]. | |
| 1224 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1225 | pub struct Lifecycle { | |
| 1226 | pub stage: Stage, | |
| 1227 | /// One sentence saying what is happening, or why it stopped. | |
| 1228 | pub detail: String, | |
| 1229 | /// How many times the agent has been sent back to revise it. | |
| 1230 | pub revisions: u32, | |
| 1231 | } | |
| 1232 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1233 | /// `advance`: merges a pull request g1t is seeing through once it is ready, |
| 1234 | /// where the repository's settings say ready pull requests land by | |
| 1235 | /// themselves. Called by the runner service. Returns `Advance`. | |
| API: notifications over REST and MCP, with notifications scopes | 1236 | #[derive(Debug, Serialize, Deserialize)] |
| 1237 | #[serde(rename_all = "camelCase")] | |
| 1238 | pub struct AdvanceArgs { | |
| 1239 | pub pull_id: String, | |
| 1240 | } | |
| 1241 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1242 | /// What the runner is to do next for a pull request: always nothing now, |
| 1243 | /// since no step of a pull request is carried out in a sandbox any more. | |
| API: notifications over REST and MCP, with notifications scopes | 1244 | #[derive(Debug, Serialize, Deserialize)] |
| 1245 | #[serde(tag = "action", rename_all = "snake_case")] | |
| 1246 | pub enum Advance { | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1247 | /// Nothing to do. |
| API: notifications over REST and MCP, with notifications scopes | 1248 | None, |
| 1249 | } | |
| 1250 | ||
| 1251 | /// How a repository wants its pull requests handled. A repository that has | |
| 1252 | /// changed nothing has the defaults. | |
| 1253 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1254 | #[serde(rename_all = "camelCase", default)] | |
| 1255 | pub struct RepoSettings { | |
| 1256 | /// Land a g1t agent's pull request without a person once it is ready: | |
| 1257 | /// required checks passed and approved as the settings below require. | |
| 1258 | pub auto_merge: bool, | |
| 1259 | /// The checks that must pass on a pull request's head before it may | |
| 1260 | /// merge into the default branch, by name: a workflow's name (`CI`), or | |
| 1261 | /// the context of another status (`g1t / deploy`). The same for a | |
| 1262 | /// person's pull request and an agent's, and for the merge queue. | |
| 1263 | pub required_checks: Vec<String>, | |
| 1264 | /// Refuse to merge a pull request that does not contain the default | |
| 1265 | /// branch's latest commits, so that what merges is what was checked. | |
| 1266 | /// When off, merging one that is behind brings it up to date first. | |
| 1267 | pub require_up_to_date: bool, | |
| 1268 | /// How many approving reviews a pull request needs before it may | |
| 1269 | /// merge. A reviewer who has since asked for changes blocks it. | |
| 1270 | pub required_approvals: u32, | |
| 1271 | /// Whether a g1t agent's approval counts towards `required_approvals`. | |
| 1272 | pub count_agent_approvals: bool, | |
| 1273 | /// Whether someone who may merge can bypass required checks that have | |
| 1274 | /// not passed, by saying so as they merge. | |
| 1275 | pub allow_ignoring_checks: bool, | |
| 1276 | /// Merge through a queue: pull requests are tested together with those | |
| 1277 | /// ahead of them, and only a combination that passed reaches the default | |
| 1278 | /// branch. | |
| 1279 | pub merge_queue: bool, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1280 | /// Refuse to merge until the code owners of every file it changes |
| 1281 | /// (its CODEOWNERS file) have approved, as many as each section asks. | |
| 1282 | /// Only people count, and `g1t` only where the file names `@g1t`. | |
| 1283 | pub require_code_owner_review: bool, | |
| API: notifications over REST and MCP, with notifications scopes | 1284 | /// Username of the member who last changed the settings, if anyone has. |
| 1285 | pub updated_by: Option<String>, | |
| 1286 | /// RFC 3339. | |
| 1287 | pub updated_at: Option<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1288 | } |
| 1289 | ||
| API: notifications over REST and MCP, with notifications scopes | 1290 | impl Default for RepoSettings { |
| 1291 | fn default() -> Self { | |
| 1292 | RepoSettings { | |
| 1293 | auto_merge: false, | |
| 1294 | required_checks: Vec::new(), | |
| 1295 | require_up_to_date: false, | |
| 1296 | required_approvals: 0, | |
| 1297 | count_agent_approvals: true, | |
| 1298 | allow_ignoring_checks: true, | |
| 1299 | merge_queue: false, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1300 | require_code_owner_review: false, |
| API: notifications over REST and MCP, with notifications scopes | 1301 | updated_by: None, |
| 1302 | updated_at: None, | |
| 1303 | } | |
| 1304 | } | |
| 1305 | } | |
| 1306 | ||
| 1307 | /// `update_settings`: replaces a repository's settings. Members of its | |
| 1308 | /// workspace only. Returns `Outcome<RepoSettings>`. `get_settings` takes | |
| 1309 | /// `ViewArgs` and returns the same. | |
| 1310 | #[derive(Debug, Serialize, Deserialize)] | |
| 1311 | pub struct UpdateSettingsArgs { | |
| 1312 | pub actor: User, | |
| 1313 | pub repo: RepoPath, | |
| 1314 | /// Who changed them and when are filled in by the service. | |
| 1315 | pub settings: RepoSettings, | |
| 1316 | } | |
| 1317 | ||
| 1318 | /// `stall`: records that a step could not be carried out, so that g1t | |
| 1319 | /// stops and a person is asked. Returns `bool`. | |
| 1320 | #[derive(Debug, Serialize, Deserialize)] | |
| 1321 | #[serde(rename_all = "camelCase")] | |
| 1322 | pub struct StallArgs { | |
| 1323 | pub pull_id: String, | |
| 1324 | pub reason: String, | |
| 1325 | /// The person who stopped it, by id, when someone did: they are not | |
| 1326 | /// told it needs them. | |
| 1327 | #[serde(default)] | |
| 1328 | pub by: Option<String>, | |
| 1329 | } | |
| 1330 | ||
| 1331 | /// `managed_pulls`: ids of the open pull requests g1t is seeing through, | |
| 1332 | /// in one repository or in all of them. Returns `Vec<String>`. | |
| 1333 | #[derive(Debug, Default, Serialize, Deserialize)] | |
| 1334 | #[serde(rename_all = "camelCase")] | |
| 1335 | pub struct ManagedPullsArgs { | |
| 1336 | #[serde(default)] | |
| 1337 | pub repo_id: Option<String>, | |
| 1338 | } | |
| 1339 | ||
| 1340 | /// `open_issue`. Returns `Outcome<Issue>`. | |
| 1341 | #[derive(Debug, Serialize, Deserialize)] | |
| 1342 | pub struct OpenIssueArgs { | |
| 1343 | pub actor: User, | |
| 1344 | pub repo: RepoPath, | |
| 1345 | pub title: String, | |
| 1346 | #[serde(default)] | |
| 1347 | pub body: String, | |
| 1348 | #[serde(default)] | |
| 1349 | pub labels: Vec<String>, | |
| 1350 | /// Deprecated: commands, added to the body under "Definition of done". | |
| 1351 | /// Checks are the workflows the branch's protection requires. | |
| 1352 | #[serde(default)] | |
| 1353 | pub checks: Vec<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1354 | /// The number of the milestone to put it in. Needs the Triage role. |
| 1355 | #[serde(default)] | |
| 1356 | pub milestone: Option<u32>, | |
| API: notifications over REST and MCP, with notifications scopes | 1357 | } |
| 1358 | ||
| 1359 | /// `list_issues`, newest first. Returns `Outcome<Vec<Issue>>`. | |
| 1360 | #[derive(Debug, Serialize, Deserialize)] | |
| 1361 | pub struct ListIssuesArgs { | |
| 1362 | pub repo: RepoPath, | |
| 1363 | pub viewer: Viewer, | |
| 1364 | #[serde(default)] | |
| 1365 | pub state: Option<State>, | |
| 1366 | /// Only issues carrying this label. | |
| 1367 | #[serde(default)] | |
| 1368 | pub label: Option<String>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1369 | /// Only issues in the milestone of this number. |
| 1370 | #[serde(default)] | |
| 1371 | pub milestone: Option<u32>, | |
| API: notifications over REST and MCP, with notifications scopes | 1372 | } |
| 1373 | ||
| 1374 | /// `list_pulls`, newest first. Returns `Outcome<Vec<Pull>>`. | |
| 1375 | #[derive(Debug, Serialize, Deserialize)] | |
| 1376 | pub struct ListPullsArgs { | |
| 1377 | pub repo: RepoPath, | |
| 1378 | pub viewer: Viewer, | |
| 1379 | #[serde(default)] | |
| 1380 | pub state: Option<State>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1381 | /// Only pull requests carrying this label. |
| 1382 | #[serde(default)] | |
| 1383 | pub label: Option<String>, | |
| 1384 | /// Only pull requests in the milestone of this number. | |
| 1385 | #[serde(default)] | |
| 1386 | pub milestone: Option<u32>, | |
| 1387 | /// Only pull requests into this branch. | |
| 1388 | #[serde(default)] | |
| 1389 | pub base: Option<String>, | |
| API: notifications over REST and MCP, with notifications scopes | 1390 | } |
| 1391 | ||
| 1392 | /// `pulls_for_repos`: the newest open and the newest closed pull requests | |
| 1393 | /// of many repositories, in one call, for pages that show several projects | |
| 1394 | /// at once. Repositories the viewer cannot read are left out, as are forks | |
| 1395 | /// (ask those with `list_pulls`). Returns `Vec<RepoPulls>`. | |
| 1396 | #[derive(Debug, Serialize, Deserialize)] | |
| 1397 | #[serde(rename_all = "camelCase")] | |
| 1398 | pub struct PullsForReposArgs { | |
| 1399 | /// At most [`MAX_PULLS_FOR_REPOS`] are looked at. | |
| 1400 | pub repo_ids: Vec<String>, | |
| 1401 | pub viewer: Viewer, | |
| 1402 | /// How many of each, open and closed, per repository (at most 100). | |
| 1403 | pub limit: u32, | |
| 1404 | } | |
| 1405 | ||
| 1406 | /// The most repositories one `pulls_for_repos` call looks at. | |
| 1407 | pub const MAX_PULLS_FOR_REPOS: usize = 50; | |
| 1408 | ||
| 1409 | /// One repository's pull requests from `pulls_for_repos`, newest first. | |
| 1410 | #[derive(Debug, Serialize, Deserialize)] | |
| 1411 | #[serde(rename_all = "camelCase")] | |
| 1412 | pub struct RepoPulls { | |
| 1413 | pub repo_id: String, | |
| 1414 | /// Draft and open. | |
| 1415 | pub open: Vec<Pull>, | |
| 1416 | /// Merged and closed. | |
| 1417 | pub closed: Vec<Pull>, | |
| 1418 | } | |
| 1419 | ||
| 1420 | /// `get_issue` (`Outcome<IssueDetail>`), `get_pull` (`Outcome<PullDetail>`), | |
| 1421 | /// `read_session` (`Outcome<Vec<SessionEntry>>`), `list_labels` | |
| 1422 | /// (`Outcome<Vec<String>>`) and `counts` (`Outcome<Counts>`). The last two | |
| 1423 | /// ignore `number`. | |
| 1424 | #[derive(Debug, Serialize, Deserialize)] | |
| 1425 | #[serde(rename_all = "camelCase")] | |
| 1426 | pub struct ViewArgs { | |
| 1427 | pub repo: RepoPath, | |
| 1428 | #[serde(default)] | |
| 1429 | pub number: u32, | |
| 1430 | pub viewer: Viewer, | |
| 1431 | /// For `read_session`: only entries after this sequence number. | |
| 1432 | #[serde(default)] | |
| 1433 | pub after_seq: u32, | |
| 1434 | } | |
| 1435 | ||
| 1436 | /// How many issues and pull requests are open on a repository. | |
| 1437 | #[derive(Debug, Serialize, Deserialize)] | |
| 1438 | pub struct Counts { | |
| 1439 | pub issues: u32, | |
| 1440 | pub pulls: u32, | |
| 1441 | } | |
| 1442 | ||
| 1443 | /// `update_issue`: changes whichever fields are given. The author or a | |
| 1444 | /// member of the workspace may. Returns `Outcome<Issue>`. | |
| 1445 | #[derive(Debug, Serialize, Deserialize)] | |
| 1446 | pub struct UpdateIssueArgs { | |
| 1447 | pub actor: User, | |
| 1448 | pub repo: RepoPath, | |
| 1449 | pub number: u32, | |
| 1450 | #[serde(default)] | |
| 1451 | pub title: Option<String>, | |
| 1452 | #[serde(default)] | |
| 1453 | pub body: Option<String>, | |
| 1454 | #[serde(default)] | |
| 1455 | pub labels: Option<Vec<String>>, | |
| 1456 | /// Usernames of the people it is assigned to; replaces the whole set. | |
| 1457 | #[serde(default)] | |
| 1458 | pub assignees: Option<Vec<String>>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1459 | /// The number of the milestone to put it in; 0 takes it out of its |
| 1460 | /// milestone. Needs the Triage role. | |
| 1461 | #[serde(default)] | |
| 1462 | pub milestone: Option<u32>, | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 1463 | /// The numbers of the issues of the same repository that have to land |
| 1464 | /// first; replaces the whole set. Refused when one is not an issue, is | |
| 1465 | /// the issue itself, or would make a cycle. | |
| 1466 | #[serde(default)] | |
| 1467 | pub blocked_by: Option<Vec<u32>>, | |
| API: notifications over REST and MCP, with notifications scopes | 1468 | } |
| 1469 | ||
| 1470 | /// `close_issue` and `reopen_issue`. Each returns `Outcome<Issue>`. | |
| 1471 | #[derive(Debug, Serialize, Deserialize)] | |
| 1472 | pub struct IssueActionArgs { | |
| 1473 | pub actor: User, | |
| 1474 | pub repo: RepoPath, | |
| 1475 | pub number: u32, | |
| 1476 | /// For `close_issue`; `completed` if left out. | |
| 1477 | #[serde(default)] | |
| 1478 | pub reason: Option<IssueReason>, | |
| 1479 | } | |
| 1480 | ||
| 1481 | /// `add_comment`, on an issue or a pull request. On a pull request it may | |
| 1482 | /// name a line of the change, and may carry a verdict; nobody can give a | |
| 1483 | /// verdict on their own pull request. Returns `Outcome<Comment>`. | |
| 1484 | #[derive(Debug, Serialize, Deserialize)] | |
| 1485 | pub struct AddCommentArgs { | |
| 1486 | pub actor: User, | |
| 1487 | pub repo: RepoPath, | |
| 1488 | pub number: u32, | |
| 1489 | /// May be empty when approving. | |
| 1490 | #[serde(default)] | |
| 1491 | pub body: String, | |
| 1492 | #[serde(default)] | |
| 1493 | pub path: Option<String>, | |
| 1494 | #[serde(default)] | |
| 1495 | pub line: Option<u32>, | |
| 1496 | #[serde(default)] | |
| 1497 | pub verdict: Option<Verdict>, | |
| 1498 | } | |
| 1499 | ||
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1500 | /// `edit_comment` and `delete_comment`: change the text of a comment on an |
| 1501 | /// issue or a pull request, or delete it. Its author may, and so may anyone | |
| 1502 | /// with the Maintain role or higher ([`may_change_comment`]). Notes of what | |
| 1503 | /// happened (`CommentKind::Event`) cannot be changed, and a review that | |
| 1504 | /// gave a verdict cannot be deleted, only edited. `edit_comment` returns | |
| 1505 | /// `Outcome<Comment>`; `delete_comment` returns `Outcome<()>`. | |
| 1506 | #[derive(Debug, Serialize, Deserialize)] | |
| 1507 | #[serde(rename_all = "camelCase")] | |
| 1508 | pub struct CommentActionArgs { | |
| 1509 | pub actor: User, | |
| 1510 | pub repo: RepoPath, | |
| 1511 | pub comment_id: String, | |
| 1512 | /// For `edit_comment`: the new text, in Markdown. | |
| 1513 | #[serde(default)] | |
| 1514 | pub body: String, | |
| 1515 | } | |
| 1516 | ||
| 1517 | /// Why `actor_id` may not edit (or, with `deleting`, delete) a comment by | |
| 1518 | /// `author_id`, or none when they may: its author may, and so may someone | |
| 1519 | /// who `maintains` the repository (the Maintain role or higher). A note of | |
| 1520 | /// something that happened is never changed, and a review's verdict stays, | |
| 1521 | /// so a comment that gave one can be edited but not deleted. | |
| 1522 | pub fn may_change_comment( | |
| 1523 | kind: CommentKind, | |
| 1524 | has_verdict: bool, | |
| 1525 | author_id: &str, | |
| 1526 | actor_id: &str, | |
| 1527 | maintains: bool, | |
| 1528 | deleting: bool, | |
| 1529 | ) -> Result<(), &'static str> { | |
| 1530 | if kind == CommentKind::Event { | |
| 1531 | return Err("That is a record of something that happened, not a comment; it cannot be changed."); | |
| 1532 | } | |
| 1533 | if author_id != actor_id && !maintains { | |
| 1534 | return Err(if deleting { | |
| 1535 | "Only its author, or someone with the Maintain role or higher, can delete this comment." | |
| 1536 | } else { | |
| 1537 | "Only its author, or someone with the Maintain role or higher, can edit this comment." | |
| 1538 | }); | |
| 1539 | } | |
| 1540 | if deleting && has_verdict { | |
| 1541 | return Err("A review cannot be deleted. Edit its text instead."); | |
| 1542 | } | |
| 1543 | Ok(()) | |
| 1544 | } | |
| 1545 | ||
| API: notifications over REST and MCP, with notifications scopes | 1546 | /// `open_pull`. Without `branch`, forks the repo and returns a draft pull |
| 1547 | /// request to push to. With it, opens a pull request, ready for review, | |
| 1548 | /// for a branch already pushed to the repo. Returns `Outcome<Pull>`. | |
| 1549 | #[derive(Debug, Serialize, Deserialize)] | |
| 1550 | pub struct OpenPullArgs { | |
| 1551 | pub actor: User, | |
| 1552 | pub repo: RepoPath, | |
| 1553 | /// The issue this is for. | |
| 1554 | #[serde(default)] | |
| 1555 | pub issue: Option<u32>, | |
| 1556 | /// Defaults to the issue's title; required without an issue. | |
| 1557 | #[serde(default)] | |
| 1558 | pub title: String, | |
| 1559 | /// What changed and why. Usually set later, when a draft is marked ready. | |
| 1560 | #[serde(default)] | |
| 1561 | pub body: String, | |
| 1562 | /// A branch of the repository that already holds the change. | |
| 1563 | #[serde(default)] | |
| 1564 | pub branch: Option<String>, | |
| 1565 | #[serde(default)] | |
| 1566 | pub agent: String, | |
| 1567 | pub runtime: Runtime, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1568 | /// The branch to merge into: the default branch when absent. |
| 1569 | #[serde(default)] | |
| 1570 | pub base: Option<String>, | |
| Open a pull request as a draft: it can't merge, and agents' review routines wait, until it is marked ready | 1571 | /// Opened from a branch as a draft, still being worked on: it can't |
| 1572 | /// merge, and agents' review routines wait, until it is marked ready. | |
| 1573 | #[serde(default)] | |
| 1574 | pub draft: bool, | |
| API: notifications over REST and MCP, with notifications scopes | 1575 | } |
| 1576 | ||
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1577 | /// `ready_pull`, `close_pull`, `reopen_pull`, `convert_pull_to_draft` and |
| 1578 | /// `merge_pull`. Each returns `Outcome<Pull>`. `reopen_pull` opens a closed | |
| 1579 | /// pull request again (never a merged one), as the draft it was if it was | |
| 1580 | /// closed as one; `convert_pull_to_draft` turns one that is open back into | |
| 1581 | /// a draft, which leaves the merge queue. | |
| API: notifications over REST and MCP, with notifications scopes | 1582 | /// |
| 1583 | /// Also `catch_up_pull`: brings the pull request up to date with the | |
| 1584 | /// default branch without a sandbox where that is safe, as the repos | |
| 1585 | /// service's `update_pull_branch` does, after checking that `actor` may | |
| 1586 | /// update it: its owner ([`Pull::owner`]) for a fork, any member for a | |
| 1587 | /// branch. | |
| 1588 | /// Returns `Outcome<repos::PullBranchUpdate>`; on `needs_agent` nothing was | |
| 1589 | /// pushed and the runner's `update` is the way on. | |
| 1590 | #[derive(Debug, Serialize, Deserialize)] | |
| 1591 | #[serde(rename_all = "camelCase")] | |
| 1592 | pub struct PullActionArgs { | |
| 1593 | pub actor: User, | |
| 1594 | pub repo: RepoPath, | |
| 1595 | pub number: u32, | |
| 1596 | /// For `ready_pull`: what changed and why. | |
| 1597 | #[serde(default)] | |
| 1598 | pub summary: String, | |
| 1599 | /// For `merge_pull`: leave the issue open and the other pull requests | |
| 1600 | /// for it untouched, because this one is only part of the work. | |
| 1601 | #[serde(default)] | |
| 1602 | pub keep_issue_open: bool, | |
| 1603 | /// For `merge_pull`: merge although required checks have not passed, | |
| 1604 | /// where the repository lets members bypass them. | |
| 1605 | #[serde(default)] | |
| 1606 | pub ignore_checks: bool, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 1607 | /// For `merge_pull`: merge although rules are not met, where a ruleset |
| 1608 | /// lists the actor as one who may bypass it. Recorded as a bypass. | |
| 1609 | #[serde(default)] | |
| 1610 | pub bypass_rules: bool, | |
| API: notifications over REST and MCP, with notifications scopes | 1611 | } |
| 1612 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1613 | /// `update_pull`: changes who a pull request is assigned to, whose review |
| 1614 | /// is asked for, its labels, its milestone and the branch it merges into. | |
| 1615 | /// Each list given replaces the whole set. Whoever opened it, or someone | |
| 1616 | /// with the Triage role, may; changing the base needs the Write role. | |
| 1617 | /// Returns `Outcome<Pull>`. | |
| API: notifications over REST and MCP, with notifications scopes | 1618 | #[derive(Debug, Serialize, Deserialize)] |
| 1619 | pub struct UpdatePullArgs { | |
| 1620 | pub actor: User, | |
| 1621 | pub repo: RepoPath, | |
| 1622 | pub number: u32, | |
| 1623 | #[serde(default)] | |
| 1624 | pub assignees: Option<Vec<String>>, | |
| 1625 | /// May include `g1t`. Asking for its review does not by itself | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1626 | /// start one; the runner's `review` does. A team is named |
| 1627 | /// `workspace/team` (or `@workspace/team`): the whole list, people and | |
| 1628 | /// teams, replaces who is asked. | |
| API: notifications over REST and MCP, with notifications scopes | 1629 | #[serde(default)] |
| 1630 | pub reviewers: Option<Vec<String>>, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1631 | /// Its labels; replaces the whole set, as `set_labels` does. |
| 1632 | #[serde(default)] | |
| 1633 | pub labels: Option<Vec<String>>, | |
| 1634 | /// The number of the milestone to put it in; 0 takes it out. Needs the | |
| 1635 | /// Triage role. | |
| 1636 | #[serde(default)] | |
| 1637 | pub milestone: Option<u32>, | |
| 1638 | /// The branch it merges into: an existing branch other than its own. | |
| 1639 | /// Needs the Write role. | |
| 1640 | #[serde(default)] | |
| 1641 | pub base: Option<String>, | |
| API: notifications over REST and MCP, with notifications scopes | 1642 | } |
| 1643 | ||
| 1644 | /// `start_checks`: always refused now; a pull request's checks are the | |
| 1645 | /// workflows run on it. Kept so that a runner from before is answered. | |
| 1646 | /// Returns `Outcome<CheckJob>`. | |
| 1647 | #[derive(Debug, Serialize, Deserialize)] | |
| 1648 | #[serde(rename_all = "camelCase")] | |
| 1649 | pub struct StartChecksArgs { | |
| 1650 | pub pull_id: String, | |
| 1651 | } | |
| 1652 | ||
| 1653 | /// What a sandbox needs to carry out a check run. | |
| 1654 | #[derive(Debug, Serialize, Deserialize)] | |
| 1655 | #[serde(rename_all = "camelCase")] | |
| 1656 | pub struct CheckJob { | |
| 1657 | pub run_id: String, | |
| 1658 | /// Lets the sandbox, and nothing else, report this run's results. | |
| 1659 | pub token: String, | |
| 1660 | pub commands: Vec<String>, | |
| 1661 | /// The repository holding the commit: the fork, or the repository itself. | |
| 1662 | pub source: RepoPath, | |
| 1663 | pub commit: String, | |
| 1664 | /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for | |
| 1665 | /// it, or its author), and so can read its source. | |
| 1666 | pub author: User, | |
| 1667 | /// Username of whoever wrote the checks: the issue's author. | |
| 1668 | pub requested_by: String, | |
| 1669 | pub repo: RepoPath, | |
| 1670 | pub number: u32, | |
| 1671 | } | |
| 1672 | ||
| 1673 | /// `report_checks`: what a sandbox says about its run. With no results and | |
| 1674 | /// no error it has started. `skip` forgets the run, for one that will not | |
| 1675 | /// be carried out. Returns `Outcome<CheckRun>`. | |
| 1676 | #[derive(Debug, Serialize, Deserialize)] | |
| 1677 | #[serde(rename_all = "camelCase")] | |
| 1678 | pub struct ReportChecksArgs { | |
| 1679 | pub run_id: String, | |
| 1680 | pub token: String, | |
| 1681 | #[serde(default)] | |
| 1682 | pub results: Vec<CheckResult>, | |
| 1683 | #[serde(default)] | |
| 1684 | pub error: Option<String>, | |
| 1685 | #[serde(default)] | |
| 1686 | pub skip: bool, | |
| 1687 | } | |
| 1688 | ||
| 1689 | /// A pull request in progress, with where it lives. | |
| 1690 | #[derive(Debug, Serialize, Deserialize)] | |
| 1691 | pub struct ActivePull { | |
| 1692 | pub pull: Pull, | |
| 1693 | pub issue: Option<Issue>, | |
| 1694 | /// Where it stands, for one g1t is seeing through. | |
| 1695 | #[serde(default)] | |
| 1696 | pub lifecycle: Option<Lifecycle>, | |
| 1697 | } | |
| 1698 | ||
| 1699 | /// `list_active_pulls`: drafts and open pull requests the viewer started, | |
| 1700 | /// most recently active first. Returns `Vec<ActivePull>`. Also | |
| 1701 | /// `list_assigned_issues`: open issues assigned to the viewer, most | |
| 1702 | /// recently changed first. Returns `Vec<Issue>`. | |
| 1703 | #[derive(Debug, Serialize, Deserialize)] | |
| 1704 | pub struct ViewerArgs { | |
| 1705 | pub viewer: Viewer, | |
| 1706 | } | |
| 1707 | ||
| 1708 | /// `append_session`. Returns `Outcome<Appended>`. | |
| 1709 | #[derive(Debug, Serialize, Deserialize)] | |
| 1710 | pub struct AppendSessionArgs { | |
| 1711 | pub actor: User, | |
| 1712 | pub repo: RepoPath, | |
| 1713 | pub number: u32, | |
| 1714 | pub entries: Vec<NewSessionEntry>, | |
| 1715 | } | |
| 1716 | ||
| 1717 | #[derive(Debug, Serialize, Deserialize)] | |
| 1718 | pub struct Appended { | |
| 1719 | pub count: u32, | |
| 1720 | } | |
| 1721 | ||
| 1722 | /// Lowercases, trims and de-duplicates labels, dropping empty ones. | |
| 1723 | /// Returns `None` if there are too many or one is too long. | |
| 1724 | pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> { | |
| 1725 | let mut normalized: Vec<String> = Vec::new(); | |
| 1726 | for label in labels { | |
| 1727 | let label = label | |
| 1728 | .split_whitespace() | |
| 1729 | .collect::<Vec<_>>() | |
| 1730 | .join(" ") | |
| 1731 | .to_lowercase(); | |
| 1732 | if label.is_empty() || normalized.contains(&label) { | |
| 1733 | continue; | |
| 1734 | } | |
| 1735 | if label.chars().count() > MAX_LABEL_CHARS { | |
| 1736 | return None; | |
| 1737 | } | |
| 1738 | normalized.push(label); | |
| 1739 | } | |
| 1740 | (normalized.len() <= MAX_LABELS).then_some(normalized) | |
| 1741 | } | |
| 1742 | ||
| 1743 | #[cfg(test)] | |
| 1744 | mod tests { | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1745 | use super::*; |
| API: notifications over REST and MCP, with notifications scopes | 1746 | |
| 1747 | fn labels(names: &[&str]) -> Vec<String> { | |
| 1748 | names.iter().map(|name| (*name).to_owned()).collect() | |
| 1749 | } | |
| 1750 | ||
| 1751 | #[test] | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1752 | fn colors_and_due_dates_are_tidied() { |
| 1753 | assert_eq!(tidy_color("#A1B2C3").as_deref(), Some("a1b2c3")); | |
| 1754 | assert_eq!(tidy_color("fc0").as_deref(), Some("ffcc00")); | |
| 1755 | assert_eq!(tidy_color("red"), None); | |
| 1756 | assert_eq!(tidy_color("12345"), None); | |
| 1757 | assert_eq!(label_color_for("bug"), "d73a4a"); | |
| 1758 | assert_eq!(label_color_for("area: web"), label_color_for("area: web")); | |
| 1759 | assert_eq!(tidy_due_on("2026-10-14").as_deref(), Some("2026-10-14")); | |
| 1760 | assert_eq!(tidy_due_on("2026-10-14T00:00:00Z").as_deref(), Some("2026-10-14")); | |
| 1761 | assert_eq!(tidy_due_on("2026-13-01"), None); | |
| 1762 | assert_eq!(tidy_due_on("soon"), None); | |
| 1763 | } | |
| 1764 | ||
| 1765 | #[test] | |
| 1766 | fn a_milestone_is_as_far_along_as_its_closed_items() { | |
| 1767 | let mut milestone: Milestone = serde_json::from_value(serde_json::json!({ | |
| 1768 | "number": 1, "title": "Launch", "state": "open", "createdAt": "", "updatedAt": "" | |
| 1769 | })) | |
| 1770 | .unwrap(); | |
| 1771 | assert_eq!(milestone.percent(), 0); | |
| 1772 | milestone.open_items = 3; | |
| 1773 | milestone.closed_items = 1; | |
| 1774 | assert_eq!(milestone.percent(), 25); | |
| 1775 | } | |
| 1776 | ||
| 1777 | #[test] | |
| API: notifications over REST and MCP, with notifications scopes | 1778 | fn labels_are_lowercased_trimmed_and_unique() { |
| 1779 | assert_eq!( | |
| 1780 | normalize_labels(&labels(&[" Bug ", "bug", "", "Good First Issue"])), | |
| 1781 | Some(labels(&["bug", "good first issue"])) | |
| 1782 | ); | |
| 1783 | } | |
| 1784 | ||
| 1785 | #[test] | |
| 1786 | fn too_long_or_too_many_labels_are_refused() { | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1787 | assert_eq!(normalize_labels(&["x".repeat(51)]), None); |
| 1788 | assert!(normalize_labels(&["x".repeat(50)]).is_some()); | |
| 1789 | let many: Vec<String> = (0..21).map(|i| format!("label-{i}")).collect(); | |
| API: notifications over REST and MCP, with notifications scopes | 1790 | assert_eq!(normalize_labels(&many), None); |
| 1791 | } | |
| 1792 | } | |
| 1793 | ||
| 1794 | ||
| 1795 | // --- Merge queue ---------------------------------------------------------- | |
| 1796 | ||
| 1797 | /// Where a pull request in a merge queue stands. | |
| 1798 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1799 | #[serde(rename_all = "snake_case")] | |
| 1800 | pub enum QueueState { | |
| 1801 | /// Waiting for its turn to be tested. | |
| 1802 | Waiting, | |
| 1803 | /// Its combined state is being built and checked. | |
| 1804 | Testing, | |
| 1805 | /// Its combined state passed; it lands once everything ahead has. | |
| 1806 | Passed, | |
| 1807 | /// Its combined state failed, or would not merge. It left the queue. | |
| 1808 | Failed, | |
| 1809 | /// On the default branch. | |
| 1810 | Landed, | |
| 1811 | /// Taken out of the queue by a person, or closed. | |
| 1812 | Removed, | |
| 1813 | } | |
| 1814 | ||
| 1815 | impl QueueState { | |
| 1816 | pub fn as_str(self) -> &'static str { | |
| 1817 | match self { | |
| 1818 | QueueState::Waiting => "waiting", | |
| 1819 | QueueState::Testing => "testing", | |
| 1820 | QueueState::Passed => "passed", | |
| 1821 | QueueState::Failed => "failed", | |
| 1822 | QueueState::Landed => "landed", | |
| 1823 | QueueState::Removed => "removed", | |
| 1824 | } | |
| 1825 | } | |
| 1826 | ||
| 1827 | /// Still in the queue. | |
| 1828 | pub fn is_active(self) -> bool { | |
| 1829 | matches!( | |
| 1830 | self, | |
| 1831 | QueueState::Waiting | QueueState::Testing | QueueState::Passed | |
| 1832 | ) | |
| 1833 | } | |
| 1834 | } | |
| 1835 | ||
| 1836 | /// One pull request's place in a merge queue. | |
| 1837 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1838 | #[serde(rename_all = "camelCase")] | |
| 1839 | pub struct QueueEntry { | |
| 1840 | pub id: String, | |
| 1841 | pub number: u32, | |
| 1842 | pub title: String, | |
| 1843 | /// Who or what made the pull request, e.g. `g1t`. | |
| 1844 | pub agent: String, | |
| 1845 | pub state: QueueState, | |
| 1846 | /// The pull requests merged ahead of it in the state being tested, in | |
| 1847 | /// queue order. Empty when it was tested on the default branch alone. | |
| 1848 | pub ahead: Vec<u32>, | |
| 1849 | /// The default branch's commit the tested state was built on. | |
| 1850 | pub base_commit: Option<String>, | |
| 1851 | /// The tested state: the default branch with everything ahead and this. | |
| 1852 | pub combined_commit: Option<String>, | |
| 1853 | /// Why it failed: a merge conflict or what could not be run. | |
| 1854 | pub error: Option<String>, | |
| 1855 | /// Commands run against the tested state by queues from before its | |
| 1856 | /// checks were workflows. Empty since. | |
| 1857 | pub results: Vec<CheckResult>, | |
| 1858 | /// Username of whoever merged it into the queue: a person, or `g1t`. | |
| 1859 | pub enqueued_by: String, | |
| 1860 | /// RFC 3339. | |
| 1861 | pub created_at: String, | |
| 1862 | /// RFC 3339. When it landed or left. | |
| 1863 | pub finished_at: Option<String>, | |
| 1864 | } | |
| 1865 | ||
| 1866 | /// A repository's merge queue: what is in it, in order, and what recently | |
| 1867 | /// left it. | |
| 1868 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1869 | #[serde(rename_all = "camelCase")] | |
| 1870 | pub struct QueueView { | |
| 1871 | /// Whether the repository merges through the queue. | |
| 1872 | pub enabled: bool, | |
| 1873 | pub active: Vec<QueueEntry>, | |
| 1874 | /// Newest first. | |
| 1875 | pub recent: Vec<QueueEntry>, | |
| 1876 | } | |
| 1877 | ||
| 1878 | /// `queue`: a repository's merge queue. Returns `Outcome<QueueView>`. | |
| 1879 | #[derive(Debug, Serialize, Deserialize)] | |
| 1880 | pub struct QueueArgs { | |
| 1881 | pub repo: RepoPath, | |
| 1882 | pub viewer: Viewer, | |
| 1883 | } | |
| 1884 | ||
| 1885 | /// `queue_build`: the next batch to test for a repository, if nothing is | |
| 1886 | /// being tested now. Returns `Vec<QueueJob>`, one per entry, each testing | |
| 1887 | /// the default branch with that entry and everything ahead of it. | |
| 1888 | #[derive(Debug, Serialize, Deserialize)] | |
| 1889 | #[serde(rename_all = "camelCase")] | |
| 1890 | pub struct QueueBuildArgs { | |
| 1891 | pub repo_id: String, | |
| 1892 | } | |
| 1893 | ||
| 1894 | /// One pull request in a state being tested: where its change is. | |
| 1895 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1896 | #[serde(rename_all = "camelCase")] | |
| 1897 | pub struct QueueStackItem { | |
| 1898 | pub number: u32, | |
| 1899 | pub title: String, | |
| 1900 | /// The repository holding the change: its fork, or the repository. | |
| 1901 | pub source: RepoPath, | |
| 1902 | /// The branch of `source` holding it. | |
| 1903 | pub branch: String, | |
| 1904 | pub commit: String, | |
| 1905 | } | |
| 1906 | ||
| 1907 | /// What a sandbox needs to build and check one combined state. | |
| 1908 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1909 | #[serde(rename_all = "camelCase")] | |
| 1910 | pub struct QueueJob { | |
| 1911 | pub entry_id: String, | |
| 1912 | /// Lets the sandbox, and nothing else, report this state's result. | |
| 1913 | pub token: String, | |
| 1914 | pub repo: RepoPath, | |
| 1915 | pub default_branch: String, | |
| 1916 | /// The default branch's commit to build on. | |
| 1917 | pub base_commit: String, | |
| 1918 | /// Where to push the tested state, in the repository itself. | |
| 1919 | pub branch: String, | |
| 1920 | /// The pull requests to merge in, in order; the last is the entry. | |
| 1921 | pub stack: Vec<QueueStackItem>, | |
| 1922 | /// Commands to run on the built state. Always empty: the state is | |
| 1923 | /// checked by the `merge_group` workflows run on it, and the default | |
| 1924 | /// branch's required checks must pass there. | |
| 1925 | pub checks: Vec<String>, | |
| 1926 | /// Always empty, as `checks`. | |
| 1927 | #[serde(default)] | |
| 1928 | pub contract_checks: Vec<String>, | |
| 1929 | /// Who the sandbox acts as: a member who can push the tested state. | |
| 1930 | pub actor: User, | |
| 1931 | } | |
| 1932 | ||
| 1933 | /// `report_queue`: a sandbox's result for one combined state. Returns | |
| 1934 | /// `Outcome<QueueState>`. | |
| 1935 | #[derive(Debug, Serialize, Deserialize)] | |
| 1936 | #[serde(rename_all = "camelCase")] | |
| 1937 | pub struct ReportQueueArgs { | |
| 1938 | pub entry_id: String, | |
| 1939 | pub token: String, | |
| 1940 | #[serde(default)] | |
| 1941 | pub combined_commit: Option<String>, | |
| 1942 | #[serde(default)] | |
| 1943 | pub results: Vec<CheckResult>, | |
| 1944 | /// Set when the state could not be built or checked. | |
| 1945 | #[serde(default)] | |
| 1946 | pub error: Option<String>, | |
| 1947 | /// For a merge conflict: the pull request whose change it collided with. | |
| 1948 | #[serde(default)] | |
| 1949 | pub conflict_with: Option<u32>, | |
| 1950 | /// For a merge conflict: the files that conflicted. | |
| 1951 | #[serde(default)] | |
| 1952 | pub conflicts: Vec<String>, | |
| 1953 | } | |
| 1954 | ||
| 1955 | ||
| 1956 | /// `locate_pull`: where a pull request lives, by its id, for a tool that | |
| 1957 | /// knows only the fork it is working in (`g1t.sh/pulls/<id>`). Returns | |
| 1958 | /// `Outcome<LocatedPull>`; not found for anyone who cannot see it. | |
| 1959 | #[derive(Debug, Serialize, Deserialize)] | |
| 1960 | pub struct LocatePullArgs { | |
| 1961 | pub id: String, | |
| 1962 | pub viewer: Viewer, | |
| 1963 | } | |
| 1964 | ||
| 1965 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 1966 | #[serde(rename_all = "camelCase")] | |
| 1967 | pub struct LocatedPull { | |
| 1968 | pub repo: RepoPath, | |
| 1969 | pub number: u32, | |
| 1970 | pub title: String, | |
| 1971 | pub status: PullStatus, | |
| 1972 | } | |
| 1973 | ||
| 1974 | // --- A person's work ------------------------------------------------------- | |
| 1975 | ||
| 1976 | /// Issues or pull requests, on a person's profile. | |
| 1977 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1978 | #[serde(rename_all = "lowercase")] | |
| 1979 | pub enum AuthoredKind { | |
| 1980 | Issue, | |
| 1981 | Pull, | |
| 1982 | } | |
| 1983 | ||
| 1984 | /// The state filter on a person's work. `Closed` takes in merged pull | |
| 1985 | /// requests too; `Merged` is only those. | |
| 1986 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 1987 | #[serde(rename_all = "lowercase")] | |
| 1988 | pub enum AuthoredState { | |
| 1989 | Open, | |
| 1990 | Closed, | |
| 1991 | Merged, | |
| 1992 | } | |
| 1993 | ||
| 1994 | /// How a person's work is ordered. | |
| 1995 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 1996 | #[serde(rename_all = "lowercase")] | |
| 1997 | pub enum AuthoredSort { | |
| 1998 | /// Newest first. | |
| 1999 | #[default] | |
| 2000 | Created, | |
| 2001 | /// Most recently changed first. | |
| 2002 | Updated, | |
| 2003 | /// Oldest first. | |
| 2004 | Oldest, | |
| 2005 | } | |
| 2006 | ||
| 2007 | /// The most items one `by_author` page holds. | |
| 2008 | pub const AUTHORED_PAGE: u32 = 25; | |
| 2009 | ||
| 2010 | /// `by_author`: the issues and pull requests a person opened, only on | |
| 2011 | /// repositories `viewer` may read, so a private title never reaches anyone | |
| 2012 | /// who could not open it. Returns `Outcome<Authored>`; not found for an | |
| 2013 | /// account that does not exist. | |
| 2014 | #[derive(Debug, Serialize, Deserialize)] | |
| 2015 | pub struct ByAuthorArgs { | |
| 2016 | pub username: String, | |
| 2017 | pub viewer: Viewer, | |
| 2018 | #[serde(default)] | |
| 2019 | pub kind: Option<AuthoredKind>, | |
| 2020 | #[serde(default)] | |
| 2021 | pub state: Option<AuthoredState>, | |
| 2022 | /// Only work on this repository: `namespace/name`. | |
| 2023 | #[serde(default)] | |
| 2024 | pub repo: Option<String>, | |
| 2025 | #[serde(default)] | |
| 2026 | pub sort: AuthoredSort, | |
| 2027 | /// The `next` of the page before, to read on from there. | |
| 2028 | #[serde(default)] | |
| 2029 | pub before: Option<String>, | |
| 2030 | /// At most [`AUTHORED_PAGE`]; that when absent. | |
| 2031 | #[serde(default)] | |
| 2032 | pub limit: Option<u32>, | |
| 2033 | } | |
| 2034 | ||
| 2035 | /// One issue or pull request a person opened. | |
| 2036 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 2037 | #[serde(rename_all = "camelCase")] | |
| 2038 | pub struct AuthoredItem { | |
| 2039 | pub kind: AuthoredKind, | |
| 2040 | pub repo: RepoPath, | |
| 2041 | pub number: u32, | |
| 2042 | pub title: String, | |
| 2043 | /// Open or closed; a merged pull request is closed. | |
| 2044 | pub state: State, | |
| 2045 | /// A pull request's own status. | |
| 2046 | pub status: Option<PullStatus>, | |
| 2047 | /// Why an issue was closed. | |
| 2048 | pub reason: Option<IssueReason>, | |
| 2049 | pub draft: bool, | |
| 2050 | pub merged: bool, | |
| 2051 | /// RFC 3339. | |
| 2052 | pub created_at: String, | |
| 2053 | /// RFC 3339. | |
| 2054 | pub updated_at: String, | |
| 2055 | /// When a pull request was merged. RFC 3339. | |
| 2056 | pub merged_at: Option<String>, | |
| 2057 | } | |
| 2058 | ||
| 2059 | /// What a person has done, as far as the viewer may see. | |
| 2060 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 2061 | #[serde(rename_all = "camelCase")] | |
| 2062 | pub struct AuthoredCounts { | |
| 2063 | pub pulls_merged: u32, | |
| 2064 | pub pulls_open: u32, | |
| 2065 | pub pulls: u32, | |
| 2066 | pub issues: u32, | |
| 2067 | pub issues_open: u32, | |
| 2068 | } | |
| 2069 | ||
| 2070 | /// A repository a person has opened work on, with how much. | |
| 2071 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 2072 | pub struct AuthoredRepo { | |
| 2073 | pub repo: RepoPath, | |
| 2074 | pub count: u32, | |
| 2075 | } | |
| 2076 | ||
| 2077 | /// A page of a person's work. | |
| 2078 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 2079 | #[serde(rename_all = "camelCase")] | |
| 2080 | pub struct Authored { | |
| 2081 | pub items: Vec<AuthoredItem>, | |
| 2082 | /// Pass as `before` for the next page; null on the last. | |
| 2083 | pub next: Option<String>, | |
| 2084 | /// Over every repository the viewer may read, whatever the filters. | |
| 2085 | pub counts: AuthoredCounts, | |
| 2086 | /// Those repositories, most work first. | |
| 2087 | pub repos: Vec<AuthoredRepo>, | |
| 2088 | } | |
| 2089 | ||
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 2090 | /// How many days `contributions` covers: today and the 364 before it. |
| 2091 | pub const CONTRIBUTION_DAYS: u64 = 365; | |
| 2092 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 2093 | /// `contributions`: what a person did each day of the last year (commits |
| 2094 | /// they pushed to a default branch, issues and pull requests opened, | |
| 2095 | /// reviews given), only on repositories `viewer` may read, for the calendar on their profile. Returns | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 2096 | /// `Outcome<Contributions>`; not found for an account that does not exist. |
| 2097 | #[derive(Debug, Serialize, Deserialize)] | |
| 2098 | pub struct ContributionsArgs { | |
| 2099 | pub username: String, | |
| 2100 | pub viewer: Viewer, | |
| 2101 | } | |
| 2102 | ||
| 2103 | /// One day with something on it. Days with nothing are left out. | |
| 2104 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 2105 | pub struct ContributionDay { | |
| 2106 | /// `YYYY-MM-DD`, UTC. | |
| 2107 | pub date: String, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 2108 | /// Everything that day, commits included. |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 2109 | pub count: u32, |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 2110 | /// How many of `count` are commits they pushed (repos' `commit_days`). |
| 2111 | #[serde(default)] | |
| 2112 | pub commits: u32, | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 2113 | } |
| 2114 | ||
| 2115 | /// A person's year, as far as the viewer may see. | |
| 2116 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 2117 | pub struct Contributions { | |
| 2118 | /// Oldest first. | |
| 2119 | pub days: Vec<ContributionDay>, | |
| 2120 | pub total: u32, | |
| 2121 | /// The first day counted, `YYYY-MM-DD`; the last is today. | |
| 2122 | pub from: String, | |
| 2123 | } | |
| 2124 | ||
| API: notifications over REST and MCP, with notifications scopes | 2125 | #[cfg(test)] |
| 2126 | mod required_tests { | |
| 2127 | use super::*; | |
| 2128 | ||
| 2129 | fn status(context: &str, state: &str) -> CommitStatus { | |
| 2130 | CommitStatus { | |
| 2131 | context: context.into(), | |
| 2132 | state: state.into(), | |
| 2133 | description: Some(format!("{context} {state}")), | |
| 2134 | target_url: None, | |
| 2135 | updated_at: String::new(), | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 2136 | source: None, |
| Merge checks: statuses and check runs on every commit | 2137 | check_run_id: None, |
| API: notifications over REST and MCP, with notifications scopes | 2138 | } |
| 2139 | } | |
| 2140 | ||
| 2141 | #[test] | |
| 2142 | fn a_context_names_its_check_and_event() { | |
| 2143 | assert_eq!(check_name("CI / pull_request"), ("CI", Some("pull_request"))); | |
| 2144 | assert_eq!(check_name("Build and test / merge_group"), ("Build and test", Some("merge_group"))); | |
| 2145 | assert_eq!(check_name("g1t / deploy"), ("g1t / deploy", None)); | |
| 2146 | assert_eq!(check_name("g1t / deploy (docs)"), ("g1t / deploy (docs)", None)); | |
| 2147 | assert_eq!(check_name("lint"), ("lint", None)); | |
| 2148 | } | |
| 2149 | ||
| 2150 | #[test] | |
| 2151 | fn required_checks_are_missing_pending_failed_or_passed() { | |
| 2152 | let required = vec!["CI".to_owned(), "Lint".to_owned(), "g1t / deploy".to_owned(), "Docs".to_owned()]; | |
| 2153 | let statuses = [ | |
| 2154 | status("CI / pull_request", "success"), | |
| 2155 | status("Lint / pull_request", "pending"), | |
| 2156 | status("g1t / deploy", "failure"), | |
| 2157 | ]; | |
| 2158 | let states: Vec<RequiredState> = required_checks(&required, &statuses).into_iter().map(|c| c.state).collect(); | |
| 2159 | assert_eq!( | |
| 2160 | states, | |
| 2161 | [RequiredState::Success, RequiredState::Pending, RequiredState::Failure, RequiredState::Expected] | |
| 2162 | ); | |
| 2163 | } | |
| 2164 | ||
| 2165 | #[test] | |
| 2166 | fn any_event_reports_a_check_and_a_failure_wins() { | |
| 2167 | let required = vec!["ci".to_owned()]; | |
| 2168 | let both = [status("CI / push", "failure"), status("CI / pull_request", "success")]; | |
| 2169 | let check = &required_checks(&required, &both)[0]; | |
| 2170 | assert_eq!(check.state, RequiredState::Failure); | |
| 2171 | assert_eq!(check.description.as_deref(), Some("CI / push failure")); | |
| 2172 | let queue = [status("CI / merge_group", "success")]; | |
| 2173 | assert_eq!(required_checks(&required, &queue)[0].state, RequiredState::Success); | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 2174 | } |
| 2175 | ||
| 2176 | #[test] | |
| 2177 | fn a_pull_request_merges_into_its_base_or_the_default_branch() { | |
| 2178 | let mut pull: Pull = serde_json::from_value(serde_json::json!({ | |
| 2179 | "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": null, "title": "Fix it", "body": null, | |
| 2180 | "agent": "agent", "runtime": "external", "status": "open", "fork": null, "forkRepoId": null, | |
| 2181 | "branch": "fix", "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null, | |
| 2182 | "supersededBy": null, "checkStatus": null, "author": { "id": "x", "username": "x" }, | |
| 2183 | "createdAt": "", "updatedAt": "" | |
| 2184 | })) | |
| 2185 | .unwrap(); | |
| 2186 | assert_eq!(pull.base_branch("main"), "main"); | |
| 2187 | assert!(pull.targets_default("main")); | |
| 2188 | assert_eq!(pull.comparison(&None).base_branch, None); | |
| 2189 | pull.base = Some("release".into()); | |
| 2190 | assert_eq!(pull.base_branch("main"), "release"); | |
| 2191 | assert!(!pull.targets_default("main")); | |
| 2192 | assert_eq!(pull.comparison(&None).base_branch.as_deref(), Some("release")); | |
| 2193 | } | |
| 2194 | ||
| 2195 | #[test] | |
| API: notifications over REST and MCP, with notifications scopes | 2196 | fn required_names_are_tidied() { |
| 2197 | let names = vec![" CI ".to_owned(), "ci".to_owned(), String::new(), "Lint".to_owned()]; | |
| 2198 | assert_eq!(tidy_required(&names), ["CI", "Lint"]); | |
| 2199 | } | |
| 2200 | ||
| 2201 | #[test] | |
| 2202 | fn a_definition_of_done_is_added_once() { | |
| 2203 | let items = commands_pass(&["cargo test".to_owned(), " ".to_owned()]); | |
| 2204 | assert_eq!(items, ["`cargo test` passes."]); | |
| 2205 | let body = with_definition_of_done("Fix the greeting.", &items); | |
| 2206 | assert_eq!(body, "Fix the greeting.\n\n## Definition of done\n\n- `cargo test` passes."); | |
| 2207 | assert_eq!(with_definition_of_done(&body, &items), body); | |
| 2208 | assert_eq!(with_definition_of_done("", &items), "## Definition of done\n\n- `cargo test` passes."); | |
| 2209 | assert_eq!(with_definition_of_done(" Text ", &[]), "Text"); | |
| 2210 | } | |
| 2211 | } | |
| 2212 | ||
| 2213 | #[cfg(test)] | |
| 2214 | mod authorship_tests { | |
| 2215 | use super::*; | |
| 2216 | use crate::PrincipalKind; | |
| 2217 | use crate::credentials::{Acting, Principal}; | |
| 2218 | use crate::identity::{AGENT_ID, AgentScope}; | |
| 2219 | ||
| 2220 | fn person() -> User { | |
| 2221 | User { | |
| 2222 | id: "usr_1".into(), | |
| 2223 | username: "syntaqx".into(), | |
| 2224 | verified: true, | |
| 2225 | ..User::default() | |
| 2226 | } | |
| 2227 | } | |
| 2228 | ||
| 2229 | fn agent_for(id: &str, username: &str) -> User { | |
| 2230 | User { | |
| 2231 | id: AGENT_ID.into(), | |
| 2232 | username: "g1t".into(), | |
| 2233 | kind: PrincipalKind::Agent, | |
| 2234 | acting: Some(Box::new(Acting { | |
| 2235 | credential_id: "tok_1".into(), | |
| 2236 | agent: "g1t".into(), | |
| 2237 | on_behalf_of: Principal { id: id.into(), username: username.into() }, | |
| 2238 | scope: AgentScope { | |
| 2239 | repo: RepoPath { namespace: "acme".into(), name: "web".into() }, | |
| 2240 | operations: Vec::new(), | |
| 2241 | run: None, | |
| 2242 | }, | |
| 2243 | })), | |
| 2244 | ..User::default() | |
| 2245 | } | |
| 2246 | } | |
| 2247 | ||
| 2248 | fn pull(author: User, requested_by: Option<User>) -> Pull { | |
| 2249 | let mut pull: Pull = serde_json::from_value(serde_json::json!({ | |
| 2250 | "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": 12, "title": "Fix it", "body": null, | |
| 2251 | "agent": "g1t", "runtime": "hosted", "status": "open", "fork": null, "forkRepoId": null, | |
| 2252 | "branch": null, "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null, | |
| 2253 | "supersededBy": null, "checkStatus": null, | |
| 2254 | "author": { "id": "x", "username": "x" }, | |
| 2255 | "createdAt": "", "updatedAt": "" | |
| 2256 | })) | |
| 2257 | .unwrap(); | |
| 2258 | pull.author = author; | |
| 2259 | pull.requested_by = requested_by; | |
| 2260 | pull | |
| 2261 | } | |
| 2262 | ||
| 2263 | #[test] | |
| 2264 | fn a_change_a_person_has_g1t_make_is_g1t_s_requested_by_them() { | |
| 2265 | let (author, asked) = authorship(&person(), true); | |
| 2266 | assert_eq!((author.id.as_str(), author.username.as_str(), author.kind), (AGENT_ID, "g1t", PrincipalKind::Agent)); | |
| 2267 | let asked = asked.expect("the person asked for it"); | |
| 2268 | assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User)); | |
| 2269 | } | |
| 2270 | ||
| 2271 | #[test] | |
| 2272 | fn what_g1t_s_agent_files_at_work_is_g1t_s_requested_by_whoever_it_works_for() { | |
| 2273 | let (author, asked) = authorship(&agent_for("usr_1", "syntaqx"), false); | |
| 2274 | assert_eq!(author.id, AGENT_ID); | |
| 2275 | assert_eq!(asked.map(|user| user.username), Some("syntaqx".into())); | |
| 2276 | } | |
| 2277 | ||
| 2278 | #[test] | |
| 2279 | fn nobody_asked_for_g1t_s_own_work() { | |
| 2280 | // A run g1t started itself acts for g1t, not for a person. | |
| 2281 | let (author, asked) = authorship(&agent_for(crate::system::ID, "g1t"), false); | |
| 2282 | assert_eq!(author.id, AGENT_ID); | |
| 2283 | assert!(asked.is_none()); | |
| 2284 | // A security update g1t opens is g1t's own, as before. | |
| 2285 | let (author, asked) = authorship(&User::system("acme"), true); | |
| 2286 | assert_eq!((author.id.as_str(), author.kind), (crate::system::ID, PrincipalKind::System)); | |
| 2287 | assert!(asked.is_none()); | |
| 2288 | } | |
| 2289 | ||
| 2290 | #[test] | |
| 2291 | fn anyone_else_opens_their_own() { | |
| 2292 | let (author, asked) = authorship(&person(), false); | |
| 2293 | assert_eq!((author.id.as_str(), author.kind), ("usr_1", PrincipalKind::User)); | |
| 2294 | assert!(asked.is_none()); | |
| 2295 | // Nothing but who they are is kept. | |
| 2296 | assert!(author.workspaces.is_empty() && author.acting.is_none()); | |
| 2297 | } | |
| 2298 | ||
| 2299 | #[test] | |
| 2300 | fn the_requester_owns_g1t_s_pull_request_and_an_author_their_own() { | |
| 2301 | let made = pull(g1t_author(), Some(person())); | |
| 2302 | assert_eq!(made.owner().id, "usr_1"); | |
| 2303 | assert!(made.is_owned_by("usr_1")); | |
| 2304 | assert!(!made.is_owned_by(AGENT_ID), "g1t's agent does not answer for its own change"); | |
| 2305 | let own = pull(person(), None); | |
| 2306 | assert_eq!(own.owner().id, "usr_1"); | |
| 2307 | assert!(own.is_owned_by("usr_1")); | |
| 2308 | } | |
| 2309 | ||
| 2310 | #[test] | |
| 2311 | fn requested_by_is_null_when_nobody_asked_and_read_as_absent_from_older_senders() { | |
| 2312 | let own = pull(person(), None); | |
| 2313 | let sent = serde_json::to_value(&own).unwrap(); | |
| 2314 | assert!(sent["requestedBy"].is_null()); | |
| 2315 | let mut older = sent.clone(); | |
| 2316 | older.as_object_mut().unwrap().remove("requestedBy"); | |
| 2317 | let read: Pull = serde_json::from_value(older).unwrap(); | |
| 2318 | assert!(read.requested_by.is_none()); | |
| 2319 | } | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 2320 | |
| 2321 | #[test] | |
| 2322 | fn a_comment_is_changed_by_its_author_or_a_maintainer() { | |
| 2323 | use CommentKind::{Comment, Event}; | |
| 2324 | // Its author edits and deletes it; someone else needs Maintain. | |
| 2325 | assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, false).is_ok()); | |
| 2326 | assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, true).is_ok()); | |
| 2327 | assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, false).is_err()); | |
| 2328 | assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, true).is_err()); | |
| 2329 | assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, false).is_ok()); | |
| 2330 | assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, true).is_ok()); | |
| 2331 | // A review's text can be edited, but its verdict stays. | |
| 2332 | assert!(may_change_comment(Comment, true, "usr_1", "usr_1", false, false).is_ok()); | |
| 2333 | assert!(may_change_comment(Comment, true, "usr_1", "usr_1", true, true).is_err()); | |
| 2334 | // A note of what happened is nobody's to change. | |
| 2335 | assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, false).is_err()); | |
| 2336 | assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, true).is_err()); | |
| 2337 | } | |
| API: notifications over REST and MCP, with notifications scopes | 2338 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.