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