Skip to content

g1t/crates/contracts/src/work.rs

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