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,800 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 "work_item",
1095 "work_item_comment",
1096 "repository_dispatch",
1097];
1098
1099/// A status context's check name and the event it was reported for:
1100/// `CI / pull_request` is `("CI", Some("pull_request"))`. A context that
1101/// does not end in an event, such as `g1t / deploy`, is its own name.
1102pub fn check_name(context: &str) -> (&str, Option<&str>) {
1103 match context.rsplit_once(" / ") {
1104 Some((name, event)) if STATUS_EVENTS.contains(&event) && !name.trim().is_empty() => (name, Some(event)),
1105 _ => (context, None),
1106 }
1107}
1108
1109/// Where each required check stands among a commit's statuses. A check is
1110/// met by any status of that name, for any event: one that failed fails
1111/// it, one still running holds it, and with neither, one that passed
1112/// passes it. Names compare without regard to case.
1113pub fn required_checks(required: &[String], statuses: &[CommitStatus]) -> Vec<RequiredCheck> {
1114 required
1115 .iter()
1116 .map(|name| {
1117 let matching: Vec<&CommitStatus> = statuses
1118 .iter()
1119 .filter(|status| check_name(&status.context).0.eq_ignore_ascii_case(name.trim()))
1120 .collect();
1121 let failed = matching.iter().find(|s| s.state == "failure" || s.state == "error");
1122 let pending = matching.iter().find(|s| s.state == "pending");
1123 let passed = matching.iter().find(|s| s.state == "success");
1124 let (state, decided) = match (failed, pending, passed) {
1125 (Some(status), _, _) => (RequiredState::Failure, Some(*status)),
1126 (None, Some(status), _) => (RequiredState::Pending, Some(*status)),
1127 (None, None, Some(status)) => (RequiredState::Success, Some(*status)),
1128 _ => (RequiredState::Expected, None),
1129 };
1130 RequiredCheck {
1131 name: name.trim().to_owned(),
1132 state,
1133 description: decided.and_then(|status| status.description.clone()),
1134 target_url: decided.and_then(|status| status.target_url.clone()),
1135 }
1136 })
1137 .collect()
1138}
1139
1140/// A repository's required check names, tidied: trimmed, without blanks
1141/// or repeats (ignoring case), at most [`MAX_REQUIRED_CHECKS`].
1142pub fn tidy_required(names: &[String]) -> Vec<String> {
1143 let mut out: Vec<String> = Vec::new();
1144 for name in names {
1145 let name: String = name.trim().chars().take(MAX_CHECK_NAME_CHARS).collect();
1146 if !name.is_empty() && !out.iter().any(|kept| kept.eq_ignore_ascii_case(&name)) {
1147 out.push(name);
1148 }
1149 }
1150 out.truncate(MAX_REQUIRED_CHECKS);
1151 out
1152}
1153
1154/// The most checks a branch can require, and the longest name of one.
1155pub const MAX_REQUIRED_CHECKS: usize = 20;
1156pub const MAX_CHECK_NAME_CHARS: usize = 100;
1157
1158/// A check name seen on the repository's commits recently, for choosing
1159/// required checks: what reported it, and for which events.
1160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1161#[serde(rename_all = "camelCase")]
1162pub struct SeenCheck {
1163 pub name: String,
1164 /// The events it was reported for, such as `pull_request`; empty for a
1165 /// status that names none, such as a deployment's.
1166 pub events: Vec<String>,
1167 /// RFC 3339. The latest report.
1168 pub last_seen: String,
1169}
1170
1171/// `seen_checks`: the check names reported on a repository's commits in
1172/// the last 30 days, most recent first. Returns `Outcome<Vec<SeenCheck>>`.
1173#[derive(Debug, Serialize, Deserialize)]
1174pub struct SeenChecksArgs {
1175 pub repo: RepoPath,
1176 pub viewer: Viewer,
1177}
1178
1179/// The heading a work item's plain-words description of done goes under.
1180pub const DEFINITION_OF_DONE: &str = "## Definition of done";
1181
1182/// A work item's body with `items` added as a bulleted "Definition of done"
1183/// section, for the agent and reviewers to read. Nothing is added when
1184/// `items` is empty, or when the body already has the section, so folding
1185/// the same items twice changes nothing.
1186pub fn with_definition_of_done(body: &str, items: &[String]) -> String {
1187 let items: Vec<&str> = items.iter().map(|item| item.trim()).filter(|item| !item.is_empty()).collect();
1188 let body = body.trim();
1189 if items.is_empty() || body.contains(DEFINITION_OF_DONE) {
1190 return body.to_owned();
1191 }
1192 let list = items.iter().map(|item| format!("- {item}")).collect::<Vec<_>>().join("\n");
1193 if body.is_empty() {
1194 format!("{DEFINITION_OF_DONE}\n\n{list}")
1195 } else {
1196 format!("{body}\n\n{DEFINITION_OF_DONE}\n\n{list}")
1197 }
1198}
1199
1200/// Commands, as items of a definition of done: "`npm test` passes."
1201pub fn commands_pass(commands: &[String]) -> Vec<String> {
1202 commands
1203 .iter()
1204 .map(|command| command.trim())
1205 .filter(|command| !command.is_empty())
1206 .map(|command| format!("`{}` passes.", command.replace('`', "'")))
1207 .collect()
1208}
1209
1210/// Whether a pull request's change merges cleanly into the branch it
1211/// targets.
1212#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1213#[serde(rename_all = "lowercase")]
1214pub enum Mergeable {
1215 /// It merges without conflicts.
1216 Clean,
1217 /// Some files conflict: see `PullDetail::conflicts`.
1218 Conflicting,
1219 /// Not known: never worked out, or it could not be.
1220 #[default]
1221 Unknown,
1222 /// Being worked out now.
1223 Checking,
1224}
1225
1226impl Mergeable {
1227 pub fn as_str(self) -> &'static str {
1228 match self {
1229 Mergeable::Clean => "clean",
1230 Mergeable::Conflicting => "conflicting",
1231 Mergeable::Unknown => "unknown",
1232 Mergeable::Checking => "checking",
1233 }
1234 }
1235
1236 pub fn parse(value: Option<&str>) -> Mergeable {
1237 match value {
1238 Some("clean") => Mergeable::Clean,
1239 Some("conflicting") => Mergeable::Conflicting,
1240 Some("checking") => Mergeable::Checking,
1241 _ => Mergeable::Unknown,
1242 }
1243 }
1244}
1245
1246/// `start_mergecheck`: claims the probe of whether a pull request merges
1247/// cleanly, which the work service asked for with a `pull.mergecheck`
1248/// event. Called by the runner service, which starts the sandbox. Refused
1249/// when it is no longer wanted, or when the repository already has as many
1250/// probes running as it may. Returns `Outcome<MergecheckJob>`.
1251#[derive(Debug, Serialize, Deserialize)]
1252#[serde(rename_all = "camelCase")]
1253pub struct StartMergecheckArgs {
1254 pub pull_id: String,
1255}
1256
1257/// What a sandbox needs to find out whether a pull request merges cleanly.
1258#[derive(Debug, Serialize, Deserialize)]
1259#[serde(rename_all = "camelCase")]
1260pub struct MergecheckJob {
1261 pub pull_id: String,
1262 /// Lets the sandbox, and nothing else, report this probe.
1263 pub token: String,
1264 pub repo: RepoPath,
1265 pub number: u32,
1266 pub default_branch: String,
1267 /// The default branch's commit to merge into.
1268 pub base: String,
1269 /// The repository holding the change: its fork, or the repository.
1270 pub source: RepoPath,
1271 /// The branch of `source` holding it.
1272 pub branch: String,
1273 /// The change's commit.
1274 pub head: String,
1275 /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
1276 /// it, or its author), and so can read its source.
1277 pub author: User,
1278}
1279
1280/// `report_mergecheck`: what a sandbox found. Returns `Outcome<Mergeable>`.
1281#[derive(Debug, Serialize, Deserialize)]
1282#[serde(rename_all = "camelCase")]
1283pub struct ReportMergecheckArgs {
1284 pub pull_id: String,
1285 pub token: String,
1286 /// The files that conflict; empty when it merges cleanly.
1287 #[serde(default)]
1288 pub conflicts: Vec<String>,
1289 /// Why it could not be found out.
1290 #[serde(default)]
1291 pub error: Option<String>,
1292}
1293
1294/// What a workflow run (or another tool) says about a commit.
1295#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1296#[serde(rename_all = "camelCase")]
1297pub struct CommitStatus {
1298 /// What reported it, such as `CI / push`.
1299 pub context: String,
1300 /// `pending`, `success`, `failure` or `error`.
1301 pub state: String,
1302 pub description: Option<String>,
1303 /// Where to see more, such as the run's page.
1304 pub target_url: Option<String>,
1305 pub updated_at: String,
1306 /// The integration that reported it (`actions`, `deployments`,
1307 /// `security`, `g1t`), when recorded. A required check can insist on
1308 /// one (`rules::RequiredCheck::integration`).
1309 #[serde(default, skip_serializing_if = "Option::is_none")]
1310 pub source: Option<String>,
1311 /// Set on the status a check run stands as (see `checks`): the check
1312 /// run's id. Such a status is listed as its check run, not again.
1313 #[serde(default, skip_serializing_if = "Option::is_none")]
1314 pub check_run_id: Option<String>,
1315}
1316
1317/// `set_commit_status`: for services only. Returns `Outcome<bool>`.
1318#[derive(Debug, Serialize, Deserialize)]
1319#[serde(rename_all = "camelCase")]
1320pub struct SetCommitStatusArgs {
1321 pub repo_id: String,
1322 pub sha: String,
1323 pub context: String,
1324 pub state: String,
1325 #[serde(default)]
1326 pub description: Option<String>,
1327 #[serde(default)]
1328 pub target_url: Option<String>,
1329 /// The integration reporting it: `actions`, `deployments`, `security`
1330 /// or `g1t`.
1331 #[serde(default)]
1332 pub source: Option<String>,
1333}
1334
1335/// A step on the way from an assigned work item to a pull request that is ready
1336/// to merge. g1t takes each one without being asked.
1337#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1338#[serde(rename_all = "snake_case")]
1339pub enum Stage {
1340 /// The agent is making the change.
1341 Working,
1342 /// Waiting for the checks workflows report on its head.
1343 Checking,
1344 /// g1t is reviewing it.
1345 Reviewing,
1346 /// The agent is addressing failed checks or a review.
1347 Revising,
1348 /// The agent is merging in the branch it would land on, which moved.
1349 CatchingUp,
1350 /// Woken to answer a question another agent asked it, or a handoff.
1351 Answering,
1352 /// In the repository's merge queue, being tested with what is ahead of
1353 /// it before it lands.
1354 Queued,
1355 /// Required checks passed, reviewed and approved, up to date. A
1356 /// person merges.
1357 Ready,
1358 /// g1t has stopped and a person has to decide what happens next.
1359 NeedsYou,
1360}
1361
1362/// Where a pull request g1t's retired agents made stands, for those still
1363/// open. See [`Stage`].
1364#[derive(Clone, Debug, Serialize, Deserialize)]
1365pub struct Lifecycle {
1366 pub stage: Stage,
1367 /// One sentence saying what is happening, or why it stopped.
1368 pub detail: String,
1369 /// How many times the agent has been sent back to revise it.
1370 pub revisions: u32,
1371}
1372
1373/// `advance`: merges a pull request g1t is seeing through once it is ready,
1374/// where the repository's settings say ready pull requests land by
1375/// themselves. Called by the runner service. Returns `Advance`.
1376#[derive(Debug, Serialize, Deserialize)]
1377#[serde(rename_all = "camelCase")]
1378pub struct AdvanceArgs {
1379 pub pull_id: String,
1380}
1381
1382/// What the runner is to do next for a pull request: always nothing now,
1383/// since no step of a pull request is carried out in a sandbox any more.
1384#[derive(Debug, Serialize, Deserialize)]
1385#[serde(tag = "action", rename_all = "snake_case")]
1386pub enum Advance {
1387 /// Nothing to do.
1388 None,
1389}
1390
1391/// How a repository wants its pull requests handled. A repository that has
1392/// changed nothing has the defaults.
1393#[derive(Clone, Debug, Serialize, Deserialize)]
1394#[serde(rename_all = "camelCase", default)]
1395pub struct RepoSettings {
1396 /// Land a pull request g1t is seeing through without a person once it
1397 /// is ready: required checks passed and approved as the settings below
1398 /// require.
1399 pub auto_merge: bool,
1400 /// The checks that must pass on a pull request's head before it may
1401 /// merge into the default branch, by name: a workflow's name (`CI`), or
1402 /// the context of another status (`g1t / deploy`). The same for a
1403 /// person's pull request and an agent's, and for the merge queue.
1404 pub required_checks: Vec<String>,
1405 /// Refuse to merge a pull request that does not contain the default
1406 /// branch's latest commits, so that what merges is what was checked.
1407 /// When off, merging one that is behind brings it up to date first.
1408 pub require_up_to_date: bool,
1409 /// How many approving reviews a pull request needs before it may
1410 /// merge. A reviewer who has since asked for changes blocks it.
1411 pub required_approvals: u32,
1412 /// Whether an agent's approval counts towards `required_approvals`.
1413 pub count_agent_approvals: bool,
1414 /// Whether someone who may merge can bypass required checks that have
1415 /// not passed, by saying so as they merge.
1416 pub allow_ignoring_checks: bool,
1417 /// Merge through a queue: pull requests are tested together with those
1418 /// ahead of them, and only a combination that passed reaches the default
1419 /// branch.
1420 pub merge_queue: bool,
1421 /// Refuse to merge until the code owners of every file it changes
1422 /// (its CODEOWNERS file) have approved, as many as each section asks.
1423 /// Only people count, and `g1t` only where the file names `@g1t`.
1424 pub require_code_owner_review: bool,
1425 /// Whether work items can be filed from it (with it as their origin):
1426 /// null for the default, which is on for a repository and off for a
1427 /// fork of one. Work items filed from it before stay readable either
1428 /// way, and it can always be linked as a target.
1429 pub has_work_items: Option<bool>,
1430 /// Whether work items can be filed from it now: `has_work_items`, or
1431 /// its default. Filled in by the service; `update_settings` ignores it.
1432 pub work_items_enabled: bool,
1433 /// Which merge methods a pull request may use (at least one). Rules
1434 /// can narrow them for a branch (`allowed_merge_methods`).
1435 #[serde(default = "yes")]
1436 pub allow_merge_commit: bool,
1437 #[serde(default = "yes")]
1438 pub allow_squash_merge: bool,
1439 #[serde(default = "yes")]
1440 pub allow_rebase_merge: bool,
1441 #[serde(default)]
1442 pub allow_fast_forward: bool,
1443 /// The method offered first, and used when none is named.
1444 #[serde(default = "merge_method")]
1445 pub default_merge_method: MergeMethod,
1446 /// What merge and squash commits say unless their merger writes it.
1447 #[serde(default)]
1448 pub merge_commit_message: CommitMessageDefault,
1449 #[serde(default)]
1450 pub squash_commit_message: CommitMessageDefault,
1451 /// For `Custom`: a template whose `{title}`, `{number}`, `{body}`,
1452 /// `{branch}`, `{head_repo}`, `{base}`, `{author}`, `{commits}`,
1453 /// `{items}` and `{co_authors}` are filled in.
1454 #[serde(default)]
1455 pub merge_commit_template: Option<String>,
1456 #[serde(default)]
1457 pub squash_commit_template: Option<String>,
1458 /// Offer to bring a pull request up to date whenever its base has new
1459 /// commits, not only when it must be.
1460 #[serde(default = "yes")]
1461 pub suggest_updating_branches: bool,
1462 /// Let people turn on auto-merge for a pull request.
1463 #[serde(default = "yes")]
1464 pub allow_auto_merge: bool,
1465 /// Delete a pull request's branch once it merges (restorable from it).
1466 #[serde(default)]
1467 pub delete_branch_on_merge: bool,
1468 /// Commits made on g1t.sh (web edits, merges, squashes) carry a
1469 /// `Signed-off-by:` trailer for whoever made them, agreeing to the
1470 /// repository's terms, commonly the Developer Certificate of Origin.
1471 #[serde(default)]
1472 pub require_web_commit_sign_off: bool,
1473 /// Let anyone who can read it comment on its commits. Existing
1474 /// comments stay readable either way.
1475 #[serde(default = "yes")]
1476 pub allow_commit_comments: bool,
1477 /// Mark the work items a pull request names (its item, and `Fixes #12`
1478 /// in its title or description) landed in this repository when it
1479 /// merges into the default branch, closing those with nothing else
1480 /// left to land; and the same for commits pushed there.
1481 #[serde(default = "yes")]
1482 pub auto_close_items: bool,
1483 /// Username of the member who last changed the settings, if anyone has.
1484 pub updated_by: Option<String>,
1485 /// RFC 3339.
1486 pub updated_at: Option<String>,
1487}
1488
1489impl Default for RepoSettings {
1490 fn default() -> Self {
1491 RepoSettings {
1492 auto_merge: false,
1493 required_checks: Vec::new(),
1494 require_up_to_date: false,
1495 required_approvals: 0,
1496 count_agent_approvals: true,
1497 allow_ignoring_checks: true,
1498 merge_queue: false,
1499 require_code_owner_review: false,
1500 has_work_items: None,
1501 work_items_enabled: true,
1502 allow_merge_commit: true,
1503 allow_squash_merge: true,
1504 allow_rebase_merge: true,
1505 allow_fast_forward: false,
1506 default_merge_method: MergeMethod::Merge,
1507 merge_commit_message: CommitMessageDefault::Default,
1508 squash_commit_message: CommitMessageDefault::Default,
1509 merge_commit_template: None,
1510 squash_commit_template: None,
1511 suggest_updating_branches: true,
1512 allow_auto_merge: true,
1513 delete_branch_on_merge: false,
1514 require_web_commit_sign_off: false,
1515 allow_commit_comments: true,
1516 auto_close_items: true,
1517 updated_by: None,
1518 updated_at: None,
1519 }
1520 }
1521}
1522
1523impl RepoSettings {
1524 /// Whether work items can be filed from it: as `has_work_items` says,
1525 /// else on unless the repository is a fork (`is_fork`).
1526 pub fn work_items_on(&self, is_fork: bool) -> bool {
1527 self.has_work_items.unwrap_or(!is_fork)
1528 }
1529
1530 /// Whether the repository lets pull requests merge by `method`.
1531 pub fn allows(&self, method: MergeMethod) -> bool {
1532 match method {
1533 MergeMethod::Merge => self.allow_merge_commit,
1534 MergeMethod::Squash => self.allow_squash_merge,
1535 MergeMethod::Rebase => self.allow_rebase_merge,
1536 MergeMethod::FastForward => self.allow_fast_forward,
1537 }
1538 }
1539
1540 /// The methods it allows, in the order they are offered.
1541 pub fn allowed_methods(&self) -> Vec<MergeMethod> {
1542 MergeMethod::ALL.into_iter().filter(|method| self.allows(*method)).collect()
1543 }
1544}
1545
1546fn yes() -> bool {
1547 true
1548}
1549
1550fn merge_method() -> MergeMethod {
1551 MergeMethod::Merge
1552}
1553
1554/// A comment on a commit: on the whole commit, or on one line of a file it
1555/// changed (`path` and `line`, a line of the file as the commit left it).
1556#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1557#[serde(rename_all = "camelCase")]
1558pub struct CommitComment {
1559 pub id: String,
1560 pub repo_id: String,
1561 /// The commit's full hash.
1562 pub commit: String,
1563 #[serde(default)]
1564 pub path: Option<String>,
1565 #[serde(default)]
1566 pub line: Option<u32>,
1567 /// Username.
1568 pub author: String,
1569 /// Markdown.
1570 pub body: String,
1571 /// RFC 3339.
1572 pub created_at: String,
1573 #[serde(default)]
1574 pub updated_at: Option<String>,
1575}
1576
1577/// `commit_comments`: a commit's comments, oldest first; with no `commit`,
1578/// the repository's, newest first, a page of 30 at a time. Returns
1579/// `Outcome<Vec<CommitComment>>`.
1580#[derive(Debug, Serialize, Deserialize)]
1581pub struct CommitCommentsArgs {
1582 pub viewer: Viewer,
1583 pub repo: RepoPath,
1584 #[serde(default)]
1585 pub commit: Option<String>,
1586 #[serde(default)]
1587 pub page: Option<u32>,
1588}
1589
1590/// `closed_by_commit`: the work items a commit pushed to a repository's
1591/// default branch resolved by naming them in its message, for anyone who
1592/// may read the repository. Returns `Outcome<Vec<ClosedItem>>`.
1593#[derive(Debug, Serialize, Deserialize)]
1594pub struct ClosedByCommitArgs {
1595 pub viewer: Viewer,
1596 pub repo: RepoPath,
1597 pub commit: String,
1598}
1599
1600/// A work item a commit resolved: it landed in the commit's repository,
1601/// and closed once nothing else was left to land.
1602#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1603pub struct ClosedItem {
1604 pub workspace: String,
1605 pub number: u32,
1606 pub title: String,
1607 /// Whether the work item is closed now.
1608 #[serde(default)]
1609 pub closed: bool,
1610}
1611
1612/// `add_commit_comment`: needs Read (refused where the repository turned
1613/// commit comments off), and the commit must exist. Notifies the commit's
1614/// author and anyone mentioned; publishes `commit_comment.created`.
1615/// Returns `Outcome<CommitComment>`.
1616#[derive(Debug, Serialize, Deserialize)]
1617pub struct AddCommitCommentArgs {
1618 pub actor: User,
1619 pub repo: RepoPath,
1620 pub commit: String,
1621 pub body: String,
1622 #[serde(default)]
1623 pub path: Option<String>,
1624 #[serde(default)]
1625 pub line: Option<u32>,
1626}
1627
1628/// `edit_commit_comment` (its author) and `delete_commit_comment` (its
1629/// author, or Maintain and up). `body` is ignored when deleting. Returns
1630/// `Outcome<CommitComment>` and `Outcome<bool>`.
1631#[derive(Debug, Serialize, Deserialize)]
1632pub struct CommitCommentArgs {
1633 pub actor: User,
1634 pub repo: RepoPath,
1635 pub id: String,
1636 #[serde(default)]
1637 pub body: String,
1638}
1639
1640/// `update_settings`: replaces a repository's settings. Members of its
1641/// workspace only. Returns `Outcome<RepoSettings>`. `get_settings` takes
1642/// `ViewArgs` and returns the same.
1643#[derive(Debug, Serialize, Deserialize)]
1644pub struct UpdateSettingsArgs {
1645 pub actor: User,
1646 pub repo: RepoPath,
1647 /// Who changed them and when are filled in by the service.
1648 pub settings: RepoSettings,
1649}
1650
1651/// What the work service records when a step could not be carried out, so
1652/// that g1t stops and a person is asked. Not served over RPC.
1653#[derive(Debug, Serialize, Deserialize)]
1654#[serde(rename_all = "camelCase")]
1655pub struct StallArgs {
1656 pub pull_id: String,
1657 pub reason: String,
1658 /// The person who stopped it, by id, when someone did: they are not
1659 /// told it needs them.
1660 #[serde(default)]
1661 pub by: Option<String>,
1662}
1663
1664/// `managed_pulls`: ids of the open pull requests to move along (`advance`):
1665/// those g1t merges by itself or someone set to auto-merge, in one
1666/// repository or in all of them; in one repository, also every open one
1667/// whose lifecycle is remembered, to bring it up to date after a merge
1668/// there. Returns `Vec<String>`.
1669#[derive(Debug, Default, Serialize, Deserialize)]
1670#[serde(rename_all = "camelCase")]
1671pub struct ManagedPullsArgs {
1672 #[serde(default)]
1673 pub repo_id: Option<String>,
1674}
1675
1676/// `list_pulls`, newest first. Returns `Outcome<Vec<Pull>>`.
1677#[derive(Debug, Serialize, Deserialize)]
1678pub struct ListPullsArgs {
1679 pub repo: RepoPath,
1680 pub viewer: Viewer,
1681 #[serde(default)]
1682 pub state: Option<State>,
1683 /// Only pull requests carrying this label.
1684 #[serde(default)]
1685 pub label: Option<String>,
1686 /// Only pull requests in the milestone of this number.
1687 #[serde(default)]
1688 pub milestone: Option<u32>,
1689 /// Only pull requests into this branch.
1690 #[serde(default)]
1691 pub base: Option<String>,
1692}
1693
1694/// `pulls_for_repos`: the newest open and the newest closed pull requests
1695/// of many repositories, in one call, for pages that show several projects
1696/// at once. Repositories the viewer cannot read are left out, as are forks
1697/// (ask those with `list_pulls`). Returns `Vec<RepoPulls>`.
1698#[derive(Debug, Serialize, Deserialize)]
1699#[serde(rename_all = "camelCase")]
1700pub struct PullsForReposArgs {
1701 /// At most [`MAX_PULLS_FOR_REPOS`] are looked at.
1702 pub repo_ids: Vec<String>,
1703 pub viewer: Viewer,
1704 /// How many of each, open and closed, per repository (at most 100).
1705 pub limit: u32,
1706}
1707
1708/// The most repositories one `pulls_for_repos` call looks at.
1709pub const MAX_PULLS_FOR_REPOS: usize = 50;
1710
1711/// One repository's pull requests from `pulls_for_repos`, newest first.
1712#[derive(Debug, Serialize, Deserialize)]
1713#[serde(rename_all = "camelCase")]
1714pub struct RepoPulls {
1715 pub repo_id: String,
1716 /// Draft and open.
1717 pub open: Vec<Pull>,
1718 /// Merged and closed.
1719 pub closed: Vec<Pull>,
1720}
1721
1722/// `get_pull` (`Outcome<PullDetail>`), `read_session`
1723/// (`Outcome<Vec<SessionEntry>>`) and `counts` (`Outcome<Counts>`, which
1724/// ignores `number`).
1725#[derive(Debug, Serialize, Deserialize)]
1726#[serde(rename_all = "camelCase")]
1727pub struct ViewArgs {
1728 pub repo: RepoPath,
1729 #[serde(default)]
1730 pub number: u32,
1731 pub viewer: Viewer,
1732 /// For `read_session`: only entries after this sequence number.
1733 #[serde(default)]
1734 pub after_seq: u32,
1735}
1736
1737/// How many work items linked to a repository, and how many of its pull
1738/// requests, are open.
1739#[derive(Debug, Serialize, Deserialize)]
1740pub struct Counts {
1741 pub items: u32,
1742 pub pulls: u32,
1743}
1744
1745/// `add_comment`, on a thread: a pull request or a work item. Every method
1746/// that takes a thread reads it the same way: with `repo`, the pull
1747/// request `number` of that repository; without, the work item `number`
1748/// of `workspace`. On a pull request a comment may name a line of the
1749/// change, and may carry a verdict; nobody can give a verdict on their own
1750/// pull request. Returns `Outcome<Comment>`.
1751#[derive(Debug, Serialize, Deserialize)]
1752pub struct AddCommentArgs {
1753 pub actor: User,
1754 #[serde(default)]
1755 pub repo: Option<RepoPath>,
1756 #[serde(default)]
1757 pub workspace: String,
1758 pub number: u32,
1759 /// May be empty when approving.
1760 #[serde(default)]
1761 pub body: String,
1762 #[serde(default)]
1763 pub path: Option<String>,
1764 #[serde(default)]
1765 pub line: Option<u32>,
1766 #[serde(default)]
1767 pub verdict: Option<Verdict>,
1768}
1769
1770/// `edit_comment` and `delete_comment`: change the text of a comment, or
1771/// delete it: one on a pull request of `repo`, or without `repo`, one on a
1772/// work item of `workspace`. Its author may, and so may anyone with the
1773/// Maintain role or higher ([`may_change_comment`]). Notes of what
1774/// happened (`CommentKind::Event`) cannot be changed, and a review that
1775/// gave a verdict cannot be deleted, only edited. `edit_comment` returns
1776/// `Outcome<Comment>`; `delete_comment` returns `Outcome<()>`.
1777#[derive(Debug, Serialize, Deserialize)]
1778#[serde(rename_all = "camelCase")]
1779pub struct CommentActionArgs {
1780 pub actor: User,
1781 #[serde(default)]
1782 pub repo: Option<RepoPath>,
1783 #[serde(default)]
1784 pub workspace: String,
1785 pub comment_id: String,
1786 /// For `edit_comment`: the new text, in Markdown.
1787 #[serde(default)]
1788 pub body: String,
1789}
1790
1791/// Why `actor_id` may not edit (or, with `deleting`, delete) a comment by
1792/// `author_id`, or none when they may: its author may, and so may someone
1793/// who `maintains` the repository (the Maintain role or higher). A note of
1794/// something that happened is never changed, and a review's verdict stays,
1795/// so a comment that gave one can be edited but not deleted.
1796pub fn may_change_comment(
1797 kind: CommentKind,
1798 has_verdict: bool,
1799 author_id: &str,
1800 actor_id: &str,
1801 maintains: bool,
1802 deleting: bool,
1803) -> Result<(), &'static str> {
1804 if kind == CommentKind::Event {
1805 return Err("That is a record of something that happened, not a comment; it cannot be changed.");
1806 }
1807 if author_id != actor_id && !maintains {
1808 return Err(if deleting {
1809 "Only its author, or someone with the Maintain role or higher, can delete this comment."
1810 } else {
1811 "Only its author, or someone with the Maintain role or higher, can edit this comment."
1812 });
1813 }
1814 if deleting && has_verdict {
1815 return Err("A review cannot be deleted. Edit its text instead.");
1816 }
1817 Ok(())
1818}
1819
1820/// `open_pull`. Without `branch`, forks the repo and returns a draft pull
1821/// request to push to. With it, opens a pull request, ready for review,
1822/// for a branch already pushed to the repo, or to `head_repo`, a fork of
1823/// it. Its number is the next in its workspace's sequence. Opening one for
1824/// a work item links the repository to the item (`touched`) unless it is
1825/// linked already. Returns `Outcome<Pull>`.
1826#[derive(Debug, Serialize, Deserialize)]
1827pub struct OpenPullArgs {
1828 pub actor: User,
1829 pub repo: RepoPath,
1830 /// The work item this is for, by number in the repository's workspace.
1831 #[serde(default)]
1832 pub item: Option<u32>,
1833 /// Defaults to the work item's title; required without one.
1834 #[serde(default)]
1835 pub title: String,
1836 /// What changed and why. Usually set later, when a draft is marked ready.
1837 #[serde(default)]
1838 pub body: String,
1839 /// A branch of the repository that already holds the change: of
1840 /// `head_repo` when given.
1841 #[serde(default)]
1842 pub branch: Option<String>,
1843 /// A fork of the repository (directly or through other forks) whose
1844 /// `branch` holds the change. Needs `branch`.
1845 #[serde(default)]
1846 pub head_repo: Option<RepoPath>,
1847 #[serde(default)]
1848 pub agent: String,
1849 pub runtime: Runtime,
1850 /// The branch to merge into: the default branch when absent.
1851 #[serde(default)]
1852 pub base: Option<String>,
1853 /// Opened from a branch as a draft, still being worked on: it can't
1854 /// merge, and agents' review routines wait, until it is marked ready.
1855 #[serde(default)]
1856 pub draft: bool,
1857}
1858
1859/// A person's request that a pull request merge on its own once its
1860/// checks, reviews and rules allow (`set_auto_merge`). Cleared when it
1861/// merges or closes, when someone turns it off, and when someone other
1862/// than who asked pushes to it.
1863#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1864#[serde(rename_all = "camelCase")]
1865pub struct AutoMerge {
1866 pub method: MergeMethod,
1867 /// The commit message's first line and the rest, for `merge` and
1868 /// `squash`; the repository's default when absent.
1869 #[serde(default)]
1870 pub commit_title: Option<String>,
1871 #[serde(default)]
1872 pub commit_message: Option<String>,
1873 /// Username of who asked.
1874 pub enabled_by: String,
1875 /// RFC 3339.
1876 pub enabled_at: String,
1877}
1878
1879/// `set_auto_merge`: turns a pull request's auto-merge on (with how to
1880/// merge) or off. Turning it on needs the Write role, since it merges as
1881/// whoever turned it on; its author may also turn it off. Refused where
1882/// the repository does not allow auto-merge, and for a method the
1883/// repository or its rules do not allow. A pull request that may merge
1884/// already merges at once instead. Publishes `pull.auto_merge_enabled` or
1885/// `pull.auto_merge_disabled`. Returns `Outcome<Pull>`.
1886#[derive(Debug, Serialize, Deserialize)]
1887#[serde(rename_all = "camelCase")]
1888pub struct SetAutoMergeArgs {
1889 pub actor: User,
1890 pub repo: RepoPath,
1891 pub number: u32,
1892 pub enabled: bool,
1893 /// The repository's default method when absent.
1894 #[serde(default)]
1895 pub merge_method: Option<MergeMethod>,
1896 #[serde(default)]
1897 pub commit_title: Option<String>,
1898 #[serde(default)]
1899 pub commit_message: Option<String>,
1900}
1901
1902/// `merge_choices`: how a pull request may merge now, for whoever is
1903/// looking: the merge box's choices and their default messages. Takes
1904/// `ViewArgs`. Returns `Outcome<MergeChoices>`.
1905#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1906#[serde(rename_all = "camelCase")]
1907pub struct MergeChoices {
1908 /// The methods it may merge by: the repository's, as the rules for the
1909 /// branch it merges into allow (linear history rules out merge
1910 /// commits). In the order offered.
1911 pub methods: Vec<MergeMethod>,
1912 /// The one offered first: the repository's default when it may be
1913 /// used, else the first of `methods`. Null when none may.
1914 pub default_method: Option<MergeMethod>,
1915 /// Each of `methods`, with its default commit message and whether it
1916 /// can be used now.
1917 pub options: Vec<MergeChoice>,
1918 /// Its base has commits it does not have.
1919 pub behind: bool,
1920 /// It contains its base's head, so it can fast-forward as it is.
1921 pub can_fast_forward: bool,
1922 /// Offer to bring it up to date: it is behind, and the repository
1923 /// suggests updating branches (or requires them up to date).
1924 pub suggest_update: bool,
1925 /// The repository requires it to be up to date before it merges.
1926 pub require_up_to_date: bool,
1927 /// It merges through the merge queue, which builds and tests it by the
1928 /// method chosen (the queue rule's when none is) and lands it as tested.
1929 pub merge_queue: bool,
1930 /// The viewer may turn on auto-merge for it: the repository allows it,
1931 /// it is open or a draft, and they may merge it.
1932 pub auto_merge_allowed: bool,
1933 /// Whether "delete the branch" starts ticked: the repository deletes
1934 /// branches on merge, and its branch can be deleted.
1935 pub delete_branch_default: bool,
1936 /// Its branch can be deleted once it merges: a branch of the
1937 /// repository (not its base) or of a fork, never a working copy.
1938 pub can_delete_branch: bool,
1939 /// Commits made merging it carry a `Signed-off-by:` trailer for the
1940 /// merger.
1941 pub sign_off: bool,
1942}
1943
1944/// One way a pull request may merge (`MergeChoices::options`).
1945#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1946#[serde(rename_all = "camelCase")]
1947pub struct MergeChoice {
1948 pub method: MergeMethod,
1949 /// For `merge` and `squash`: the commit's title and the rest of its
1950 /// message, as the repository's settings write them.
1951 #[serde(default)]
1952 pub commit_title: Option<String>,
1953 #[serde(default)]
1954 pub commit_message: Option<String>,
1955 /// Why it cannot merge this way now, if it cannot.
1956 #[serde(default)]
1957 pub unavailable: Option<String>,
1958}
1959
1960/// `ready_pull`, `close_pull`, `reopen_pull`, `convert_pull_to_draft` and
1961/// `merge_pull`. Each returns `Outcome<Pull>`. `reopen_pull` opens a closed
1962/// pull request again (never a merged one), as the draft it was if it was
1963/// closed as one; `convert_pull_to_draft` turns one that is open back into
1964/// a draft, which leaves the merge queue.
1965///
1966/// Also `delete_head_branch` (a merged or closed one's branch, for whoever
1967/// may push where it is; never a working copy's, which repos retires) and
1968/// `restore_head_branch` (makes it again at `head_commit`). Both publish
1969/// `pull.head_branch_deleted` or `pull.head_branch_restored` and return
1970/// `Outcome<Pull>`.
1971///
1972/// Also `catch_up_pull`: brings the pull request up to date with the
1973/// default branch without a sandbox where that is safe, as the repos
1974/// service's `update_pull_branch` does, after checking that `actor` may
1975/// update it: its owner ([`Pull::owner`]) for a fork, any member for a
1976/// branch.
1977/// Returns `Outcome<repos::PullBranchUpdate>`; on `needs_agent` nothing was
1978/// pushed and the runner's `update` is the way on.
1979#[derive(Debug, Serialize, Deserialize)]
1980#[serde(rename_all = "camelCase")]
1981pub struct PullActionArgs {
1982 pub actor: User,
1983 pub repo: RepoPath,
1984 pub number: u32,
1985 /// For `ready_pull`: what changed and why.
1986 #[serde(default)]
1987 pub summary: String,
1988 /// For `merge_pull`: leave the work item open and the other pull
1989 /// requests for it untouched, because this one is only part of the
1990 /// work: it is recorded as part of it, as `Part of #N` would be.
1991 #[serde(default)]
1992 pub keep_item_open: bool,
1993 /// For `merge_pull`: merge although required checks have not passed,
1994 /// where the repository lets members bypass them.
1995 #[serde(default)]
1996 pub ignore_checks: bool,
1997 /// For `merge_pull`: merge although rules are not met, where a ruleset
1998 /// lists the actor as one who may bypass it. Recorded as a bypass.
1999 #[serde(default)]
2000 pub bypass_rules: bool,
2001 /// For `merge_pull`: how to merge; the repository's default method
2002 /// when absent. Refused when the repository or a rule does not allow
2003 /// it.
2004 #[serde(default)]
2005 pub merge_method: Option<MergeMethod>,
2006 /// For `merge_pull` with `merge` or `squash`: the commit's title and
2007 /// the rest of its message; the repository's default when absent.
2008 #[serde(default)]
2009 pub commit_title: Option<String>,
2010 #[serde(default)]
2011 pub commit_message: Option<String>,
2012 /// For `merge_pull` (and `catch_up_pull`): refuse if the pull
2013 /// request's head is no longer this commit.
2014 #[serde(default)]
2015 pub expected_head: Option<String>,
2016 /// For `merge_pull`: delete its branch once merged; the repository's
2017 /// setting when absent.
2018 #[serde(default)]
2019 pub delete_branch: Option<bool>,
2020 /// For `catch_up_pull`: merge the base in (the default) or rebase.
2021 #[serde(default)]
2022 pub update_method: Option<UpdateMethod>,
2023}
2024
2025/// What a merge or squash commit's message says by default.
2026#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
2027#[serde(rename_all = "snake_case")]
2028pub enum CommitMessageDefault {
2029 /// Merge: "Merge pull request #12 from owner/branch" and its title.
2030 /// Squash: its title for one commit, else its title and the commits'
2031 /// messages.
2032 #[default]
2033 Default,
2034 /// Its title, with its number.
2035 PullTitle,
2036 /// Its title, and its description below.
2037 PullTitleAndDescription,
2038 /// Squash only: its title, and each commit's message below.
2039 PullTitleAndCommits,
2040 /// The repository's own template (`merge_commit_template` or
2041 /// `squash_commit_template`).
2042 Custom,
2043}
2044
2045/// `update_pull`: changes who a pull request is assigned to, whose review
2046/// is asked for, its labels, its milestone and the branch it merges into.
2047/// Each list given replaces the whole set. Whoever opened it, or someone
2048/// with the Triage role, may; changing the base needs the Write role.
2049/// Returns `Outcome<Pull>`.
2050#[derive(Debug, Serialize, Deserialize)]
2051pub struct UpdatePullArgs {
2052 pub actor: User,
2053 pub repo: RepoPath,
2054 pub number: u32,
2055 #[serde(default)]
2056 pub assignees: Option<Vec<String>>,
2057 /// May include `g1t`. Asking for its review does not by itself
2058 /// start one; the runner's `review` does. A team is named
2059 /// `workspace/team` (or `@workspace/team`): the whole list, people and
2060 /// teams, replaces who is asked.
2061 #[serde(default)]
2062 pub reviewers: Option<Vec<String>>,
2063 /// Its labels; replaces the whole set, as `set_labels` does.
2064 #[serde(default)]
2065 pub labels: Option<Vec<String>>,
2066 /// The number of the milestone to put it in; 0 takes it out. Needs the
2067 /// Triage role.
2068 #[serde(default)]
2069 pub milestone: Option<u32>,
2070 /// The branch it merges into: an existing branch other than its own.
2071 /// Needs the Write role.
2072 #[serde(default)]
2073 pub base: Option<String>,
2074}
2075
2076/// `report_checks`: what a sandbox says about its run. With no results and
2077/// no error it has started. `skip` forgets the run, for one that will not
2078/// be carried out. Returns `Outcome<CheckRun>`.
2079#[derive(Debug, Serialize, Deserialize)]
2080#[serde(rename_all = "camelCase")]
2081pub struct ReportChecksArgs {
2082 pub run_id: String,
2083 pub token: String,
2084 #[serde(default)]
2085 pub results: Vec<CheckResult>,
2086 #[serde(default)]
2087 pub error: Option<String>,
2088 #[serde(default)]
2089 pub skip: bool,
2090}
2091
2092/// A pull request in progress, with where it lives.
2093#[derive(Debug, Serialize, Deserialize)]
2094pub struct ActivePull {
2095 pub pull: Pull,
2096 pub item: Option<WorkItem>,
2097 /// Where it stands, for one g1t is seeing through.
2098 #[serde(default)]
2099 pub lifecycle: Option<Lifecycle>,
2100}
2101
2102/// `list_active_pulls`: drafts and open pull requests the viewer started,
2103/// most recently active first. Returns `Vec<ActivePull>`.
2104#[derive(Debug, Serialize, Deserialize)]
2105pub struct ViewerArgs {
2106 pub viewer: Viewer,
2107}
2108
2109/// `append_session`. Returns `Outcome<Appended>`.
2110#[derive(Debug, Serialize, Deserialize)]
2111pub struct AppendSessionArgs {
2112 pub actor: User,
2113 pub repo: RepoPath,
2114 pub number: u32,
2115 pub entries: Vec<NewSessionEntry>,
2116}
2117
2118#[derive(Debug, Serialize, Deserialize)]
2119pub struct Appended {
2120 pub count: u32,
2121}
2122
2123/// Lowercases, trims and de-duplicates labels, dropping empty ones.
2124/// Returns `None` if there are too many or one is too long.
2125pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> {
2126 let mut normalized: Vec<String> = Vec::new();
2127 for label in labels {
2128 let label = label
2129 .split_whitespace()
2130 .collect::<Vec<_>>()
2131 .join(" ")
2132 .to_lowercase();
2133 if label.is_empty() || normalized.contains(&label) {
2134 continue;
2135 }
2136 if label.chars().count() > MAX_LABEL_CHARS {
2137 return None;
2138 }
2139 normalized.push(label);
2140 }
2141 (normalized.len() <= MAX_LABELS).then_some(normalized)
2142}
2143
2144#[cfg(test)]
2145mod tests {
2146 use super::*;
2147
2148 fn labels(names: &[&str]) -> Vec<String> {
2149 names.iter().map(|name| (*name).to_owned()).collect()
2150 }
2151
2152 #[test]
2153 fn colors_and_due_dates_are_tidied() {
2154 assert_eq!(tidy_color("#A1B2C3").as_deref(), Some("a1b2c3"));
2155 assert_eq!(tidy_color("fc0").as_deref(), Some("ffcc00"));
2156 assert_eq!(tidy_color("red"), None);
2157 assert_eq!(tidy_color("12345"), None);
2158 assert_eq!(label_color_for("bug"), "d73a4a");
2159 assert_eq!(label_color_for("area: web"), label_color_for("area: web"));
2160 assert_eq!(tidy_due_on("2026-10-14").as_deref(), Some("2026-10-14"));
2161 assert_eq!(tidy_due_on("2026-10-14T00:00:00Z").as_deref(), Some("2026-10-14"));
2162 assert_eq!(tidy_due_on("2026-13-01"), None);
2163 assert_eq!(tidy_due_on("soon"), None);
2164 }
2165
2166 #[test]
2167 fn a_milestone_is_as_far_along_as_its_closed_items() {
2168 let mut milestone: Milestone = serde_json::from_value(serde_json::json!({
2169 "number": 1, "title": "Launch", "state": "open", "createdAt": "", "updatedAt": ""
2170 }))
2171 .unwrap();
2172 assert_eq!(milestone.percent(), 0);
2173 milestone.open_items = 3;
2174 milestone.closed_items = 1;
2175 assert_eq!(milestone.percent(), 25);
2176 }
2177
2178 #[test]
2179 fn labels_are_lowercased_trimmed_and_unique() {
2180 assert_eq!(
2181 normalize_labels(&labels(&[" Bug ", "bug", "", "Good First Issue"])),
2182 Some(labels(&["bug", "good first issue"]))
2183 );
2184 }
2185
2186 #[test]
2187 fn too_long_or_too_many_labels_are_refused() {
2188 assert_eq!(normalize_labels(&["x".repeat(51)]), None);
2189 assert!(normalize_labels(&["x".repeat(50)]).is_some());
2190 let many: Vec<String> = (0..21).map(|i| format!("label-{i}")).collect();
2191 assert_eq!(normalize_labels(&many), None);
2192 }
2193}
2194
2195
2196// --- Merge queue ----------------------------------------------------------
2197
2198/// Where a pull request in a merge queue stands.
2199#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2200#[serde(rename_all = "snake_case")]
2201pub enum QueueState {
2202 /// Waiting for its turn to be tested.
2203 Waiting,
2204 /// Its combined state is being built and checked.
2205 Testing,
2206 /// Its combined state passed; it lands once everything ahead has.
2207 Passed,
2208 /// Its combined state failed, or would not merge. It left the queue.
2209 Failed,
2210 /// On the default branch.
2211 Landed,
2212 /// Taken out of the queue by a person, or closed.
2213 Removed,
2214}
2215
2216impl QueueState {
2217 pub fn as_str(self) -> &'static str {
2218 match self {
2219 QueueState::Waiting => "waiting",
2220 QueueState::Testing => "testing",
2221 QueueState::Passed => "passed",
2222 QueueState::Failed => "failed",
2223 QueueState::Landed => "landed",
2224 QueueState::Removed => "removed",
2225 }
2226 }
2227
2228 /// Still in the queue.
2229 pub fn is_active(self) -> bool {
2230 matches!(
2231 self,
2232 QueueState::Waiting | QueueState::Testing | QueueState::Passed
2233 )
2234 }
2235}
2236
2237/// One pull request's place in a merge queue.
2238#[derive(Clone, Debug, Serialize, Deserialize)]
2239#[serde(rename_all = "camelCase")]
2240pub struct QueueEntry {
2241 pub id: String,
2242 pub number: u32,
2243 pub title: String,
2244 /// Who or what made the pull request, e.g. `g1t`.
2245 pub agent: String,
2246 pub state: QueueState,
2247 /// The pull requests merged ahead of it in the state being tested, in
2248 /// queue order. Empty when it was tested on the default branch alone.
2249 pub ahead: Vec<u32>,
2250 /// The default branch's commit the tested state was built on.
2251 pub base_commit: Option<String>,
2252 /// The tested state: the default branch with everything ahead and this.
2253 pub combined_commit: Option<String>,
2254 /// Why it failed: a merge conflict or what could not be run.
2255 pub error: Option<String>,
2256 /// Commands run against the tested state by queues from before its
2257 /// checks were workflows. Empty since.
2258 pub results: Vec<CheckResult>,
2259 /// Username of whoever merged it into the queue: a person, or `g1t`.
2260 pub enqueued_by: String,
2261 /// RFC 3339.
2262 pub created_at: String,
2263 /// RFC 3339. When it landed or left.
2264 pub finished_at: Option<String>,
2265}
2266
2267/// A repository's merge queue: what is in it, in order, and what recently
2268/// left it.
2269#[derive(Clone, Debug, Serialize, Deserialize)]
2270#[serde(rename_all = "camelCase")]
2271pub struct QueueView {
2272 /// Whether the repository merges through the queue.
2273 pub enabled: bool,
2274 pub active: Vec<QueueEntry>,
2275 /// Newest first.
2276 pub recent: Vec<QueueEntry>,
2277}
2278
2279/// `queue`: a repository's merge queue. Returns `Outcome<QueueView>`.
2280#[derive(Debug, Serialize, Deserialize)]
2281pub struct QueueArgs {
2282 pub repo: RepoPath,
2283 pub viewer: Viewer,
2284}
2285
2286/// `queue_build`: builds the next batch to test for a repository, if
2287/// nothing is being tested now: each entry landed by its method on the
2288/// state of the entries ahead of it, in the repository itself, with its
2289/// `merge_group` workflows started. Returns `Vec<QueueJob>`, which is now
2290/// always empty: no state is left for a sandbox to build.
2291#[derive(Debug, Serialize, Deserialize)]
2292#[serde(rename_all = "camelCase")]
2293pub struct QueueBuildArgs {
2294 pub repo_id: String,
2295}
2296
2297/// One pull request in a state being tested: where its change is.
2298#[derive(Clone, Debug, Serialize, Deserialize)]
2299#[serde(rename_all = "camelCase")]
2300pub struct QueueStackItem {
2301 pub number: u32,
2302 pub title: String,
2303 /// The repository holding the change: its fork, or the repository.
2304 pub source: RepoPath,
2305 /// The branch of `source` holding it.
2306 pub branch: String,
2307 pub commit: String,
2308 /// How it lands: the state is built with its commits made this way.
2309 /// States recorded before methods were merged in by merge commit.
2310 #[serde(default = "merge_method")]
2311 pub method: MergeMethod,
2312 /// For `merge` and `squash`: the commit's message, worked out when
2313 /// the state was first built, so that building it again makes the
2314 /// same commit.
2315 #[serde(default, skip_serializing_if = "Option::is_none")]
2316 pub message: Option<String>,
2317 /// For `squash`: who the commit is by.
2318 #[serde(default, skip_serializing_if = "Option::is_none")]
2319 pub author: Option<User>,
2320}
2321
2322/// One combined state to build and check. The work service builds every
2323/// state in the repository itself now, so `queue_build` returns none.
2324#[derive(Clone, Debug, Serialize, Deserialize)]
2325#[serde(rename_all = "camelCase")]
2326pub struct QueueJob {
2327 pub entry_id: String,
2328 /// Lets the sandbox, and nothing else, report this state's result.
2329 pub token: String,
2330 pub repo: RepoPath,
2331 pub default_branch: String,
2332 /// The default branch's commit to build on.
2333 pub base_commit: String,
2334 /// Where to push the tested state, in the repository itself.
2335 pub branch: String,
2336 /// The pull requests to merge in, in order; the last is the entry.
2337 pub stack: Vec<QueueStackItem>,
2338 /// Commands to run on the built state. Always empty: the state is
2339 /// checked by the `merge_group` workflows run on it, and the default
2340 /// branch's required checks must pass there.
2341 pub checks: Vec<String>,
2342 /// Always empty, as `checks`.
2343 #[serde(default)]
2344 pub contract_checks: Vec<String>,
2345 /// Who the sandbox acts as: a member who can push the tested state.
2346 pub actor: User,
2347}
2348
2349/// `report_queue`: the result of building one combined state. Returns
2350/// `Outcome<QueueState>`.
2351#[derive(Debug, Serialize, Deserialize)]
2352#[serde(rename_all = "camelCase")]
2353pub struct ReportQueueArgs {
2354 pub entry_id: String,
2355 pub token: String,
2356 #[serde(default)]
2357 pub combined_commit: Option<String>,
2358 #[serde(default)]
2359 pub results: Vec<CheckResult>,
2360 /// Set when the state could not be built or checked.
2361 #[serde(default)]
2362 pub error: Option<String>,
2363 /// For a merge conflict: the pull request whose change it collided with.
2364 #[serde(default)]
2365 pub conflict_with: Option<u32>,
2366 /// For a merge conflict: the files that conflicted.
2367 #[serde(default)]
2368 pub conflicts: Vec<String>,
2369}
2370
2371
2372/// `locate_pull`: where a pull request lives, by its id, for a tool that
2373/// knows only the fork it is working in (`g1t.sh/pulls/<id>`). Returns
2374/// `Outcome<LocatedPull>`; not found for anyone who cannot see it.
2375#[derive(Debug, Serialize, Deserialize)]
2376pub struct LocatePullArgs {
2377 pub id: String,
2378 pub viewer: Viewer,
2379}
2380
2381#[derive(Clone, Debug, Serialize, Deserialize)]
2382#[serde(rename_all = "camelCase")]
2383pub struct LocatedPull {
2384 pub repo: RepoPath,
2385 pub number: u32,
2386 pub title: String,
2387 pub status: PullStatus,
2388}
2389
2390// --- A person's work -------------------------------------------------------
2391
2392/// Work items or pull requests, on a person's profile.
2393#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2394#[serde(rename_all = "lowercase")]
2395pub enum AuthoredKind {
2396 Item,
2397 Pull,
2398}
2399
2400/// The state filter on a person's work. `Closed` takes in merged pull
2401/// requests too; `Merged` is only those.
2402#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2403#[serde(rename_all = "lowercase")]
2404pub enum AuthoredState {
2405 Open,
2406 Closed,
2407 Merged,
2408}
2409
2410/// How a person's work is ordered.
2411#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
2412#[serde(rename_all = "lowercase")]
2413pub enum AuthoredSort {
2414 /// Newest first.
2415 #[default]
2416 Created,
2417 /// Most recently changed first.
2418 Updated,
2419 /// Oldest first.
2420 Oldest,
2421}
2422
2423/// The most items one `by_author` page holds.
2424pub const AUTHORED_PAGE: u32 = 25;
2425
2426/// `by_author`: the work items and pull requests a person opened, only
2427/// those `viewer` may read, so a private title never reaches anyone who
2428/// could not open it. Returns `Outcome<Authored>`; not found for an
2429/// account that does not exist.
2430#[derive(Debug, Serialize, Deserialize)]
2431pub struct ByAuthorArgs {
2432 pub username: String,
2433 pub viewer: Viewer,
2434 #[serde(default)]
2435 pub kind: Option<AuthoredKind>,
2436 #[serde(default)]
2437 pub state: Option<AuthoredState>,
2438 /// Only work on this repository (`namespace/name`): its pull requests,
2439 /// and the work items filed from it.
2440 #[serde(default)]
2441 pub repo: Option<String>,
2442 #[serde(default)]
2443 pub sort: AuthoredSort,
2444 /// The `next` of the page before, to read on from there.
2445 #[serde(default)]
2446 pub before: Option<String>,
2447 /// At most [`AUTHORED_PAGE`]; that when absent.
2448 #[serde(default)]
2449 pub limit: Option<u32>,
2450}
2451
2452/// One work item or pull request a person opened.
2453#[derive(Clone, Debug, Serialize, Deserialize)]
2454#[serde(rename_all = "camelCase")]
2455pub struct AuthoredItem {
2456 pub kind: AuthoredKind,
2457 /// The workspace it is numbered in.
2458 pub workspace: String,
2459 /// A pull request's repository, or the repository a work item was
2460 /// filed from, if it was.
2461 pub repo: Option<RepoPath>,
2462 pub number: u32,
2463 pub title: String,
2464 /// Open or closed; a merged pull request is closed.
2465 pub state: State,
2466 /// A pull request's own status.
2467 pub status: Option<PullStatus>,
2468 /// Why a work item was closed.
2469 pub reason: Option<CloseReason>,
2470 pub draft: bool,
2471 pub merged: bool,
2472 /// RFC 3339.
2473 pub created_at: String,
2474 /// RFC 3339.
2475 pub updated_at: String,
2476 /// When a pull request was merged. RFC 3339.
2477 pub merged_at: Option<String>,
2478}
2479
2480/// What a person has done, as far as the viewer may see.
2481#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2482#[serde(rename_all = "camelCase")]
2483pub struct AuthoredCounts {
2484 pub pulls_merged: u32,
2485 pub pulls_open: u32,
2486 pub pulls: u32,
2487 pub items: u32,
2488 pub items_open: u32,
2489}
2490
2491/// A repository a person has opened work on, with how much.
2492#[derive(Clone, Debug, Serialize, Deserialize)]
2493pub struct AuthoredRepo {
2494 pub repo: RepoPath,
2495 pub count: u32,
2496}
2497
2498/// A page of a person's work.
2499#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2500#[serde(rename_all = "camelCase")]
2501pub struct Authored {
2502 pub items: Vec<AuthoredItem>,
2503 /// Pass as `before` for the next page; null on the last.
2504 pub next: Option<String>,
2505 /// Over every repository the viewer may read, whatever the filters.
2506 pub counts: AuthoredCounts,
2507 /// Those repositories, most work first.
2508 pub repos: Vec<AuthoredRepo>,
2509}
2510
2511/// How many days `contributions` covers: today and the 364 before it.
2512pub const CONTRIBUTION_DAYS: u64 = 365;
2513
2514/// `contributions`: what a person did each day of the last year (commits
2515/// they pushed to a default branch, work items and pull requests opened,
2516/// reviews given), only on repositories `viewer` may read, for the calendar on their profile. Returns
2517/// `Outcome<Contributions>`; not found for an account that does not exist.
2518#[derive(Debug, Serialize, Deserialize)]
2519pub struct ContributionsArgs {
2520 pub username: String,
2521 pub viewer: Viewer,
2522}
2523
2524/// One day with something on it. Days with nothing are left out.
2525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2526pub struct ContributionDay {
2527 /// `YYYY-MM-DD`, UTC.
2528 pub date: String,
2529 /// Everything that day, commits included.
2530 pub count: u32,
2531 /// How many of `count` are commits they pushed (repos' `commit_days`).
2532 #[serde(default)]
2533 pub commits: u32,
2534}
2535
2536/// A person's year, as far as the viewer may see.
2537#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2538pub struct Contributions {
2539 /// Oldest first.
2540 pub days: Vec<ContributionDay>,
2541 pub total: u32,
2542 /// The first day counted, `YYYY-MM-DD`; the last is today.
2543 pub from: String,
2544}
2545
2546#[cfg(test)]
2547mod required_tests {
2548 use super::*;
2549
2550 fn status(context: &str, state: &str) -> CommitStatus {
2551 CommitStatus {
2552 context: context.into(),
2553 state: state.into(),
2554 description: Some(format!("{context} {state}")),
2555 target_url: None,
2556 updated_at: String::new(),
2557 source: None,
2558 check_run_id: None,
2559 }
2560 }
2561
2562 #[test]
2563 fn a_context_names_its_check_and_event() {
2564 assert_eq!(check_name("CI / pull_request"), ("CI", Some("pull_request")));
2565 assert_eq!(check_name("Build and test / merge_group"), ("Build and test", Some("merge_group")));
2566 assert_eq!(check_name("Triage / work_item"), ("Triage", Some("work_item")));
2567 assert_eq!(check_name("g1t / deploy"), ("g1t / deploy", None));
2568 assert_eq!(check_name("g1t / deploy (docs)"), ("g1t / deploy (docs)", None));
2569 assert_eq!(check_name("lint"), ("lint", None));
2570 }
2571
2572 #[test]
2573 fn required_checks_are_missing_pending_failed_or_passed() {
2574 let required = vec!["CI".to_owned(), "Lint".to_owned(), "g1t / deploy".to_owned(), "Docs".to_owned()];
2575 let statuses = [
2576 status("CI / pull_request", "success"),
2577 status("Lint / pull_request", "pending"),
2578 status("g1t / deploy", "failure"),
2579 ];
2580 let states: Vec<RequiredState> = required_checks(&required, &statuses).into_iter().map(|c| c.state).collect();
2581 assert_eq!(
2582 states,
2583 [RequiredState::Success, RequiredState::Pending, RequiredState::Failure, RequiredState::Expected]
2584 );
2585 }
2586
2587 #[test]
2588 fn any_event_reports_a_check_and_a_failure_wins() {
2589 let required = vec!["ci".to_owned()];
2590 let both = [status("CI / push", "failure"), status("CI / pull_request", "success")];
2591 let check = &required_checks(&required, &both)[0];
2592 assert_eq!(check.state, RequiredState::Failure);
2593 assert_eq!(check.description.as_deref(), Some("CI / push failure"));
2594 let queue = [status("CI / merge_group", "success")];
2595 assert_eq!(required_checks(&required, &queue)[0].state, RequiredState::Success);
2596 }
2597
2598 #[test]
2599 fn a_pull_request_merges_into_its_base_or_the_default_branch() {
2600 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2601 "id": "pr_1", "repoId": "rep_1", "number": 14, "item": null, "title": "Fix it", "body": null,
2602 "agent": "agent", "runtime": "external", "status": "open", "fork": null, "forkRepoId": null,
2603 "branch": "fix", "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2604 "supersededBy": null, "checkStatus": null, "author": { "id": "x", "username": "x" },
2605 "createdAt": "", "updatedAt": ""
2606 }))
2607 .unwrap();
2608 assert_eq!(pull.base_branch("main"), "main");
2609 assert!(pull.targets_default("main"));
2610 assert_eq!(pull.comparison(&None).base_branch, None);
2611 pull.base = Some("release".into());
2612 assert_eq!(pull.base_branch("main"), "release");
2613 assert!(!pull.targets_default("main"));
2614 assert_eq!(pull.comparison(&None).base_branch.as_deref(), Some("release"));
2615 }
2616
2617 #[test]
2618 fn required_names_are_tidied() {
2619 let names = vec![" CI ".to_owned(), "ci".to_owned(), String::new(), "Lint".to_owned()];
2620 assert_eq!(tidy_required(&names), ["CI", "Lint"]);
2621 }
2622
2623 #[test]
2624 fn a_definition_of_done_is_added_once() {
2625 let items = commands_pass(&["cargo test".to_owned(), " ".to_owned()]);
2626 assert_eq!(items, ["`cargo test` passes."]);
2627 let body = with_definition_of_done("Fix the greeting.", &items);
2628 assert_eq!(body, "Fix the greeting.\n\n## Definition of done\n\n- `cargo test` passes.");
2629 assert_eq!(with_definition_of_done(&body, &items), body);
2630 assert_eq!(with_definition_of_done("", &items), "## Definition of done\n\n- `cargo test` passes.");
2631 assert_eq!(with_definition_of_done(" Text ", &[]), "Text");
2632 }
2633}
2634
2635#[cfg(test)]
2636mod authorship_tests {
2637 use super::*;
2638 use crate::PrincipalKind;
2639 use crate::credentials::{Acting, Principal};
2640 use crate::identity::{AGENT_ID, AgentScope};
2641
2642 fn person() -> User {
2643 User {
2644 id: "usr_1".into(),
2645 username: "syntaqx".into(),
2646 verified: true,
2647 ..User::default()
2648 }
2649 }
2650
2651 fn agent_for(id: &str, username: &str) -> User {
2652 User {
2653 id: AGENT_ID.into(),
2654 username: "g1t".into(),
2655 kind: PrincipalKind::Agent,
2656 acting: Some(Box::new(Acting {
2657 credential_id: "tok_1".into(),
2658 agent: "g1t".into(),
2659 on_behalf_of: Principal { id: id.into(), username: username.into() },
2660 scope: AgentScope {
2661 repo: RepoPath { namespace: "acme".into(), name: "web".into() },
2662 operations: Vec::new(),
2663 run: None,
2664 },
2665 })),
2666 ..User::default()
2667 }
2668 }
2669
2670 fn pull(author: User, requested_by: Option<User>) -> Pull {
2671 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2672 "id": "pr_1", "repoId": "rep_1", "number": 14, "item": 12, "title": "Fix it", "body": null,
2673 "agent": "g1t", "runtime": "hosted", "status": "open", "fork": null, "forkRepoId": null,
2674 "branch": null, "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2675 "supersededBy": null, "checkStatus": null,
2676 "author": { "id": "x", "username": "x" },
2677 "createdAt": "", "updatedAt": ""
2678 }))
2679 .unwrap();
2680 pull.author = author;
2681 pull.requested_by = requested_by;
2682 pull
2683 }
2684
2685 #[test]
2686 fn a_pull_request_knows_where_its_change_is() {
2687 let repo = RepoPath { namespace: "flagon".into(), name: "g1t".into() };
2688 let viewer: Viewer = None;
2689 // A branch of the repository itself.
2690 let mut branch = pull(User::default(), None);
2691 branch.branch = Some("fix".into());
2692 assert_eq!((branch.source_id(), branch.source_path(&repo)), ("rep_1", repo.clone()));
2693 assert!(!branch.from_other_repo() && branch.target_id().is_none());
2694 let compared = branch.comparison(&viewer);
2695 assert_eq!((compared.repo_id.as_str(), compared.head.as_deref()), ("rep_1", Some("fix")));
2696 assert!(compared.target_id.is_none());
2697
2698 // A branch of someone's fork: compared by name in the fork, from
2699 // the repository it merges into.
2700 let fork = RepoPath { namespace: "ana".into(), name: "g1t".into() };
2701 let mut forked = branch.clone();
2702 forked.head_repo = Some(fork.clone());
2703 forked.head_repo_id = Some("rep_2".into());
2704 assert_eq!((forked.source_id(), forked.source_path(&repo)), ("rep_2", fork));
2705 assert!(forked.from_other_repo() && forked.from_head_repo());
2706 assert_eq!(forked.target_id().as_deref(), Some("rep_1"));
2707 let compared = forked.comparison(&viewer);
2708 assert_eq!((compared.repo_id.as_str(), compared.head.as_deref()), ("rep_2", Some("fix")));
2709 assert_eq!(compared.target_id.as_deref(), Some("rep_1"));
2710 // Settled, by the commit it was closed at.
2711 forked.status = PullStatus::Closed;
2712 forked.head_commit = Some("abc".into());
2713 assert_eq!(forked.comparison(&viewer).head.as_deref(), Some("abc"));
2714
2715 // A working copy: compared as a whole, and knows its own target.
2716 let mut copy = pull(User::default(), None);
2717 copy.fork = Some(RepoPath { namespace: "pulls".into(), name: "pr_1".into() });
2718 copy.fork_repo_id = Some("rep_3".into());
2719 assert_eq!(copy.source_id(), "rep_3");
2720 assert!(copy.from_other_repo() && !copy.from_head_repo() && copy.target_id().is_none());
2721 let compared = copy.comparison(&viewer);
2722 assert_eq!((compared.repo_id.as_str(), compared.head), ("rep_3", None));
2723 }
2724
2725 #[test]
2726 fn a_pull_request_from_before_forks_reads_without_a_head_repo() {
2727 let read = pull(User::default(), None);
2728 assert!(read.head_repo.is_none() && read.head_repo_id.is_none());
2729 let json = serde_json::to_value(&read).unwrap();
2730 assert!(json.get("headRepo").is_some() && json.get("headRepoId").is_some());
2731 }
2732
2733 #[test]
2734 fn what_g1t_s_agent_files_at_work_is_g1t_s_requested_by_whoever_it_works_for() {
2735 let (author, asked) = authorship(&agent_for("usr_1", "syntaqx"));
2736 assert_eq!(author.id, AGENT_ID);
2737 assert_eq!(asked.map(|user| user.username), Some("syntaqx".into()));
2738 }
2739
2740 #[test]
2741 fn nobody_asked_for_g1t_s_own_work() {
2742 // A run g1t started itself acts for g1t, not for a person.
2743 let (author, asked) = authorship(&agent_for(crate::system::ID, "g1t"));
2744 assert_eq!(author.id, AGENT_ID);
2745 assert!(asked.is_none());
2746 // A security update g1t opens is g1t's own, as before.
2747 let (author, asked) = authorship(&User::system("acme"));
2748 assert_eq!((author.id.as_str(), author.kind), (crate::system::ID, PrincipalKind::System));
2749 assert!(asked.is_none());
2750 }
2751
2752 #[test]
2753 fn anyone_else_opens_their_own() {
2754 let (author, asked) = authorship(&person());
2755 assert_eq!((author.id.as_str(), author.kind), ("usr_1", PrincipalKind::User));
2756 assert!(asked.is_none());
2757 // Nothing but who they are is kept.
2758 assert!(author.workspaces.is_empty() && author.acting.is_none());
2759 }
2760
2761 #[test]
2762 fn the_requester_owns_g1t_s_pull_request_and_an_author_their_own() {
2763 let made = pull(g1t_author(), Some(person()));
2764 assert_eq!(made.owner().id, "usr_1");
2765 assert!(made.is_owned_by("usr_1"));
2766 assert!(!made.is_owned_by(AGENT_ID), "g1t's agent does not answer for its own change");
2767 let own = pull(person(), None);
2768 assert_eq!(own.owner().id, "usr_1");
2769 assert!(own.is_owned_by("usr_1"));
2770 }
2771
2772 #[test]
2773 fn requested_by_is_null_when_nobody_asked_and_read_as_absent_from_older_senders() {
2774 let own = pull(person(), None);
2775 let sent = serde_json::to_value(&own).unwrap();
2776 assert!(sent["requestedBy"].is_null());
2777 let mut older = sent.clone();
2778 older.as_object_mut().unwrap().remove("requestedBy");
2779 let read: Pull = serde_json::from_value(older).unwrap();
2780 assert!(read.requested_by.is_none());
2781 }
2782
2783 #[test]
2784 fn a_comment_is_changed_by_its_author_or_a_maintainer() {
2785 use CommentKind::{Comment, Event};
2786 // Its author edits and deletes it; someone else needs Maintain.
2787 assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, false).is_ok());
2788 assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, true).is_ok());
2789 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, false).is_err());
2790 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, true).is_err());
2791 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, false).is_ok());
2792 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, true).is_ok());
2793 // A review's text can be edited, but its verdict stays.
2794 assert!(may_change_comment(Comment, true, "usr_1", "usr_1", false, false).is_ok());
2795 assert!(may_change_comment(Comment, true, "usr_1", "usr_1", true, true).is_err());
2796 // A note of what happened is nobody's to change.
2797 assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, false).is_err());
2798 assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, true).is_err());
2799 }
2800}