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