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