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