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