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