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