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.

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