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,338 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 /// A skill the session used: its name only.
812 Skill,
813}
814
815/// One step of an agent's session: the "why" behind a pull request's commits.
816#[derive(Clone, Debug, Serialize, Deserialize)]
817pub struct SessionEntry {
818 pub seq: u32,
819 pub kind: SessionEntryKind,
820 pub text: String,
821 /// For tool calls and results.
822 pub tool: Option<String>,
823 /// The fork's head commit when this entry was recorded, if known.
824 pub commit: Option<String>,
825 /// RFC 3339.
826 pub at: String,
827}
828
829#[derive(Clone, Debug, Serialize, Deserialize)]
830pub struct NewSessionEntry {
831 pub kind: SessionEntryKind,
832 pub text: String,
833 #[serde(default)]
834 pub tool: Option<String>,
835 #[serde(default)]
836 pub commit: Option<String>,
837}
838
839#[derive(Clone, Debug, Serialize, Deserialize)]
840pub struct IssueDetail {
841 pub issue: Issue,
842 /// Every pull request made against it, oldest first.
843 pub pulls: Vec<Pull>,
844 pub comments: Vec<Comment>,
845}
846
847#[derive(Clone, Debug, Serialize, Deserialize)]
848pub struct PullDetail {
849 pub pull: Pull,
850 /// The issue it is for, if any.
851 pub issue: Option<Issue>,
852 pub comments: Vec<Comment>,
853 /// The latest record against its head: the merge queue taking it out,
854 /// or (from before checks were workflows) a run of its issue's commands.
855 pub checks: Option<CheckRun>,
856 /// Other pull requests in progress that change the same files.
857 #[serde(default)]
858 pub overlaps: Vec<Overlap>,
859 /// Whether the branch it would merge into has moved on without it, so
860 /// that it has to catch up before it can merge.
861 #[serde(default)]
862 pub behind: bool,
863 /// Where it stands on its way to being merged, for a pull request g1t
864 /// is seeing through. Absent on anyone else's.
865 #[serde(default)]
866 pub lifecycle: Option<Lifecycle>,
867 /// A merge was asked for while it was behind: g1t is bringing it up to
868 /// date and will then land it.
869 #[serde(default)]
870 pub landing: bool,
871 /// Why g1t stopped working on it, if it did: a catch-up that could not
872 /// be completed, for example.
873 #[serde(default)]
874 pub stalled: Option<String>,
875 /// What workflow runs said about its head commit, one per workflow.
876 #[serde(default)]
877 pub statuses: Vec<CommitStatus>,
878 /// Whether it merges cleanly into the branch it targets, worked out
879 /// ahead of time whenever either side moves.
880 #[serde(default)]
881 pub mergeable: Mergeable,
882 /// When `mergeable` is `conflicting`: the files that conflict.
883 #[serde(default)]
884 pub conflicts: Vec<String>,
885 /// Earlier records like `checks`, newest first, without their output.
886 #[serde(default)]
887 pub earlier_checks: Vec<CheckRun>,
888 /// The checks the default branch's protection requires, each as it
889 /// stands on the head commit. Empty when none are required.
890 #[serde(default, alias = "requiredChecks")]
891 pub required_checks: Vec<RequiredCheck>,
892 /// The rules of the branch it merges into that it does not meet yet,
893 /// for whoever is looking. Absent while it is not open.
894 #[serde(default, skip_serializing_if = "Option::is_none")]
895 pub rules: Option<crate::rules::MergeRules>,
896 /// Who owns the files it changes, from the CODEOWNERS file of the
897 /// branch it merges into, and whose approval is still needed. Absent
898 /// when that branch has no CODEOWNERS file.
899 #[serde(default, alias = "codeOwners")]
900 pub code_owners: Option<crate::codeowners::PullCodeOwners>,
901}
902
903/// Where a required check stands on a commit.
904#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
905#[serde(rename_all = "lowercase")]
906pub enum RequiredState {
907 Success,
908 Failure,
909 /// Reported and still running.
910 Pending,
911 /// Nothing has reported it on this commit yet.
912 Expected,
913}
914
915impl RequiredState {
916 pub fn as_str(self) -> &'static str {
917 match self {
918 RequiredState::Success => "success",
919 RequiredState::Failure => "failure",
920 RequiredState::Pending => "pending",
921 RequiredState::Expected => "expected",
922 }
923 }
924}
925
926/// One check a branch's protection requires, as it stands on a commit.
927#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
928#[serde(rename_all = "camelCase")]
929pub struct RequiredCheck {
930 /// The check's name, such as `CI` or `g1t / deploy`.
931 pub name: String,
932 pub state: RequiredState,
933 /// What the status that decided it says, if one did.
934 #[serde(default)]
935 pub description: Option<String>,
936 /// Where to see more: the workflow run, for one a workflow reported.
937 #[serde(default)]
938 pub target_url: Option<String>,
939}
940
941/// The events a workflow status's context can end in: `CI / pull_request`
942/// is the `CI` check, reported by a run for a `pull_request` event.
943const STATUS_EVENTS: &[&str] = &[
944 "push",
945 "pull_request",
946 "pull_request_target",
947 "pull_request_review",
948 "merge_group",
949 "workflow_dispatch",
950 "workflow_run",
951 "workflow_call",
952 "schedule",
953 "release",
954 "issues",
955 "issue_comment",
956 "repository_dispatch",
957];
958
959/// A status context's check name and the event it was reported for:
960/// `CI / pull_request` is `("CI", Some("pull_request"))`. A context that
961/// does not end in an event, such as `g1t / deploy`, is its own name.
962pub fn check_name(context: &str) -> (&str, Option<&str>) {
963 match context.rsplit_once(" / ") {
964 Some((name, event)) if STATUS_EVENTS.contains(&event) && !name.trim().is_empty() => (name, Some(event)),
965 _ => (context, None),
966 }
967}
968
969/// Where each required check stands among a commit's statuses. A check is
970/// met by any status of that name, for any event: one that failed fails
971/// it, one still running holds it, and with neither, one that passed
972/// passes it. Names compare without regard to case.
973pub fn required_checks(required: &[String], statuses: &[CommitStatus]) -> Vec<RequiredCheck> {
974 required
975 .iter()
976 .map(|name| {
977 let matching: Vec<&CommitStatus> = statuses
978 .iter()
979 .filter(|status| check_name(&status.context).0.eq_ignore_ascii_case(name.trim()))
980 .collect();
981 let failed = matching.iter().find(|s| s.state == "failure" || s.state == "error");
982 let pending = matching.iter().find(|s| s.state == "pending");
983 let passed = matching.iter().find(|s| s.state == "success");
984 let (state, decided) = match (failed, pending, passed) {
985 (Some(status), _, _) => (RequiredState::Failure, Some(*status)),
986 (None, Some(status), _) => (RequiredState::Pending, Some(*status)),
987 (None, None, Some(status)) => (RequiredState::Success, Some(*status)),
988 _ => (RequiredState::Expected, None),
989 };
990 RequiredCheck {
991 name: name.trim().to_owned(),
992 state,
993 description: decided.and_then(|status| status.description.clone()),
994 target_url: decided.and_then(|status| status.target_url.clone()),
995 }
996 })
997 .collect()
998}
999
1000/// A repository's required check names, tidied: trimmed, without blanks
1001/// or repeats (ignoring case), at most [`MAX_REQUIRED_CHECKS`].
1002pub fn tidy_required(names: &[String]) -> Vec<String> {
1003 let mut out: Vec<String> = Vec::new();
1004 for name in names {
1005 let name: String = name.trim().chars().take(MAX_CHECK_NAME_CHARS).collect();
1006 if !name.is_empty() && !out.iter().any(|kept| kept.eq_ignore_ascii_case(&name)) {
1007 out.push(name);
1008 }
1009 }
1010 out.truncate(MAX_REQUIRED_CHECKS);
1011 out
1012}
1013
1014/// The most checks a branch can require, and the longest name of one.
1015pub const MAX_REQUIRED_CHECKS: usize = 20;
1016pub const MAX_CHECK_NAME_CHARS: usize = 100;
1017
1018/// A check name seen on the repository's commits recently, for choosing
1019/// required checks: what reported it, and for which events.
1020#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1021#[serde(rename_all = "camelCase")]
1022pub struct SeenCheck {
1023 pub name: String,
1024 /// The events it was reported for, such as `pull_request`; empty for a
1025 /// status that names none, such as a deployment's.
1026 pub events: Vec<String>,
1027 /// RFC 3339. The latest report.
1028 pub last_seen: String,
1029}
1030
1031/// `seen_checks`: the check names reported on a repository's commits in
1032/// the last 30 days, most recent first. Returns `Outcome<Vec<SeenCheck>>`.
1033#[derive(Debug, Serialize, Deserialize)]
1034pub struct SeenChecksArgs {
1035 pub repo: RepoPath,
1036 pub viewer: Viewer,
1037}
1038
1039/// The heading an issue's plain-words description of done goes under.
1040pub const DEFINITION_OF_DONE: &str = "## Definition of done";
1041
1042/// An issue's body with `items` added as a bulleted "Definition of done"
1043/// section, for the agent and reviewers to read. Nothing is added when
1044/// `items` is empty, or when the body already has the section, so folding
1045/// the same items twice changes nothing.
1046pub fn with_definition_of_done(body: &str, items: &[String]) -> String {
1047 let items: Vec<&str> = items.iter().map(|item| item.trim()).filter(|item| !item.is_empty()).collect();
1048 let body = body.trim();
1049 if items.is_empty() || body.contains(DEFINITION_OF_DONE) {
1050 return body.to_owned();
1051 }
1052 let list = items.iter().map(|item| format!("- {item}")).collect::<Vec<_>>().join("\n");
1053 if body.is_empty() {
1054 format!("{DEFINITION_OF_DONE}\n\n{list}")
1055 } else {
1056 format!("{body}\n\n{DEFINITION_OF_DONE}\n\n{list}")
1057 }
1058}
1059
1060/// Commands, as items of a definition of done: "`npm test` passes."
1061pub fn commands_pass(commands: &[String]) -> Vec<String> {
1062 commands
1063 .iter()
1064 .map(|command| command.trim())
1065 .filter(|command| !command.is_empty())
1066 .map(|command| format!("`{}` passes.", command.replace('`', "'")))
1067 .collect()
1068}
1069
1070/// Whether a pull request's change merges cleanly into the branch it
1071/// targets.
1072#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1073#[serde(rename_all = "lowercase")]
1074pub enum Mergeable {
1075 /// It merges without conflicts.
1076 Clean,
1077 /// Some files conflict: see `PullDetail::conflicts`.
1078 Conflicting,
1079 /// Not known: never worked out, or it could not be.
1080 #[default]
1081 Unknown,
1082 /// Being worked out now.
1083 Checking,
1084}
1085
1086impl Mergeable {
1087 pub fn as_str(self) -> &'static str {
1088 match self {
1089 Mergeable::Clean => "clean",
1090 Mergeable::Conflicting => "conflicting",
1091 Mergeable::Unknown => "unknown",
1092 Mergeable::Checking => "checking",
1093 }
1094 }
1095
1096 pub fn parse(value: Option<&str>) -> Mergeable {
1097 match value {
1098 Some("clean") => Mergeable::Clean,
1099 Some("conflicting") => Mergeable::Conflicting,
1100 Some("checking") => Mergeable::Checking,
1101 _ => Mergeable::Unknown,
1102 }
1103 }
1104}
1105
1106/// `start_mergecheck`: claims the probe of whether a pull request merges
1107/// cleanly, which the work service asked for with a `pull.mergecheck`
1108/// event. Called by the runner service, which starts the sandbox. Refused
1109/// when it is no longer wanted, or when the repository already has as many
1110/// probes running as it may. Returns `Outcome<MergecheckJob>`.
1111#[derive(Debug, Serialize, Deserialize)]
1112#[serde(rename_all = "camelCase")]
1113pub struct StartMergecheckArgs {
1114 pub pull_id: String,
1115}
1116
1117/// What a sandbox needs to find out whether a pull request merges cleanly.
1118#[derive(Debug, Serialize, Deserialize)]
1119#[serde(rename_all = "camelCase")]
1120pub struct MergecheckJob {
1121 pub pull_id: String,
1122 /// Lets the sandbox, and nothing else, report this probe.
1123 pub token: String,
1124 pub repo: RepoPath,
1125 pub number: u32,
1126 pub default_branch: String,
1127 /// The default branch's commit to merge into.
1128 pub base: String,
1129 /// The repository holding the change: its fork, or the repository.
1130 pub source: RepoPath,
1131 /// The branch of `source` holding it.
1132 pub branch: String,
1133 /// The change's commit.
1134 pub head: String,
1135 /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
1136 /// it, or its author), and so can read its source.
1137 pub author: User,
1138}
1139
1140/// `report_mergecheck`: what a sandbox found. Returns `Outcome<Mergeable>`.
1141#[derive(Debug, Serialize, Deserialize)]
1142#[serde(rename_all = "camelCase")]
1143pub struct ReportMergecheckArgs {
1144 pub pull_id: String,
1145 pub token: String,
1146 /// The files that conflict; empty when it merges cleanly.
1147 #[serde(default)]
1148 pub conflicts: Vec<String>,
1149 /// Why it could not be found out.
1150 #[serde(default)]
1151 pub error: Option<String>,
1152}
1153
1154/// What a workflow run (or another tool) says about a commit.
1155#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1156#[serde(rename_all = "camelCase")]
1157pub struct CommitStatus {
1158 /// What reported it, such as `CI / push`.
1159 pub context: String,
1160 /// `pending`, `success`, `failure` or `error`.
1161 pub state: String,
1162 pub description: Option<String>,
1163 /// Where to see more, such as the run's page.
1164 pub target_url: Option<String>,
1165 pub updated_at: String,
1166 /// The integration that reported it (`actions`, `deployments`,
1167 /// `security`, `g1t`), when recorded. A required check can insist on
1168 /// one (`rules::RequiredCheck::integration`).
1169 #[serde(default, skip_serializing_if = "Option::is_none")]
1170 pub source: Option<String>,
1171 /// Set on the status a check run stands as (see `checks`): the check
1172 /// run's id. Such a status is listed as its check run, not again.
1173 #[serde(default, skip_serializing_if = "Option::is_none")]
1174 pub check_run_id: Option<String>,
1175}
1176
1177/// `set_commit_status`: for services only. Returns `Outcome<bool>`.
1178#[derive(Debug, Serialize, Deserialize)]
1179#[serde(rename_all = "camelCase")]
1180pub struct SetCommitStatusArgs {
1181 pub repo_id: String,
1182 pub sha: String,
1183 pub context: String,
1184 pub state: String,
1185 #[serde(default)]
1186 pub description: Option<String>,
1187 #[serde(default)]
1188 pub target_url: Option<String>,
1189 /// The integration reporting it: `actions`, `deployments`, `security`
1190 /// or `g1t`.
1191 #[serde(default)]
1192 pub source: Option<String>,
1193}
1194
1195/// A step on the way from an assigned issue to a pull request that is ready
1196/// to merge. g1t takes each one without being asked.
1197#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1198#[serde(rename_all = "snake_case")]
1199pub enum Stage {
1200 /// The agent is making the change.
1201 Working,
1202 /// Waiting for the checks workflows report on its head.
1203 Checking,
1204 /// g1t is reviewing it.
1205 Reviewing,
1206 /// The agent is addressing failed checks or a review.
1207 Revising,
1208 /// The agent is merging in the branch it would land on, which moved.
1209 CatchingUp,
1210 /// Woken to answer a question another agent asked it, or a handoff.
1211 Answering,
1212 /// In the repository's merge queue, being tested with what is ahead of
1213 /// it before it lands.
1214 Queued,
1215 /// Required checks passed, reviewed and approved, up to date. A
1216 /// person merges.
1217 Ready,
1218 /// g1t has stopped and a person has to decide what happens next.
1219 NeedsYou,
1220}
1221
1222/// Where a pull request made by a g1t agent stands. See [`Stage`].
1223#[derive(Clone, Debug, Serialize, Deserialize)]
1224pub struct Lifecycle {
1225 pub stage: Stage,
1226 /// One sentence saying what is happening, or why it stopped.
1227 pub detail: String,
1228 /// How many times the agent has been sent back to revise it.
1229 pub revisions: u32,
1230}
1231
1232/// `advance`: merges a pull request g1t is seeing through once it is ready,
1233/// where the repository's settings say ready pull requests land by
1234/// themselves. Called by the runner service. Returns `Advance`.
1235#[derive(Debug, Serialize, Deserialize)]
1236#[serde(rename_all = "camelCase")]
1237pub struct AdvanceArgs {
1238 pub pull_id: String,
1239}
1240
1241/// What the runner is to do next for a pull request: always nothing now,
1242/// since no step of a pull request is carried out in a sandbox any more.
1243#[derive(Debug, Serialize, Deserialize)]
1244#[serde(tag = "action", rename_all = "snake_case")]
1245pub enum Advance {
1246 /// Nothing to do.
1247 None,
1248}
1249
1250/// How a repository wants its pull requests handled. A repository that has
1251/// changed nothing has the defaults.
1252#[derive(Clone, Debug, Serialize, Deserialize)]
1253#[serde(rename_all = "camelCase", default)]
1254pub struct RepoSettings {
1255 /// Land a g1t agent's pull request without a person once it is ready:
1256 /// required checks passed and approved as the settings below require.
1257 pub auto_merge: bool,
1258 /// The checks that must pass on a pull request's head before it may
1259 /// merge into the default branch, by name: a workflow's name (`CI`), or
1260 /// the context of another status (`g1t / deploy`). The same for a
1261 /// person's pull request and an agent's, and for the merge queue.
1262 pub required_checks: Vec<String>,
1263 /// Refuse to merge a pull request that does not contain the default
1264 /// branch's latest commits, so that what merges is what was checked.
1265 /// When off, merging one that is behind brings it up to date first.
1266 pub require_up_to_date: bool,
1267 /// How many approving reviews a pull request needs before it may
1268 /// merge. A reviewer who has since asked for changes blocks it.
1269 pub required_approvals: u32,
1270 /// Whether a g1t agent's approval counts towards `required_approvals`.
1271 pub count_agent_approvals: bool,
1272 /// Whether someone who may merge can bypass required checks that have
1273 /// not passed, by saying so as they merge.
1274 pub allow_ignoring_checks: bool,
1275 /// Merge through a queue: pull requests are tested together with those
1276 /// ahead of them, and only a combination that passed reaches the default
1277 /// branch.
1278 pub merge_queue: bool,
1279 /// Refuse to merge until the code owners of every file it changes
1280 /// (its CODEOWNERS file) have approved, as many as each section asks.
1281 /// Only people count, and `g1t` only where the file names `@g1t`.
1282 pub require_code_owner_review: bool,
1283 /// Username of the member who last changed the settings, if anyone has.
1284 pub updated_by: Option<String>,
1285 /// RFC 3339.
1286 pub updated_at: Option<String>,
1287}
1288
1289impl Default for RepoSettings {
1290 fn default() -> Self {
1291 RepoSettings {
1292 auto_merge: false,
1293 required_checks: Vec::new(),
1294 require_up_to_date: false,
1295 required_approvals: 0,
1296 count_agent_approvals: true,
1297 allow_ignoring_checks: true,
1298 merge_queue: false,
1299 require_code_owner_review: false,
1300 updated_by: None,
1301 updated_at: None,
1302 }
1303 }
1304}
1305
1306/// `update_settings`: replaces a repository's settings. Members of its
1307/// workspace only. Returns `Outcome<RepoSettings>`. `get_settings` takes
1308/// `ViewArgs` and returns the same.
1309#[derive(Debug, Serialize, Deserialize)]
1310pub struct UpdateSettingsArgs {
1311 pub actor: User,
1312 pub repo: RepoPath,
1313 /// Who changed them and when are filled in by the service.
1314 pub settings: RepoSettings,
1315}
1316
1317/// `stall`: records that a step could not be carried out, so that g1t
1318/// stops and a person is asked. Returns `bool`.
1319#[derive(Debug, Serialize, Deserialize)]
1320#[serde(rename_all = "camelCase")]
1321pub struct StallArgs {
1322 pub pull_id: String,
1323 pub reason: String,
1324 /// The person who stopped it, by id, when someone did: they are not
1325 /// told it needs them.
1326 #[serde(default)]
1327 pub by: Option<String>,
1328}
1329
1330/// `managed_pulls`: ids of the open pull requests g1t is seeing through,
1331/// in one repository or in all of them. Returns `Vec<String>`.
1332#[derive(Debug, Default, Serialize, Deserialize)]
1333#[serde(rename_all = "camelCase")]
1334pub struct ManagedPullsArgs {
1335 #[serde(default)]
1336 pub repo_id: Option<String>,
1337}
1338
1339/// `open_issue`. Returns `Outcome<Issue>`.
1340#[derive(Debug, Serialize, Deserialize)]
1341pub struct OpenIssueArgs {
1342 pub actor: User,
1343 pub repo: RepoPath,
1344 pub title: String,
1345 #[serde(default)]
1346 pub body: String,
1347 #[serde(default)]
1348 pub labels: Vec<String>,
1349 /// Deprecated: commands, added to the body under "Definition of done".
1350 /// Checks are the workflows the branch's protection requires.
1351 #[serde(default)]
1352 pub checks: Vec<String>,
1353 /// The number of the milestone to put it in. Needs the Triage role.
1354 #[serde(default)]
1355 pub milestone: Option<u32>,
1356}
1357
1358/// `list_issues`, newest first. Returns `Outcome<Vec<Issue>>`.
1359#[derive(Debug, Serialize, Deserialize)]
1360pub struct ListIssuesArgs {
1361 pub repo: RepoPath,
1362 pub viewer: Viewer,
1363 #[serde(default)]
1364 pub state: Option<State>,
1365 /// Only issues carrying this label.
1366 #[serde(default)]
1367 pub label: Option<String>,
1368 /// Only issues in the milestone of this number.
1369 #[serde(default)]
1370 pub milestone: Option<u32>,
1371}
1372
1373/// `list_pulls`, newest first. Returns `Outcome<Vec<Pull>>`.
1374#[derive(Debug, Serialize, Deserialize)]
1375pub struct ListPullsArgs {
1376 pub repo: RepoPath,
1377 pub viewer: Viewer,
1378 #[serde(default)]
1379 pub state: Option<State>,
1380 /// Only pull requests carrying this label.
1381 #[serde(default)]
1382 pub label: Option<String>,
1383 /// Only pull requests in the milestone of this number.
1384 #[serde(default)]
1385 pub milestone: Option<u32>,
1386 /// Only pull requests into this branch.
1387 #[serde(default)]
1388 pub base: Option<String>,
1389}
1390
1391/// `pulls_for_repos`: the newest open and the newest closed pull requests
1392/// of many repositories, in one call, for pages that show several projects
1393/// at once. Repositories the viewer cannot read are left out, as are forks
1394/// (ask those with `list_pulls`). Returns `Vec<RepoPulls>`.
1395#[derive(Debug, Serialize, Deserialize)]
1396#[serde(rename_all = "camelCase")]
1397pub struct PullsForReposArgs {
1398 /// At most [`MAX_PULLS_FOR_REPOS`] are looked at.
1399 pub repo_ids: Vec<String>,
1400 pub viewer: Viewer,
1401 /// How many of each, open and closed, per repository (at most 100).
1402 pub limit: u32,
1403}
1404
1405/// The most repositories one `pulls_for_repos` call looks at.
1406pub const MAX_PULLS_FOR_REPOS: usize = 50;
1407
1408/// One repository's pull requests from `pulls_for_repos`, newest first.
1409#[derive(Debug, Serialize, Deserialize)]
1410#[serde(rename_all = "camelCase")]
1411pub struct RepoPulls {
1412 pub repo_id: String,
1413 /// Draft and open.
1414 pub open: Vec<Pull>,
1415 /// Merged and closed.
1416 pub closed: Vec<Pull>,
1417}
1418
1419/// `get_issue` (`Outcome<IssueDetail>`), `get_pull` (`Outcome<PullDetail>`),
1420/// `read_session` (`Outcome<Vec<SessionEntry>>`), `list_labels`
1421/// (`Outcome<Vec<String>>`) and `counts` (`Outcome<Counts>`). The last two
1422/// ignore `number`.
1423#[derive(Debug, Serialize, Deserialize)]
1424#[serde(rename_all = "camelCase")]
1425pub struct ViewArgs {
1426 pub repo: RepoPath,
1427 #[serde(default)]
1428 pub number: u32,
1429 pub viewer: Viewer,
1430 /// For `read_session`: only entries after this sequence number.
1431 #[serde(default)]
1432 pub after_seq: u32,
1433}
1434
1435/// How many issues and pull requests are open on a repository.
1436#[derive(Debug, Serialize, Deserialize)]
1437pub struct Counts {
1438 pub issues: u32,
1439 pub pulls: u32,
1440}
1441
1442/// `update_issue`: changes whichever fields are given. The author or a
1443/// member of the workspace may. Returns `Outcome<Issue>`.
1444#[derive(Debug, Serialize, Deserialize)]
1445pub struct UpdateIssueArgs {
1446 pub actor: User,
1447 pub repo: RepoPath,
1448 pub number: u32,
1449 #[serde(default)]
1450 pub title: Option<String>,
1451 #[serde(default)]
1452 pub body: Option<String>,
1453 #[serde(default)]
1454 pub labels: Option<Vec<String>>,
1455 /// Usernames of the people it is assigned to; replaces the whole set.
1456 /// Assigning it to g1t is the runner's `run`, not this.
1457 #[serde(default)]
1458 pub assignees: Option<Vec<String>>,
1459 /// The number of the milestone to put it in; 0 takes it out of its
1460 /// milestone. Needs the Triage role.
1461 #[serde(default)]
1462 pub milestone: Option<u32>,
1463 /// The numbers of the issues of the same repository that have to land
1464 /// first; replaces the whole set. Refused when one is not an issue, is
1465 /// the issue itself, or would make a cycle.
1466 #[serde(default)]
1467 pub blocked_by: Option<Vec<u32>>,
1468}
1469
1470/// `close_issue` and `reopen_issue`. Each returns `Outcome<Issue>`.
1471#[derive(Debug, Serialize, Deserialize)]
1472pub struct IssueActionArgs {
1473 pub actor: User,
1474 pub repo: RepoPath,
1475 pub number: u32,
1476 /// For `close_issue`; `completed` if left out.
1477 #[serde(default)]
1478 pub reason: Option<IssueReason>,
1479}
1480
1481/// `add_comment`, on an issue or a pull request. On a pull request it may
1482/// name a line of the change, and may carry a verdict; nobody can give a
1483/// verdict on their own pull request. Returns `Outcome<Comment>`.
1484#[derive(Debug, Serialize, Deserialize)]
1485pub struct AddCommentArgs {
1486 pub actor: User,
1487 pub repo: RepoPath,
1488 pub number: u32,
1489 /// May be empty when approving.
1490 #[serde(default)]
1491 pub body: String,
1492 #[serde(default)]
1493 pub path: Option<String>,
1494 #[serde(default)]
1495 pub line: Option<u32>,
1496 #[serde(default)]
1497 pub verdict: Option<Verdict>,
1498}
1499
1500/// `edit_comment` and `delete_comment`: change the text of a comment on an
1501/// issue or a pull request, or delete it. Its author may, and so may anyone
1502/// with the Maintain role or higher ([`may_change_comment`]). Notes of what
1503/// happened (`CommentKind::Event`) cannot be changed, and a review that
1504/// gave a verdict cannot be deleted, only edited. `edit_comment` returns
1505/// `Outcome<Comment>`; `delete_comment` returns `Outcome<()>`.
1506#[derive(Debug, Serialize, Deserialize)]
1507#[serde(rename_all = "camelCase")]
1508pub struct CommentActionArgs {
1509 pub actor: User,
1510 pub repo: RepoPath,
1511 pub comment_id: String,
1512 /// For `edit_comment`: the new text, in Markdown.
1513 #[serde(default)]
1514 pub body: String,
1515}
1516
1517/// Why `actor_id` may not edit (or, with `deleting`, delete) a comment by
1518/// `author_id`, or none when they may: its author may, and so may someone
1519/// who `maintains` the repository (the Maintain role or higher). A note of
1520/// something that happened is never changed, and a review's verdict stays,
1521/// so a comment that gave one can be edited but not deleted.
1522pub fn may_change_comment(
1523 kind: CommentKind,
1524 has_verdict: bool,
1525 author_id: &str,
1526 actor_id: &str,
1527 maintains: bool,
1528 deleting: bool,
1529) -> Result<(), &'static str> {
1530 if kind == CommentKind::Event {
1531 return Err("That is a record of something that happened, not a comment; it cannot be changed.");
1532 }
1533 if author_id != actor_id && !maintains {
1534 return Err(if deleting {
1535 "Only its author, or someone with the Maintain role or higher, can delete this comment."
1536 } else {
1537 "Only its author, or someone with the Maintain role or higher, can edit this comment."
1538 });
1539 }
1540 if deleting && has_verdict {
1541 return Err("A review cannot be deleted. Edit its text instead.");
1542 }
1543 Ok(())
1544}
1545
1546/// `open_pull`. Without `branch`, forks the repo and returns a draft pull
1547/// request to push to. With it, opens a pull request, ready for review,
1548/// for a branch already pushed to the repo. Returns `Outcome<Pull>`.
1549#[derive(Debug, Serialize, Deserialize)]
1550pub struct OpenPullArgs {
1551 pub actor: User,
1552 pub repo: RepoPath,
1553 /// The issue this is for.
1554 #[serde(default)]
1555 pub issue: Option<u32>,
1556 /// Defaults to the issue's title; required without an issue.
1557 #[serde(default)]
1558 pub title: String,
1559 /// What changed and why. Usually set later, when a draft is marked ready.
1560 #[serde(default)]
1561 pub body: String,
1562 /// A branch of the repository that already holds the change.
1563 #[serde(default)]
1564 pub branch: Option<String>,
1565 #[serde(default)]
1566 pub agent: String,
1567 pub runtime: Runtime,
1568 /// The branch to merge into: the default branch when absent.
1569 #[serde(default)]
1570 pub base: Option<String>,
1571 /// Opened from a branch as a draft, still being worked on: it can't
1572 /// merge, and agents' review routines wait, until it is marked ready.
1573 #[serde(default)]
1574 pub draft: bool,
1575}
1576
1577/// `ready_pull`, `close_pull`, `reopen_pull`, `convert_pull_to_draft` and
1578/// `merge_pull`. Each returns `Outcome<Pull>`. `reopen_pull` opens a closed
1579/// pull request again (never a merged one), as the draft it was if it was
1580/// closed as one; `convert_pull_to_draft` turns one that is open back into
1581/// a draft, which leaves the merge queue.
1582///
1583/// Also `catch_up_pull`: brings the pull request up to date with the
1584/// default branch without a sandbox where that is safe, as the repos
1585/// service's `update_pull_branch` does, after checking that `actor` may
1586/// update it: its owner ([`Pull::owner`]) for a fork, any member for a
1587/// branch.
1588/// Returns `Outcome<repos::PullBranchUpdate>`; on `needs_agent` nothing was
1589/// pushed and the runner's `update` is the way on.
1590#[derive(Debug, Serialize, Deserialize)]
1591#[serde(rename_all = "camelCase")]
1592pub struct PullActionArgs {
1593 pub actor: User,
1594 pub repo: RepoPath,
1595 pub number: u32,
1596 /// For `ready_pull`: what changed and why.
1597 #[serde(default)]
1598 pub summary: String,
1599 /// For `merge_pull`: leave the issue open and the other pull requests
1600 /// for it untouched, because this one is only part of the work.
1601 #[serde(default)]
1602 pub keep_issue_open: bool,
1603 /// For `merge_pull`: merge although required checks have not passed,
1604 /// where the repository lets members bypass them.
1605 #[serde(default)]
1606 pub ignore_checks: bool,
1607 /// For `merge_pull`: merge although rules are not met, where a ruleset
1608 /// lists the actor as one who may bypass it. Recorded as a bypass.
1609 #[serde(default)]
1610 pub bypass_rules: bool,
1611}
1612
1613/// `update_pull`: changes who a pull request is assigned to, whose review
1614/// is asked for, its labels, its milestone and the branch it merges into.
1615/// Each list given replaces the whole set. Whoever opened it, or someone
1616/// with the Triage role, may; changing the base needs the Write role.
1617/// Returns `Outcome<Pull>`.
1618#[derive(Debug, Serialize, Deserialize)]
1619pub struct UpdatePullArgs {
1620 pub actor: User,
1621 pub repo: RepoPath,
1622 pub number: u32,
1623 #[serde(default)]
1624 pub assignees: Option<Vec<String>>,
1625 /// May include `g1t`. Asking for its review does not by itself
1626 /// start one; the runner's `review` does. A team is named
1627 /// `workspace/team` (or `@workspace/team`): the whole list, people and
1628 /// teams, replaces who is asked.
1629 #[serde(default)]
1630 pub reviewers: Option<Vec<String>>,
1631 /// Its labels; replaces the whole set, as `set_labels` does.
1632 #[serde(default)]
1633 pub labels: Option<Vec<String>>,
1634 /// The number of the milestone to put it in; 0 takes it out. Needs the
1635 /// Triage role.
1636 #[serde(default)]
1637 pub milestone: Option<u32>,
1638 /// The branch it merges into: an existing branch other than its own.
1639 /// Needs the Write role.
1640 #[serde(default)]
1641 pub base: Option<String>,
1642}
1643
1644/// `start_checks`: always refused now; a pull request's checks are the
1645/// workflows run on it. Kept so that a runner from before is answered.
1646/// Returns `Outcome<CheckJob>`.
1647#[derive(Debug, Serialize, Deserialize)]
1648#[serde(rename_all = "camelCase")]
1649pub struct StartChecksArgs {
1650 pub pull_id: String,
1651}
1652
1653/// What a sandbox needs to carry out a check run.
1654#[derive(Debug, Serialize, Deserialize)]
1655#[serde(rename_all = "camelCase")]
1656pub struct CheckJob {
1657 pub run_id: String,
1658 /// Lets the sandbox, and nothing else, report this run's results.
1659 pub token: String,
1660 pub commands: Vec<String>,
1661 /// The repository holding the commit: the fork, or the repository itself.
1662 pub source: RepoPath,
1663 pub commit: String,
1664 /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
1665 /// it, or its author), and so can read its source.
1666 pub author: User,
1667 /// Username of whoever wrote the checks: the issue's author.
1668 pub requested_by: String,
1669 pub repo: RepoPath,
1670 pub number: u32,
1671}
1672
1673/// `report_checks`: what a sandbox says about its run. With no results and
1674/// no error it has started. `skip` forgets the run, for one that will not
1675/// be carried out. Returns `Outcome<CheckRun>`.
1676#[derive(Debug, Serialize, Deserialize)]
1677#[serde(rename_all = "camelCase")]
1678pub struct ReportChecksArgs {
1679 pub run_id: String,
1680 pub token: String,
1681 #[serde(default)]
1682 pub results: Vec<CheckResult>,
1683 #[serde(default)]
1684 pub error: Option<String>,
1685 #[serde(default)]
1686 pub skip: bool,
1687}
1688
1689/// A pull request in progress, with where it lives.
1690#[derive(Debug, Serialize, Deserialize)]
1691pub struct ActivePull {
1692 pub pull: Pull,
1693 pub issue: Option<Issue>,
1694 /// Where it stands, for one g1t is seeing through.
1695 #[serde(default)]
1696 pub lifecycle: Option<Lifecycle>,
1697}
1698
1699/// `list_active_pulls`: drafts and open pull requests the viewer started,
1700/// most recently active first. Returns `Vec<ActivePull>`. Also
1701/// `list_assigned_issues`: open issues assigned to the viewer, most
1702/// recently changed first. Returns `Vec<Issue>`.
1703#[derive(Debug, Serialize, Deserialize)]
1704pub struct ViewerArgs {
1705 pub viewer: Viewer,
1706}
1707
1708/// `append_session`. Returns `Outcome<Appended>`.
1709#[derive(Debug, Serialize, Deserialize)]
1710pub struct AppendSessionArgs {
1711 pub actor: User,
1712 pub repo: RepoPath,
1713 pub number: u32,
1714 pub entries: Vec<NewSessionEntry>,
1715}
1716
1717#[derive(Debug, Serialize, Deserialize)]
1718pub struct Appended {
1719 pub count: u32,
1720}
1721
1722/// Lowercases, trims and de-duplicates labels, dropping empty ones.
1723/// Returns `None` if there are too many or one is too long.
1724pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> {
1725 let mut normalized: Vec<String> = Vec::new();
1726 for label in labels {
1727 let label = label
1728 .split_whitespace()
1729 .collect::<Vec<_>>()
1730 .join(" ")
1731 .to_lowercase();
1732 if label.is_empty() || normalized.contains(&label) {
1733 continue;
1734 }
1735 if label.chars().count() > MAX_LABEL_CHARS {
1736 return None;
1737 }
1738 normalized.push(label);
1739 }
1740 (normalized.len() <= MAX_LABELS).then_some(normalized)
1741}
1742
1743#[cfg(test)]
1744mod tests {
1745 use super::*;
1746
1747 fn labels(names: &[&str]) -> Vec<String> {
1748 names.iter().map(|name| (*name).to_owned()).collect()
1749 }
1750
1751 #[test]
1752 fn colors_and_due_dates_are_tidied() {
1753 assert_eq!(tidy_color("#A1B2C3").as_deref(), Some("a1b2c3"));
1754 assert_eq!(tidy_color("fc0").as_deref(), Some("ffcc00"));
1755 assert_eq!(tidy_color("red"), None);
1756 assert_eq!(tidy_color("12345"), None);
1757 assert_eq!(label_color_for("bug"), "d73a4a");
1758 assert_eq!(label_color_for("area: web"), label_color_for("area: web"));
1759 assert_eq!(tidy_due_on("2026-10-14").as_deref(), Some("2026-10-14"));
1760 assert_eq!(tidy_due_on("2026-10-14T00:00:00Z").as_deref(), Some("2026-10-14"));
1761 assert_eq!(tidy_due_on("2026-13-01"), None);
1762 assert_eq!(tidy_due_on("soon"), None);
1763 }
1764
1765 #[test]
1766 fn a_milestone_is_as_far_along_as_its_closed_items() {
1767 let mut milestone: Milestone = serde_json::from_value(serde_json::json!({
1768 "number": 1, "title": "Launch", "state": "open", "createdAt": "", "updatedAt": ""
1769 }))
1770 .unwrap();
1771 assert_eq!(milestone.percent(), 0);
1772 milestone.open_items = 3;
1773 milestone.closed_items = 1;
1774 assert_eq!(milestone.percent(), 25);
1775 }
1776
1777 #[test]
1778 fn labels_are_lowercased_trimmed_and_unique() {
1779 assert_eq!(
1780 normalize_labels(&labels(&[" Bug ", "bug", "", "Good First Issue"])),
1781 Some(labels(&["bug", "good first issue"]))
1782 );
1783 }
1784
1785 #[test]
1786 fn too_long_or_too_many_labels_are_refused() {
1787 assert_eq!(normalize_labels(&["x".repeat(51)]), None);
1788 assert!(normalize_labels(&["x".repeat(50)]).is_some());
1789 let many: Vec<String> = (0..21).map(|i| format!("label-{i}")).collect();
1790 assert_eq!(normalize_labels(&many), None);
1791 }
1792}
1793
1794
1795// --- Merge queue ----------------------------------------------------------
1796
1797/// Where a pull request in a merge queue stands.
1798#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1799#[serde(rename_all = "snake_case")]
1800pub enum QueueState {
1801 /// Waiting for its turn to be tested.
1802 Waiting,
1803 /// Its combined state is being built and checked.
1804 Testing,
1805 /// Its combined state passed; it lands once everything ahead has.
1806 Passed,
1807 /// Its combined state failed, or would not merge. It left the queue.
1808 Failed,
1809 /// On the default branch.
1810 Landed,
1811 /// Taken out of the queue by a person, or closed.
1812 Removed,
1813}
1814
1815impl QueueState {
1816 pub fn as_str(self) -> &'static str {
1817 match self {
1818 QueueState::Waiting => "waiting",
1819 QueueState::Testing => "testing",
1820 QueueState::Passed => "passed",
1821 QueueState::Failed => "failed",
1822 QueueState::Landed => "landed",
1823 QueueState::Removed => "removed",
1824 }
1825 }
1826
1827 /// Still in the queue.
1828 pub fn is_active(self) -> bool {
1829 matches!(
1830 self,
1831 QueueState::Waiting | QueueState::Testing | QueueState::Passed
1832 )
1833 }
1834}
1835
1836/// One pull request's place in a merge queue.
1837#[derive(Clone, Debug, Serialize, Deserialize)]
1838#[serde(rename_all = "camelCase")]
1839pub struct QueueEntry {
1840 pub id: String,
1841 pub number: u32,
1842 pub title: String,
1843 /// Who or what made the pull request, e.g. `g1t`.
1844 pub agent: String,
1845 pub state: QueueState,
1846 /// The pull requests merged ahead of it in the state being tested, in
1847 /// queue order. Empty when it was tested on the default branch alone.
1848 pub ahead: Vec<u32>,
1849 /// The default branch's commit the tested state was built on.
1850 pub base_commit: Option<String>,
1851 /// The tested state: the default branch with everything ahead and this.
1852 pub combined_commit: Option<String>,
1853 /// Why it failed: a merge conflict or what could not be run.
1854 pub error: Option<String>,
1855 /// Commands run against the tested state by queues from before its
1856 /// checks were workflows. Empty since.
1857 pub results: Vec<CheckResult>,
1858 /// Username of whoever merged it into the queue: a person, or `g1t`.
1859 pub enqueued_by: String,
1860 /// RFC 3339.
1861 pub created_at: String,
1862 /// RFC 3339. When it landed or left.
1863 pub finished_at: Option<String>,
1864}
1865
1866/// A repository's merge queue: what is in it, in order, and what recently
1867/// left it.
1868#[derive(Clone, Debug, Serialize, Deserialize)]
1869#[serde(rename_all = "camelCase")]
1870pub struct QueueView {
1871 /// Whether the repository merges through the queue.
1872 pub enabled: bool,
1873 pub active: Vec<QueueEntry>,
1874 /// Newest first.
1875 pub recent: Vec<QueueEntry>,
1876}
1877
1878/// `queue`: a repository's merge queue. Returns `Outcome<QueueView>`.
1879#[derive(Debug, Serialize, Deserialize)]
1880pub struct QueueArgs {
1881 pub repo: RepoPath,
1882 pub viewer: Viewer,
1883}
1884
1885/// `queue_build`: the next batch to test for a repository, if nothing is
1886/// being tested now. Returns `Vec<QueueJob>`, one per entry, each testing
1887/// the default branch with that entry and everything ahead of it.
1888#[derive(Debug, Serialize, Deserialize)]
1889#[serde(rename_all = "camelCase")]
1890pub struct QueueBuildArgs {
1891 pub repo_id: String,
1892}
1893
1894/// One pull request in a state being tested: where its change is.
1895#[derive(Clone, Debug, Serialize, Deserialize)]
1896#[serde(rename_all = "camelCase")]
1897pub struct QueueStackItem {
1898 pub number: u32,
1899 pub title: String,
1900 /// The repository holding the change: its fork, or the repository.
1901 pub source: RepoPath,
1902 /// The branch of `source` holding it.
1903 pub branch: String,
1904 pub commit: String,
1905}
1906
1907/// What a sandbox needs to build and check one combined state.
1908#[derive(Clone, Debug, Serialize, Deserialize)]
1909#[serde(rename_all = "camelCase")]
1910pub struct QueueJob {
1911 pub entry_id: String,
1912 /// Lets the sandbox, and nothing else, report this state's result.
1913 pub token: String,
1914 pub repo: RepoPath,
1915 pub default_branch: String,
1916 /// The default branch's commit to build on.
1917 pub base_commit: String,
1918 /// Where to push the tested state, in the repository itself.
1919 pub branch: String,
1920 /// The pull requests to merge in, in order; the last is the entry.
1921 pub stack: Vec<QueueStackItem>,
1922 /// Commands to run on the built state. Always empty: the state is
1923 /// checked by the `merge_group` workflows run on it, and the default
1924 /// branch's required checks must pass there.
1925 pub checks: Vec<String>,
1926 /// Always empty, as `checks`.
1927 #[serde(default)]
1928 pub contract_checks: Vec<String>,
1929 /// Who the sandbox acts as: a member who can push the tested state.
1930 pub actor: User,
1931}
1932
1933/// `report_queue`: a sandbox's result for one combined state. Returns
1934/// `Outcome<QueueState>`.
1935#[derive(Debug, Serialize, Deserialize)]
1936#[serde(rename_all = "camelCase")]
1937pub struct ReportQueueArgs {
1938 pub entry_id: String,
1939 pub token: String,
1940 #[serde(default)]
1941 pub combined_commit: Option<String>,
1942 #[serde(default)]
1943 pub results: Vec<CheckResult>,
1944 /// Set when the state could not be built or checked.
1945 #[serde(default)]
1946 pub error: Option<String>,
1947 /// For a merge conflict: the pull request whose change it collided with.
1948 #[serde(default)]
1949 pub conflict_with: Option<u32>,
1950 /// For a merge conflict: the files that conflicted.
1951 #[serde(default)]
1952 pub conflicts: Vec<String>,
1953}
1954
1955
1956/// `locate_pull`: where a pull request lives, by its id, for a tool that
1957/// knows only the fork it is working in (`g1t.sh/pulls/<id>`). Returns
1958/// `Outcome<LocatedPull>`; not found for anyone who cannot see it.
1959#[derive(Debug, Serialize, Deserialize)]
1960pub struct LocatePullArgs {
1961 pub id: String,
1962 pub viewer: Viewer,
1963}
1964
1965#[derive(Clone, Debug, Serialize, Deserialize)]
1966#[serde(rename_all = "camelCase")]
1967pub struct LocatedPull {
1968 pub repo: RepoPath,
1969 pub number: u32,
1970 pub title: String,
1971 pub status: PullStatus,
1972}
1973
1974// --- A person's work -------------------------------------------------------
1975
1976/// Issues or pull requests, on a person's profile.
1977#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1978#[serde(rename_all = "lowercase")]
1979pub enum AuthoredKind {
1980 Issue,
1981 Pull,
1982}
1983
1984/// The state filter on a person's work. `Closed` takes in merged pull
1985/// requests too; `Merged` is only those.
1986#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1987#[serde(rename_all = "lowercase")]
1988pub enum AuthoredState {
1989 Open,
1990 Closed,
1991 Merged,
1992}
1993
1994/// How a person's work is ordered.
1995#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1996#[serde(rename_all = "lowercase")]
1997pub enum AuthoredSort {
1998 /// Newest first.
1999 #[default]
2000 Created,
2001 /// Most recently changed first.
2002 Updated,
2003 /// Oldest first.
2004 Oldest,
2005}
2006
2007/// The most items one `by_author` page holds.
2008pub const AUTHORED_PAGE: u32 = 25;
2009
2010/// `by_author`: the issues and pull requests a person opened, only on
2011/// repositories `viewer` may read, so a private title never reaches anyone
2012/// who could not open it. Returns `Outcome<Authored>`; not found for an
2013/// account that does not exist.
2014#[derive(Debug, Serialize, Deserialize)]
2015pub struct ByAuthorArgs {
2016 pub username: String,
2017 pub viewer: Viewer,
2018 #[serde(default)]
2019 pub kind: Option<AuthoredKind>,
2020 #[serde(default)]
2021 pub state: Option<AuthoredState>,
2022 /// Only work on this repository: `namespace/name`.
2023 #[serde(default)]
2024 pub repo: Option<String>,
2025 #[serde(default)]
2026 pub sort: AuthoredSort,
2027 /// The `next` of the page before, to read on from there.
2028 #[serde(default)]
2029 pub before: Option<String>,
2030 /// At most [`AUTHORED_PAGE`]; that when absent.
2031 #[serde(default)]
2032 pub limit: Option<u32>,
2033}
2034
2035/// One issue or pull request a person opened.
2036#[derive(Clone, Debug, Serialize, Deserialize)]
2037#[serde(rename_all = "camelCase")]
2038pub struct AuthoredItem {
2039 pub kind: AuthoredKind,
2040 pub repo: RepoPath,
2041 pub number: u32,
2042 pub title: String,
2043 /// Open or closed; a merged pull request is closed.
2044 pub state: State,
2045 /// A pull request's own status.
2046 pub status: Option<PullStatus>,
2047 /// Why an issue was closed.
2048 pub reason: Option<IssueReason>,
2049 pub draft: bool,
2050 pub merged: bool,
2051 /// RFC 3339.
2052 pub created_at: String,
2053 /// RFC 3339.
2054 pub updated_at: String,
2055 /// When a pull request was merged. RFC 3339.
2056 pub merged_at: Option<String>,
2057}
2058
2059/// What a person has done, as far as the viewer may see.
2060#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2061#[serde(rename_all = "camelCase")]
2062pub struct AuthoredCounts {
2063 pub pulls_merged: u32,
2064 pub pulls_open: u32,
2065 pub pulls: u32,
2066 pub issues: u32,
2067 pub issues_open: u32,
2068}
2069
2070/// A repository a person has opened work on, with how much.
2071#[derive(Clone, Debug, Serialize, Deserialize)]
2072pub struct AuthoredRepo {
2073 pub repo: RepoPath,
2074 pub count: u32,
2075}
2076
2077/// A page of a person's work.
2078#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2079#[serde(rename_all = "camelCase")]
2080pub struct Authored {
2081 pub items: Vec<AuthoredItem>,
2082 /// Pass as `before` for the next page; null on the last.
2083 pub next: Option<String>,
2084 /// Over every repository the viewer may read, whatever the filters.
2085 pub counts: AuthoredCounts,
2086 /// Those repositories, most work first.
2087 pub repos: Vec<AuthoredRepo>,
2088}
2089
2090/// How many days `contributions` covers: today and the 364 before it.
2091pub const CONTRIBUTION_DAYS: u64 = 365;
2092
2093/// `contributions`: what a person did each day of the last year (commits
2094/// they pushed to a default branch, issues and pull requests opened,
2095/// reviews given), only on repositories `viewer` may read, for the calendar on their profile. Returns
2096/// `Outcome<Contributions>`; not found for an account that does not exist.
2097#[derive(Debug, Serialize, Deserialize)]
2098pub struct ContributionsArgs {
2099 pub username: String,
2100 pub viewer: Viewer,
2101}
2102
2103/// One day with something on it. Days with nothing are left out.
2104#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2105pub struct ContributionDay {
2106 /// `YYYY-MM-DD`, UTC.
2107 pub date: String,
2108 /// Everything that day, commits included.
2109 pub count: u32,
2110 /// How many of `count` are commits they pushed (repos' `commit_days`).
2111 #[serde(default)]
2112 pub commits: u32,
2113}
2114
2115/// A person's year, as far as the viewer may see.
2116#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2117pub struct Contributions {
2118 /// Oldest first.
2119 pub days: Vec<ContributionDay>,
2120 pub total: u32,
2121 /// The first day counted, `YYYY-MM-DD`; the last is today.
2122 pub from: String,
2123}
2124
2125#[cfg(test)]
2126mod required_tests {
2127 use super::*;
2128
2129 fn status(context: &str, state: &str) -> CommitStatus {
2130 CommitStatus {
2131 context: context.into(),
2132 state: state.into(),
2133 description: Some(format!("{context} {state}")),
2134 target_url: None,
2135 updated_at: String::new(),
2136 source: None,
2137 check_run_id: None,
2138 }
2139 }
2140
2141 #[test]
2142 fn a_context_names_its_check_and_event() {
2143 assert_eq!(check_name("CI / pull_request"), ("CI", Some("pull_request")));
2144 assert_eq!(check_name("Build and test / merge_group"), ("Build and test", Some("merge_group")));
2145 assert_eq!(check_name("g1t / deploy"), ("g1t / deploy", None));
2146 assert_eq!(check_name("g1t / deploy (docs)"), ("g1t / deploy (docs)", None));
2147 assert_eq!(check_name("lint"), ("lint", None));
2148 }
2149
2150 #[test]
2151 fn required_checks_are_missing_pending_failed_or_passed() {
2152 let required = vec!["CI".to_owned(), "Lint".to_owned(), "g1t / deploy".to_owned(), "Docs".to_owned()];
2153 let statuses = [
2154 status("CI / pull_request", "success"),
2155 status("Lint / pull_request", "pending"),
2156 status("g1t / deploy", "failure"),
2157 ];
2158 let states: Vec<RequiredState> = required_checks(&required, &statuses).into_iter().map(|c| c.state).collect();
2159 assert_eq!(
2160 states,
2161 [RequiredState::Success, RequiredState::Pending, RequiredState::Failure, RequiredState::Expected]
2162 );
2163 }
2164
2165 #[test]
2166 fn any_event_reports_a_check_and_a_failure_wins() {
2167 let required = vec!["ci".to_owned()];
2168 let both = [status("CI / push", "failure"), status("CI / pull_request", "success")];
2169 let check = &required_checks(&required, &both)[0];
2170 assert_eq!(check.state, RequiredState::Failure);
2171 assert_eq!(check.description.as_deref(), Some("CI / push failure"));
2172 let queue = [status("CI / merge_group", "success")];
2173 assert_eq!(required_checks(&required, &queue)[0].state, RequiredState::Success);
2174 }
2175
2176 #[test]
2177 fn a_pull_request_merges_into_its_base_or_the_default_branch() {
2178 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2179 "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": null, "title": "Fix it", "body": null,
2180 "agent": "agent", "runtime": "external", "status": "open", "fork": null, "forkRepoId": null,
2181 "branch": "fix", "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2182 "supersededBy": null, "checkStatus": null, "author": { "id": "x", "username": "x" },
2183 "createdAt": "", "updatedAt": ""
2184 }))
2185 .unwrap();
2186 assert_eq!(pull.base_branch("main"), "main");
2187 assert!(pull.targets_default("main"));
2188 assert_eq!(pull.comparison(&None).base_branch, None);
2189 pull.base = Some("release".into());
2190 assert_eq!(pull.base_branch("main"), "release");
2191 assert!(!pull.targets_default("main"));
2192 assert_eq!(pull.comparison(&None).base_branch.as_deref(), Some("release"));
2193 }
2194
2195 #[test]
2196 fn required_names_are_tidied() {
2197 let names = vec![" CI ".to_owned(), "ci".to_owned(), String::new(), "Lint".to_owned()];
2198 assert_eq!(tidy_required(&names), ["CI", "Lint"]);
2199 }
2200
2201 #[test]
2202 fn a_definition_of_done_is_added_once() {
2203 let items = commands_pass(&["cargo test".to_owned(), " ".to_owned()]);
2204 assert_eq!(items, ["`cargo test` passes."]);
2205 let body = with_definition_of_done("Fix the greeting.", &items);
2206 assert_eq!(body, "Fix the greeting.\n\n## Definition of done\n\n- `cargo test` passes.");
2207 assert_eq!(with_definition_of_done(&body, &items), body);
2208 assert_eq!(with_definition_of_done("", &items), "## Definition of done\n\n- `cargo test` passes.");
2209 assert_eq!(with_definition_of_done(" Text ", &[]), "Text");
2210 }
2211}
2212
2213#[cfg(test)]
2214mod authorship_tests {
2215 use super::*;
2216 use crate::PrincipalKind;
2217 use crate::credentials::{Acting, Principal};
2218 use crate::identity::{AGENT_ID, AgentScope};
2219
2220 fn person() -> User {
2221 User {
2222 id: "usr_1".into(),
2223 username: "syntaqx".into(),
2224 verified: true,
2225 ..User::default()
2226 }
2227 }
2228
2229 fn agent_for(id: &str, username: &str) -> User {
2230 User {
2231 id: AGENT_ID.into(),
2232 username: "g1t".into(),
2233 kind: PrincipalKind::Agent,
2234 acting: Some(Box::new(Acting {
2235 credential_id: "tok_1".into(),
2236 agent: "g1t".into(),
2237 on_behalf_of: Principal { id: id.into(), username: username.into() },
2238 scope: AgentScope {
2239 repo: RepoPath { namespace: "acme".into(), name: "web".into() },
2240 operations: Vec::new(),
2241 run: None,
2242 },
2243 })),
2244 ..User::default()
2245 }
2246 }
2247
2248 fn pull(author: User, requested_by: Option<User>) -> Pull {
2249 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2250 "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": 12, "title": "Fix it", "body": null,
2251 "agent": "g1t", "runtime": "hosted", "status": "open", "fork": null, "forkRepoId": null,
2252 "branch": null, "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2253 "supersededBy": null, "checkStatus": null,
2254 "author": { "id": "x", "username": "x" },
2255 "createdAt": "", "updatedAt": ""
2256 }))
2257 .unwrap();
2258 pull.author = author;
2259 pull.requested_by = requested_by;
2260 pull
2261 }
2262
2263 #[test]
2264 fn a_change_a_person_has_g1t_make_is_g1t_s_requested_by_them() {
2265 let (author, asked) = authorship(&person(), true);
2266 assert_eq!((author.id.as_str(), author.username.as_str(), author.kind), (AGENT_ID, "g1t", PrincipalKind::Agent));
2267 let asked = asked.expect("the person asked for it");
2268 assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
2269 }
2270
2271 #[test]
2272 fn what_g1t_s_agent_files_at_work_is_g1t_s_requested_by_whoever_it_works_for() {
2273 let (author, asked) = authorship(&agent_for("usr_1", "syntaqx"), false);
2274 assert_eq!(author.id, AGENT_ID);
2275 assert_eq!(asked.map(|user| user.username), Some("syntaqx".into()));
2276 }
2277
2278 #[test]
2279 fn nobody_asked_for_g1t_s_own_work() {
2280 // A run g1t started itself acts for g1t, not for a person.
2281 let (author, asked) = authorship(&agent_for(crate::system::ID, "g1t"), false);
2282 assert_eq!(author.id, AGENT_ID);
2283 assert!(asked.is_none());
2284 // A security update g1t opens is g1t's own, as before.
2285 let (author, asked) = authorship(&User::system("acme"), true);
2286 assert_eq!((author.id.as_str(), author.kind), (crate::system::ID, PrincipalKind::System));
2287 assert!(asked.is_none());
2288 }
2289
2290 #[test]
2291 fn anyone_else_opens_their_own() {
2292 let (author, asked) = authorship(&person(), false);
2293 assert_eq!((author.id.as_str(), author.kind), ("usr_1", PrincipalKind::User));
2294 assert!(asked.is_none());
2295 // Nothing but who they are is kept.
2296 assert!(author.workspaces.is_empty() && author.acting.is_none());
2297 }
2298
2299 #[test]
2300 fn the_requester_owns_g1t_s_pull_request_and_an_author_their_own() {
2301 let made = pull(g1t_author(), Some(person()));
2302 assert_eq!(made.owner().id, "usr_1");
2303 assert!(made.is_owned_by("usr_1"));
2304 assert!(!made.is_owned_by(AGENT_ID), "g1t's agent does not answer for its own change");
2305 let own = pull(person(), None);
2306 assert_eq!(own.owner().id, "usr_1");
2307 assert!(own.is_owned_by("usr_1"));
2308 }
2309
2310 #[test]
2311 fn requested_by_is_null_when_nobody_asked_and_read_as_absent_from_older_senders() {
2312 let own = pull(person(), None);
2313 let sent = serde_json::to_value(&own).unwrap();
2314 assert!(sent["requestedBy"].is_null());
2315 let mut older = sent.clone();
2316 older.as_object_mut().unwrap().remove("requestedBy");
2317 let read: Pull = serde_json::from_value(older).unwrap();
2318 assert!(read.requested_by.is_none());
2319 }
2320
2321 #[test]
2322 fn a_comment_is_changed_by_its_author_or_a_maintainer() {
2323 use CommentKind::{Comment, Event};
2324 // Its author edits and deletes it; someone else needs Maintain.
2325 assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, false).is_ok());
2326 assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, true).is_ok());
2327 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, false).is_err());
2328 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, true).is_err());
2329 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, false).is_ok());
2330 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, true).is_ok());
2331 // A review's text can be edited, but its verdict stays.
2332 assert!(may_change_comment(Comment, true, "usr_1", "usr_1", false, false).is_ok());
2333 assert!(may_change_comment(Comment, true, "usr_1", "usr_1", true, true).is_err());
2334 // A note of what happened is nobody's to change.
2335 assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, false).is_err());
2336 assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, true).is_err());
2337 }
2338}