Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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