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