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,806 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. Returns `Outcome<Vec<Pull>>`.
1683#[derive(Debug, Serialize, Deserialize)]
1684pub struct ListPullsArgs {
1685 pub repo: RepoPath,
1686 pub viewer: Viewer,
1687 #[serde(default)]
1688 pub state: Option<State>,
1689 /// Only pull requests carrying this label.
1690 #[serde(default)]
1691 pub label: Option<String>,
1692 /// Only pull requests in the milestone of this number.
1693 #[serde(default)]
1694 pub milestone: Option<u32>,
1695 /// Only pull requests into this branch.
1696 #[serde(default)]
1697 pub base: Option<String>,
1698}
1699
1700/// `pulls_for_repos`: the newest open and the newest closed pull requests
1701/// of many repositories, in one call, for pages that show several projects
1702/// at once. Repositories the viewer cannot read are left out, as are forks
1703/// (ask those with `list_pulls`). Returns `Vec<RepoPulls>`.
1704#[derive(Debug, Serialize, Deserialize)]
1705#[serde(rename_all = "camelCase")]
1706pub struct PullsForReposArgs {
1707 /// At most [`MAX_PULLS_FOR_REPOS`] are looked at.
1708 pub repo_ids: Vec<String>,
1709 pub viewer: Viewer,
1710 /// How many of each, open and closed, per repository (at most 100).
1711 pub limit: u32,
1712}
1713
1714/// The most repositories one `pulls_for_repos` call looks at.
1715pub const MAX_PULLS_FOR_REPOS: usize = 50;
1716
1717/// One repository's pull requests from `pulls_for_repos`, newest first.
1718#[derive(Debug, Serialize, Deserialize)]
1719#[serde(rename_all = "camelCase")]
1720pub struct RepoPulls {
1721 pub repo_id: String,
1722 /// Draft and open.
1723 pub open: Vec<Pull>,
1724 /// Merged and closed.
1725 pub closed: Vec<Pull>,
1726}
1727
1728/// `get_pull` (`Outcome<PullDetail>`), `read_session`
1729/// (`Outcome<Vec<SessionEntry>>`) and `counts` (`Outcome<Counts>`, which
1730/// ignores `number`).
1731#[derive(Debug, Serialize, Deserialize)]
1732#[serde(rename_all = "camelCase")]
1733pub struct ViewArgs {
1734 pub repo: RepoPath,
1735 #[serde(default)]
1736 pub number: u32,
1737 pub viewer: Viewer,
1738 /// For `read_session`: only entries after this sequence number.
1739 #[serde(default)]
1740 pub after_seq: u32,
1741}
1742
1743/// How many work items linked to a repository, and how many of its pull
1744/// requests, are open.
1745#[derive(Debug, Serialize, Deserialize)]
1746pub struct Counts {
1747 pub items: u32,
1748 pub pulls: u32,
1749}
1750
1751/// `add_comment`, on a thread: a pull request or a work item. Every method
1752/// that takes a thread reads it the same way: with `repo`, the pull
1753/// request `number` of that repository; without, the work item `number`
1754/// of `workspace`. On a pull request a comment may name a line of the
1755/// change, and may carry a verdict; nobody can give a verdict on their own
1756/// pull request. Returns `Outcome<Comment>`.
1757#[derive(Debug, Serialize, Deserialize)]
1758pub struct AddCommentArgs {
1759 pub actor: User,
1760 #[serde(default)]
1761 pub repo: Option<RepoPath>,
1762 #[serde(default)]
1763 pub workspace: String,
1764 pub number: u32,
1765 /// May be empty when approving.
1766 #[serde(default)]
1767 pub body: String,
1768 #[serde(default)]
1769 pub path: Option<String>,
1770 #[serde(default)]
1771 pub line: Option<u32>,
1772 #[serde(default)]
1773 pub verdict: Option<Verdict>,
1774}
1775
1776/// `edit_comment` and `delete_comment`: change the text of a comment, or
1777/// delete it: one on a pull request of `repo`, or without `repo`, one on a
1778/// work item of `workspace`. Its author may, and so may anyone with the
1779/// Maintain role or higher ([`may_change_comment`]). Notes of what
1780/// happened (`CommentKind::Event`) cannot be changed, and a review that
1781/// gave a verdict cannot be deleted, only edited. `edit_comment` returns
1782/// `Outcome<Comment>`; `delete_comment` returns `Outcome<()>`.
1783#[derive(Debug, Serialize, Deserialize)]
1784#[serde(rename_all = "camelCase")]
1785pub struct CommentActionArgs {
1786 pub actor: User,
1787 #[serde(default)]
1788 pub repo: Option<RepoPath>,
1789 #[serde(default)]
1790 pub workspace: String,
1791 pub comment_id: String,
1792 /// For `edit_comment`: the new text, in Markdown.
1793 #[serde(default)]
1794 pub body: String,
1795}
1796
1797/// Why `actor_id` may not edit (or, with `deleting`, delete) a comment by
1798/// `author_id`, or none when they may: its author may, and so may someone
1799/// who `maintains` the repository (the Maintain role or higher). A note of
1800/// something that happened is never changed, and a review's verdict stays,
1801/// so a comment that gave one can be edited but not deleted.
1802pub fn may_change_comment(
1803 kind: CommentKind,
1804 has_verdict: bool,
1805 author_id: &str,
1806 actor_id: &str,
1807 maintains: bool,
1808 deleting: bool,
1809) -> Result<(), &'static str> {
1810 if kind == CommentKind::Event {
1811 return Err("That is a record of something that happened, not a comment; it cannot be changed.");
1812 }
1813 if author_id != actor_id && !maintains {
1814 return Err(if deleting {
1815 "Only its author, or someone with the Maintain role or higher, can delete this comment."
1816 } else {
1817 "Only its author, or someone with the Maintain role or higher, can edit this comment."
1818 });
1819 }
1820 if deleting && has_verdict {
1821 return Err("A review cannot be deleted. Edit its text instead.");
1822 }
1823 Ok(())
1824}
1825
1826/// `open_pull`. Without `branch`, forks the repo and returns a draft pull
1827/// request to push to. With it, opens a pull request, ready for review,
1828/// for a branch already pushed to the repo, or to `head_repo`, a fork of
1829/// it. Its number is the next in its workspace's sequence. Opening one for
1830/// a work item links the repository to the item (`touched`) unless it is
1831/// linked already. Returns `Outcome<Pull>`.
1832#[derive(Debug, Serialize, Deserialize)]
1833pub struct OpenPullArgs {
1834 pub actor: User,
1835 pub repo: RepoPath,
1836 /// The work item this is for, by number in the repository's workspace.
1837 #[serde(default)]
1838 pub item: Option<u32>,
1839 /// Defaults to the work item's title; required without one.
1840 #[serde(default)]
1841 pub title: String,
1842 /// What changed and why. Usually set later, when a draft is marked ready.
1843 #[serde(default)]
1844 pub body: String,
1845 /// A branch of the repository that already holds the change: of
1846 /// `head_repo` when given.
1847 #[serde(default)]
1848 pub branch: Option<String>,
1849 /// A fork of the repository (directly or through other forks) whose
1850 /// `branch` holds the change. Needs `branch`.
1851 #[serde(default)]
1852 pub head_repo: Option<RepoPath>,
1853 #[serde(default)]
1854 pub agent: String,
1855 pub runtime: Runtime,
1856 /// The branch to merge into: the default branch when absent.
1857 #[serde(default)]
1858 pub base: Option<String>,
1859 /// Opened from a branch as a draft, still being worked on: it can't
1860 /// merge, and agents' review routines wait, until it is marked ready.
1861 #[serde(default)]
1862 pub draft: bool,
1863}
1864
1865/// A person's request that a pull request merge on its own once its
1866/// checks, reviews and rules allow (`set_auto_merge`). Cleared when it
1867/// merges or closes, when someone turns it off, and when someone other
1868/// than who asked pushes to it.
1869#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1870#[serde(rename_all = "camelCase")]
1871pub struct AutoMerge {
1872 pub method: MergeMethod,
1873 /// The commit message's first line and the rest, for `merge` and
1874 /// `squash`; the repository's default when absent.
1875 #[serde(default)]
1876 pub commit_title: Option<String>,
1877 #[serde(default)]
1878 pub commit_message: Option<String>,
1879 /// Username of who asked.
1880 pub enabled_by: String,
1881 /// RFC 3339.
1882 pub enabled_at: String,
1883}
1884
1885/// `set_auto_merge`: turns a pull request's auto-merge on (with how to
1886/// merge) or off. Turning it on needs the Write role, since it merges as
1887/// whoever turned it on; its author may also turn it off. Refused where
1888/// the repository does not allow auto-merge, and for a method the
1889/// repository or its rules do not allow. A pull request that may merge
1890/// already merges at once instead. Publishes `pull.auto_merge_enabled` or
1891/// `pull.auto_merge_disabled`. Returns `Outcome<Pull>`.
1892#[derive(Debug, Serialize, Deserialize)]
1893#[serde(rename_all = "camelCase")]
1894pub struct SetAutoMergeArgs {
1895 pub actor: User,
1896 pub repo: RepoPath,
1897 pub number: u32,
1898 pub enabled: bool,
1899 /// The repository's default method when absent.
1900 #[serde(default)]
1901 pub merge_method: Option<MergeMethod>,
1902 #[serde(default)]
1903 pub commit_title: Option<String>,
1904 #[serde(default)]
1905 pub commit_message: Option<String>,
1906}
1907
1908/// `merge_choices`: how a pull request may merge now, for whoever is
1909/// looking: the merge box's choices and their default messages. Takes
1910/// `ViewArgs`. Returns `Outcome<MergeChoices>`.
1911#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1912#[serde(rename_all = "camelCase")]
1913pub struct MergeChoices {
1914 /// The methods it may merge by: the repository's, as the rules for the
1915 /// branch it merges into allow (linear history rules out merge
1916 /// commits). In the order offered.
1917 pub methods: Vec<MergeMethod>,
1918 /// The one offered first: the repository's default when it may be
1919 /// used, else the first of `methods`. Null when none may.
1920 pub default_method: Option<MergeMethod>,
1921 /// Each of `methods`, with its default commit message and whether it
1922 /// can be used now.
1923 pub options: Vec<MergeChoice>,
1924 /// Its base has commits it does not have.
1925 pub behind: bool,
1926 /// It contains its base's head, so it can fast-forward as it is.
1927 pub can_fast_forward: bool,
1928 /// Offer to bring it up to date: it is behind, and the repository
1929 /// suggests updating branches (or requires them up to date).
1930 pub suggest_update: bool,
1931 /// The repository requires it to be up to date before it merges.
1932 pub require_up_to_date: bool,
1933 /// It merges through the merge queue, which builds and tests it by the
1934 /// method chosen (the queue rule's when none is) and lands it as tested.
1935 pub merge_queue: bool,
1936 /// The viewer may turn on auto-merge for it: the repository allows it,
1937 /// it is open or a draft, and they may merge it.
1938 pub auto_merge_allowed: bool,
1939 /// Whether "delete the branch" starts ticked: the repository deletes
1940 /// branches on merge, and its branch can be deleted.
1941 pub delete_branch_default: bool,
1942 /// Its branch can be deleted once it merges: a branch of the
1943 /// repository (not its base) or of a fork, never a working copy.
1944 pub can_delete_branch: bool,
1945 /// Commits made merging it carry a `Signed-off-by:` trailer for the
1946 /// merger.
1947 pub sign_off: bool,
1948}
1949
1950/// One way a pull request may merge (`MergeChoices::options`).
1951#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1952#[serde(rename_all = "camelCase")]
1953pub struct MergeChoice {
1954 pub method: MergeMethod,
1955 /// For `merge` and `squash`: the commit's title and the rest of its
1956 /// message, as the repository's settings write them.
1957 #[serde(default)]
1958 pub commit_title: Option<String>,
1959 #[serde(default)]
1960 pub commit_message: Option<String>,
1961 /// Why it cannot merge this way now, if it cannot.
1962 #[serde(default)]
1963 pub unavailable: Option<String>,
1964}
1965
1966/// `ready_pull`, `close_pull`, `reopen_pull`, `convert_pull_to_draft` and
1967/// `merge_pull`. Each returns `Outcome<Pull>`. `reopen_pull` opens a closed
1968/// pull request again (never a merged one), as the draft it was if it was
1969/// closed as one; `convert_pull_to_draft` turns one that is open back into
1970/// a draft, which leaves the merge queue.
1971///
1972/// Also `delete_head_branch` (a merged or closed one's branch, for whoever
1973/// may push where it is; never a working copy's, which repos retires) and
1974/// `restore_head_branch` (makes it again at `head_commit`). Both publish
1975/// `pull.head_branch_deleted` or `pull.head_branch_restored` and return
1976/// `Outcome<Pull>`.
1977///
1978/// Also `catch_up_pull`: brings the pull request up to date with the
1979/// default branch without a sandbox where that is safe, as the repos
1980/// service's `update_pull_branch` does, after checking that `actor` may
1981/// update it: its owner ([`Pull::owner`]) for a fork, any member for a
1982/// branch.
1983/// Returns `Outcome<repos::PullBranchUpdate>`; on `needs_agent` nothing was
1984/// pushed and the runner's `update` is the way on.
1985#[derive(Debug, Serialize, Deserialize)]
1986#[serde(rename_all = "camelCase")]
1987pub struct PullActionArgs {
1988 pub actor: User,
1989 pub repo: RepoPath,
1990 pub number: u32,
1991 /// For `ready_pull`: what changed and why.
1992 #[serde(default)]
1993 pub summary: String,
1994 /// For `merge_pull`: leave the work item open and the other pull
1995 /// requests for it untouched, because this one is only part of the
1996 /// work: it is recorded as part of it, as `Part of #N` would be.
1997 #[serde(default)]
1998 pub keep_item_open: bool,
1999 /// For `merge_pull`: merge although required checks have not passed,
2000 /// where the repository lets members bypass them.
2001 #[serde(default)]
2002 pub ignore_checks: bool,
2003 /// For `merge_pull`: merge although rules are not met, where a ruleset
2004 /// lists the actor as one who may bypass it. Recorded as a bypass.
2005 #[serde(default)]
2006 pub bypass_rules: bool,
2007 /// For `merge_pull`: how to merge; the repository's default method
2008 /// when absent. Refused when the repository or a rule does not allow
2009 /// it.
2010 #[serde(default)]
2011 pub merge_method: Option<MergeMethod>,
2012 /// For `merge_pull` with `merge` or `squash`: the commit's title and
2013 /// the rest of its message; the repository's default when absent.
2014 #[serde(default)]
2015 pub commit_title: Option<String>,
2016 #[serde(default)]
2017 pub commit_message: Option<String>,
2018 /// For `merge_pull` (and `catch_up_pull`): refuse if the pull
2019 /// request's head is no longer this commit.
2020 #[serde(default)]
2021 pub expected_head: Option<String>,
2022 /// For `merge_pull`: delete its branch once merged; the repository's
2023 /// setting when absent.
2024 #[serde(default)]
2025 pub delete_branch: Option<bool>,
2026 /// For `catch_up_pull`: merge the base in (the default) or rebase.
2027 #[serde(default)]
2028 pub update_method: Option<UpdateMethod>,
2029}
2030
2031/// What a merge or squash commit's message says by default.
2032#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
2033#[serde(rename_all = "snake_case")]
2034pub enum CommitMessageDefault {
2035 /// Merge: "Merge pull request #12 from owner/branch" and its title.
2036 /// Squash: its title for one commit, else its title and the commits'
2037 /// messages.
2038 #[default]
2039 Default,
2040 /// Its title, with its number.
2041 PullTitle,
2042 /// Its title, and its description below.
2043 PullTitleAndDescription,
2044 /// Squash only: its title, and each commit's message below.
2045 PullTitleAndCommits,
2046 /// The repository's own template (`merge_commit_template` or
2047 /// `squash_commit_template`).
2048 Custom,
2049}
2050
2051/// `update_pull`: changes who a pull request is assigned to, whose review
2052/// is asked for, its labels, its milestone and the branch it merges into.
2053/// Each list given replaces the whole set. Whoever opened it, or someone
2054/// with the Triage role, may; changing the base needs the Write role.
2055/// Returns `Outcome<Pull>`.
2056#[derive(Debug, Serialize, Deserialize)]
2057pub struct UpdatePullArgs {
2058 pub actor: User,
2059 pub repo: RepoPath,
2060 pub number: u32,
2061 #[serde(default)]
2062 pub assignees: Option<Vec<String>>,
2063 /// May include `g1t`. Asking for its review does not by itself
2064 /// start one; the runner's `review` does. A team is named
2065 /// `workspace/team` (or `@workspace/team`): the whole list, people and
2066 /// teams, replaces who is asked.
2067 #[serde(default)]
2068 pub reviewers: Option<Vec<String>>,
2069 /// Its labels; replaces the whole set, as `set_labels` does.
2070 #[serde(default)]
2071 pub labels: Option<Vec<String>>,
2072 /// The number of the milestone to put it in; 0 takes it out. Needs the
2073 /// Triage role.
2074 #[serde(default)]
2075 pub milestone: Option<u32>,
2076 /// The branch it merges into: an existing branch other than its own.
2077 /// Needs the Write role.
2078 #[serde(default)]
2079 pub base: Option<String>,
2080}
2081
2082/// `report_checks`: what a sandbox says about its run. With no results and
2083/// no error it has started. `skip` forgets the run, for one that will not
2084/// be carried out. Returns `Outcome<CheckRun>`.
2085#[derive(Debug, Serialize, Deserialize)]
2086#[serde(rename_all = "camelCase")]
2087pub struct ReportChecksArgs {
2088 pub run_id: String,
2089 pub token: String,
2090 #[serde(default)]
2091 pub results: Vec<CheckResult>,
2092 #[serde(default)]
2093 pub error: Option<String>,
2094 #[serde(default)]
2095 pub skip: bool,
2096}
2097
2098/// A pull request in progress, with where it lives.
2099#[derive(Debug, Serialize, Deserialize)]
2100pub struct ActivePull {
2101 pub pull: Pull,
2102 pub item: Option<WorkItem>,
2103 /// Where it stands, for one g1t is seeing through.
2104 #[serde(default)]
2105 pub lifecycle: Option<Lifecycle>,
2106}
2107
2108/// `list_active_pulls`: drafts and open pull requests the viewer started,
2109/// most recently active first. Returns `Vec<ActivePull>`.
2110#[derive(Debug, Serialize, Deserialize)]
2111pub struct ViewerArgs {
2112 pub viewer: Viewer,
2113}
2114
2115/// `append_session`. Returns `Outcome<Appended>`.
2116#[derive(Debug, Serialize, Deserialize)]
2117pub struct AppendSessionArgs {
2118 pub actor: User,
2119 pub repo: RepoPath,
2120 pub number: u32,
2121 pub entries: Vec<NewSessionEntry>,
2122}
2123
2124#[derive(Debug, Serialize, Deserialize)]
2125pub struct Appended {
2126 pub count: u32,
2127}
2128
2129/// Lowercases, trims and de-duplicates labels, dropping empty ones.
2130/// Returns `None` if there are too many or one is too long.
2131pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> {
2132 let mut normalized: Vec<String> = Vec::new();
2133 for label in labels {
2134 let label = label
2135 .split_whitespace()
2136 .collect::<Vec<_>>()
2137 .join(" ")
2138 .to_lowercase();
2139 if label.is_empty() || normalized.contains(&label) {
2140 continue;
2141 }
2142 if label.chars().count() > MAX_LABEL_CHARS {
2143 return None;
2144 }
2145 normalized.push(label);
2146 }
2147 (normalized.len() <= MAX_LABELS).then_some(normalized)
2148}
2149
2150#[cfg(test)]
2151mod tests {
2152 use super::*;
2153
2154 fn labels(names: &[&str]) -> Vec<String> {
2155 names.iter().map(|name| (*name).to_owned()).collect()
2156 }
2157
2158 #[test]
2159 fn colors_and_due_dates_are_tidied() {
2160 assert_eq!(tidy_color("#A1B2C3").as_deref(), Some("a1b2c3"));
2161 assert_eq!(tidy_color("fc0").as_deref(), Some("ffcc00"));
2162 assert_eq!(tidy_color("red"), None);
2163 assert_eq!(tidy_color("12345"), None);
2164 assert_eq!(label_color_for("bug"), "d73a4a");
2165 assert_eq!(label_color_for("area: web"), label_color_for("area: web"));
2166 assert_eq!(tidy_due_on("2026-10-14").as_deref(), Some("2026-10-14"));
2167 assert_eq!(tidy_due_on("2026-10-14T00:00:00Z").as_deref(), Some("2026-10-14"));
2168 assert_eq!(tidy_due_on("2026-13-01"), None);
2169 assert_eq!(tidy_due_on("soon"), None);
2170 }
2171
2172 #[test]
2173 fn a_milestone_is_as_far_along_as_its_closed_items() {
2174 let mut milestone: Milestone = serde_json::from_value(serde_json::json!({
2175 "number": 1, "title": "Launch", "state": "open", "createdAt": "", "updatedAt": ""
2176 }))
2177 .unwrap();
2178 assert_eq!(milestone.percent(), 0);
2179 milestone.open_items = 3;
2180 milestone.closed_items = 1;
2181 assert_eq!(milestone.percent(), 25);
2182 }
2183
2184 #[test]
2185 fn labels_are_lowercased_trimmed_and_unique() {
2186 assert_eq!(
2187 normalize_labels(&labels(&[" Bug ", "bug", "", "Good First Issue"])),
2188 Some(labels(&["bug", "good first issue"]))
2189 );
2190 }
2191
2192 #[test]
2193 fn too_long_or_too_many_labels_are_refused() {
2194 assert_eq!(normalize_labels(&["x".repeat(51)]), None);
2195 assert!(normalize_labels(&["x".repeat(50)]).is_some());
2196 let many: Vec<String> = (0..21).map(|i| format!("label-{i}")).collect();
2197 assert_eq!(normalize_labels(&many), None);
2198 }
2199}
2200
2201
2202// --- Merge queue ----------------------------------------------------------
2203
2204/// Where a pull request in a merge queue stands.
2205#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2206#[serde(rename_all = "snake_case")]
2207pub enum QueueState {
2208 /// Waiting for its turn to be tested.
2209 Waiting,
2210 /// Its combined state is being built and checked.
2211 Testing,
2212 /// Its combined state passed; it lands once everything ahead has.
2213 Passed,
2214 /// Its combined state failed, or would not merge. It left the queue.
2215 Failed,
2216 /// On the default branch.
2217 Landed,
2218 /// Taken out of the queue by a person, or closed.
2219 Removed,
2220}
2221
2222impl QueueState {
2223 pub fn as_str(self) -> &'static str {
2224 match self {
2225 QueueState::Waiting => "waiting",
2226 QueueState::Testing => "testing",
2227 QueueState::Passed => "passed",
2228 QueueState::Failed => "failed",
2229 QueueState::Landed => "landed",
2230 QueueState::Removed => "removed",
2231 }
2232 }
2233
2234 /// Still in the queue.
2235 pub fn is_active(self) -> bool {
2236 matches!(
2237 self,
2238 QueueState::Waiting | QueueState::Testing | QueueState::Passed
2239 )
2240 }
2241}
2242
2243/// One pull request's place in a merge queue.
2244#[derive(Clone, Debug, Serialize, Deserialize)]
2245#[serde(rename_all = "camelCase")]
2246pub struct QueueEntry {
2247 pub id: String,
2248 pub number: u32,
2249 pub title: String,
2250 /// Who or what made the pull request, e.g. `g1t`.
2251 pub agent: String,
2252 pub state: QueueState,
2253 /// The pull requests merged ahead of it in the state being tested, in
2254 /// queue order. Empty when it was tested on the default branch alone.
2255 pub ahead: Vec<u32>,
2256 /// The default branch's commit the tested state was built on.
2257 pub base_commit: Option<String>,
2258 /// The tested state: the default branch with everything ahead and this.
2259 pub combined_commit: Option<String>,
2260 /// Why it failed: a merge conflict or what could not be run.
2261 pub error: Option<String>,
2262 /// Commands run against the tested state by queues from before its
2263 /// checks were workflows. Empty since.
2264 pub results: Vec<CheckResult>,
2265 /// Username of whoever merged it into the queue: a person, or `g1t`.
2266 pub enqueued_by: String,
2267 /// RFC 3339.
2268 pub created_at: String,
2269 /// RFC 3339. When it landed or left.
2270 pub finished_at: Option<String>,
2271}
2272
2273/// A repository's merge queue: what is in it, in order, and what recently
2274/// left it.
2275#[derive(Clone, Debug, Serialize, Deserialize)]
2276#[serde(rename_all = "camelCase")]
2277pub struct QueueView {
2278 /// Whether the repository merges through the queue.
2279 pub enabled: bool,
2280 pub active: Vec<QueueEntry>,
2281 /// Newest first.
2282 pub recent: Vec<QueueEntry>,
2283}
2284
2285/// `queue`: a repository's merge queue. Returns `Outcome<QueueView>`.
2286#[derive(Debug, Serialize, Deserialize)]
2287pub struct QueueArgs {
2288 pub repo: RepoPath,
2289 pub viewer: Viewer,
2290}
2291
2292/// `queue_build`: builds the next batch to test for a repository, if
2293/// nothing is being tested now: each entry landed by its method on the
2294/// state of the entries ahead of it, in the repository itself, with its
2295/// `merge_group` workflows started. Returns `Vec<QueueJob>`, which is now
2296/// always empty: no state is left for a sandbox to build.
2297#[derive(Debug, Serialize, Deserialize)]
2298#[serde(rename_all = "camelCase")]
2299pub struct QueueBuildArgs {
2300 pub repo_id: String,
2301}
2302
2303/// One pull request in a state being tested: where its change is.
2304#[derive(Clone, Debug, Serialize, Deserialize)]
2305#[serde(rename_all = "camelCase")]
2306pub struct QueueStackItem {
2307 pub number: u32,
2308 pub title: String,
2309 /// The repository holding the change: its fork, or the repository.
2310 pub source: RepoPath,
2311 /// The branch of `source` holding it.
2312 pub branch: String,
2313 pub commit: String,
2314 /// How it lands: the state is built with its commits made this way.
2315 /// States recorded before methods were merged in by merge commit.
2316 #[serde(default = "merge_method")]
2317 pub method: MergeMethod,
2318 /// For `merge` and `squash`: the commit's message, worked out when
2319 /// the state was first built, so that building it again makes the
2320 /// same commit.
2321 #[serde(default, skip_serializing_if = "Option::is_none")]
2322 pub message: Option<String>,
2323 /// For `squash`: who the commit is by.
2324 #[serde(default, skip_serializing_if = "Option::is_none")]
2325 pub author: Option<User>,
2326}
2327
2328/// One combined state to build and check. The work service builds every
2329/// state in the repository itself now, so `queue_build` returns none.
2330#[derive(Clone, Debug, Serialize, Deserialize)]
2331#[serde(rename_all = "camelCase")]
2332pub struct QueueJob {
2333 pub entry_id: String,
2334 /// Lets the sandbox, and nothing else, report this state's result.
2335 pub token: String,
2336 pub repo: RepoPath,
2337 pub default_branch: String,
2338 /// The default branch's commit to build on.
2339 pub base_commit: String,
2340 /// Where to push the tested state, in the repository itself.
2341 pub branch: String,
2342 /// The pull requests to merge in, in order; the last is the entry.
2343 pub stack: Vec<QueueStackItem>,
2344 /// Commands to run on the built state. Always empty: the state is
2345 /// checked by the `merge_group` workflows run on it, and the default
2346 /// branch's required checks must pass there.
2347 pub checks: Vec<String>,
2348 /// Always empty, as `checks`.
2349 #[serde(default)]
2350 pub contract_checks: Vec<String>,
2351 /// Who the sandbox acts as: a member who can push the tested state.
2352 pub actor: User,
2353}
2354
2355/// `report_queue`: the result of building one combined state. Returns
2356/// `Outcome<QueueState>`.
2357#[derive(Debug, Serialize, Deserialize)]
2358#[serde(rename_all = "camelCase")]
2359pub struct ReportQueueArgs {
2360 pub entry_id: String,
2361 pub token: String,
2362 #[serde(default)]
2363 pub combined_commit: Option<String>,
2364 #[serde(default)]
2365 pub results: Vec<CheckResult>,
2366 /// Set when the state could not be built or checked.
2367 #[serde(default)]
2368 pub error: Option<String>,
2369 /// For a merge conflict: the pull request whose change it collided with.
2370 #[serde(default)]
2371 pub conflict_with: Option<u32>,
2372 /// For a merge conflict: the files that conflicted.
2373 #[serde(default)]
2374 pub conflicts: Vec<String>,
2375}
2376
2377
2378/// `locate_pull`: where a pull request lives, by its id, for a tool that
2379/// knows only the fork it is working in (`g1t.sh/pulls/<id>`). Returns
2380/// `Outcome<LocatedPull>`; not found for anyone who cannot see it.
2381#[derive(Debug, Serialize, Deserialize)]
2382pub struct LocatePullArgs {
2383 pub id: String,
2384 pub viewer: Viewer,
2385}
2386
2387#[derive(Clone, Debug, Serialize, Deserialize)]
2388#[serde(rename_all = "camelCase")]
2389pub struct LocatedPull {
2390 pub repo: RepoPath,
2391 pub number: u32,
2392 pub title: String,
2393 pub status: PullStatus,
2394}
2395
2396// --- A person's work -------------------------------------------------------
2397
2398/// Work items or pull requests, on a person's profile.
2399#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2400#[serde(rename_all = "lowercase")]
2401pub enum AuthoredKind {
2402 Item,
2403 Pull,
2404}
2405
2406/// The state filter on a person's work. `Closed` takes in merged pull
2407/// requests too; `Merged` is only those.
2408#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2409#[serde(rename_all = "lowercase")]
2410pub enum AuthoredState {
2411 Open,
2412 Closed,
2413 Merged,
2414}
2415
2416/// How a person's work is ordered.
2417#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
2418#[serde(rename_all = "lowercase")]
2419pub enum AuthoredSort {
2420 /// Newest first.
2421 #[default]
2422 Created,
2423 /// Most recently changed first.
2424 Updated,
2425 /// Oldest first.
2426 Oldest,
2427}
2428
2429/// The most items one `by_author` page holds.
2430pub const AUTHORED_PAGE: u32 = 25;
2431
2432/// `by_author`: the work items and pull requests a person opened, only
2433/// those `viewer` may read, so a private title never reaches anyone who
2434/// could not open it. Returns `Outcome<Authored>`; not found for an
2435/// account that does not exist.
2436#[derive(Debug, Serialize, Deserialize)]
2437pub struct ByAuthorArgs {
2438 pub username: String,
2439 pub viewer: Viewer,
2440 #[serde(default)]
2441 pub kind: Option<AuthoredKind>,
2442 #[serde(default)]
2443 pub state: Option<AuthoredState>,
2444 /// Only work on this repository (`namespace/name`): its pull requests,
2445 /// and the work items filed from it.
2446 #[serde(default)]
2447 pub repo: Option<String>,
2448 #[serde(default)]
2449 pub sort: AuthoredSort,
2450 /// The `next` of the page before, to read on from there.
2451 #[serde(default)]
2452 pub before: Option<String>,
2453 /// At most [`AUTHORED_PAGE`]; that when absent.
2454 #[serde(default)]
2455 pub limit: Option<u32>,
2456}
2457
2458/// One work item or pull request a person opened.
2459#[derive(Clone, Debug, Serialize, Deserialize)]
2460#[serde(rename_all = "camelCase")]
2461pub struct AuthoredItem {
2462 pub kind: AuthoredKind,
2463 /// The workspace it is numbered in.
2464 pub workspace: String,
2465 /// A pull request's repository, or the repository a work item was
2466 /// filed from, if it was.
2467 pub repo: Option<RepoPath>,
2468 pub number: u32,
2469 pub title: String,
2470 /// Open or closed; a merged pull request is closed.
2471 pub state: State,
2472 /// A pull request's own status.
2473 pub status: Option<PullStatus>,
2474 /// Why a work item was closed.
2475 pub reason: Option<CloseReason>,
2476 pub draft: bool,
2477 pub merged: bool,
2478 /// RFC 3339.
2479 pub created_at: String,
2480 /// RFC 3339.
2481 pub updated_at: String,
2482 /// When a pull request was merged. RFC 3339.
2483 pub merged_at: Option<String>,
2484}
2485
2486/// What a person has done, as far as the viewer may see.
2487#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2488#[serde(rename_all = "camelCase")]
2489pub struct AuthoredCounts {
2490 pub pulls_merged: u32,
2491 pub pulls_open: u32,
2492 pub pulls: u32,
2493 pub items: u32,
2494 pub items_open: u32,
2495}
2496
2497/// A repository a person has opened work on, with how much.
2498#[derive(Clone, Debug, Serialize, Deserialize)]
2499pub struct AuthoredRepo {
2500 pub repo: RepoPath,
2501 pub count: u32,
2502}
2503
2504/// A page of a person's work.
2505#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2506#[serde(rename_all = "camelCase")]
2507pub struct Authored {
2508 pub items: Vec<AuthoredItem>,
2509 /// Pass as `before` for the next page; null on the last.
2510 pub next: Option<String>,
2511 /// Over every repository the viewer may read, whatever the filters.
2512 pub counts: AuthoredCounts,
2513 /// Those repositories, most work first.
2514 pub repos: Vec<AuthoredRepo>,
2515}
2516
2517/// How many days `contributions` covers: today and the 364 before it.
2518pub const CONTRIBUTION_DAYS: u64 = 365;
2519
2520/// `contributions`: what a person did each day of the last year (commits
2521/// they pushed to a default branch, work items and pull requests opened,
2522/// reviews given), only on repositories `viewer` may read, for the calendar on their profile. Returns
2523/// `Outcome<Contributions>`; not found for an account that does not exist.
2524#[derive(Debug, Serialize, Deserialize)]
2525pub struct ContributionsArgs {
2526 pub username: String,
2527 pub viewer: Viewer,
2528}
2529
2530/// One day with something on it. Days with nothing are left out.
2531#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2532pub struct ContributionDay {
2533 /// `YYYY-MM-DD`, UTC.
2534 pub date: String,
2535 /// Everything that day, commits included.
2536 pub count: u32,
2537 /// How many of `count` are commits they pushed (repos' `commit_days`).
2538 #[serde(default)]
2539 pub commits: u32,
2540}
2541
2542/// A person's year, as far as the viewer may see.
2543#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2544pub struct Contributions {
2545 /// Oldest first.
2546 pub days: Vec<ContributionDay>,
2547 pub total: u32,
2548 /// The first day counted, `YYYY-MM-DD`; the last is today.
2549 pub from: String,
2550}
2551
2552#[cfg(test)]
2553mod required_tests {
2554 use super::*;
2555
2556 fn status(context: &str, state: &str) -> CommitStatus {
2557 CommitStatus {
2558 context: context.into(),
2559 state: state.into(),
2560 description: Some(format!("{context} {state}")),
2561 target_url: None,
2562 updated_at: String::new(),
2563 source: None,
2564 check_run_id: None,
2565 }
2566 }
2567
2568 #[test]
2569 fn a_context_names_its_check_and_event() {
2570 assert_eq!(check_name("CI / pull_request"), ("CI", Some("pull_request")));
2571 assert_eq!(check_name("Build and test / merge_group"), ("Build and test", Some("merge_group")));
2572 assert_eq!(check_name("Triage / work_item"), ("Triage", Some("work_item")));
2573 assert_eq!(check_name("g1t / deploy"), ("g1t / deploy", None));
2574 assert_eq!(check_name("g1t / deploy (docs)"), ("g1t / deploy (docs)", None));
2575 assert_eq!(check_name("lint"), ("lint", None));
2576 }
2577
2578 #[test]
2579 fn required_checks_are_missing_pending_failed_or_passed() {
2580 let required = vec!["CI".to_owned(), "Lint".to_owned(), "g1t / deploy".to_owned(), "Docs".to_owned()];
2581 let statuses = [
2582 status("CI / pull_request", "success"),
2583 status("Lint / pull_request", "pending"),
2584 status("g1t / deploy", "failure"),
2585 ];
2586 let states: Vec<RequiredState> = required_checks(&required, &statuses).into_iter().map(|c| c.state).collect();
2587 assert_eq!(
2588 states,
2589 [RequiredState::Success, RequiredState::Pending, RequiredState::Failure, RequiredState::Expected]
2590 );
2591 }
2592
2593 #[test]
2594 fn any_event_reports_a_check_and_a_failure_wins() {
2595 let required = vec!["ci".to_owned()];
2596 let both = [status("CI / push", "failure"), status("CI / pull_request", "success")];
2597 let check = &required_checks(&required, &both)[0];
2598 assert_eq!(check.state, RequiredState::Failure);
2599 assert_eq!(check.description.as_deref(), Some("CI / push failure"));
2600 let queue = [status("CI / merge_group", "success")];
2601 assert_eq!(required_checks(&required, &queue)[0].state, RequiredState::Success);
2602 }
2603
2604 #[test]
2605 fn a_pull_request_merges_into_its_base_or_the_default_branch() {
2606 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2607 "id": "pr_1", "repoId": "rep_1", "number": 14, "item": null, "title": "Fix it", "body": null,
2608 "agent": "agent", "runtime": "external", "status": "open", "fork": null, "forkRepoId": null,
2609 "branch": "fix", "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2610 "supersededBy": null, "checkStatus": null, "author": { "id": "x", "username": "x" },
2611 "createdAt": "", "updatedAt": ""
2612 }))
2613 .unwrap();
2614 assert_eq!(pull.base_branch("main"), "main");
2615 assert!(pull.targets_default("main"));
2616 assert_eq!(pull.comparison(&None).base_branch, None);
2617 pull.base = Some("release".into());
2618 assert_eq!(pull.base_branch("main"), "release");
2619 assert!(!pull.targets_default("main"));
2620 assert_eq!(pull.comparison(&None).base_branch.as_deref(), Some("release"));
2621 }
2622
2623 #[test]
2624 fn required_names_are_tidied() {
2625 let names = vec![" CI ".to_owned(), "ci".to_owned(), String::new(), "Lint".to_owned()];
2626 assert_eq!(tidy_required(&names), ["CI", "Lint"]);
2627 }
2628
2629 #[test]
2630 fn a_definition_of_done_is_added_once() {
2631 let items = commands_pass(&["cargo test".to_owned(), " ".to_owned()]);
2632 assert_eq!(items, ["`cargo test` passes."]);
2633 let body = with_definition_of_done("Fix the greeting.", &items);
2634 assert_eq!(body, "Fix the greeting.\n\n## Definition of done\n\n- `cargo test` passes.");
2635 assert_eq!(with_definition_of_done(&body, &items), body);
2636 assert_eq!(with_definition_of_done("", &items), "## Definition of done\n\n- `cargo test` passes.");
2637 assert_eq!(with_definition_of_done(" Text ", &[]), "Text");
2638 }
2639}
2640
2641#[cfg(test)]
2642mod authorship_tests {
2643 use super::*;
2644 use crate::PrincipalKind;
2645 use crate::credentials::{Acting, Principal};
2646 use crate::identity::{AGENT_ID, AgentScope};
2647
2648 fn person() -> User {
2649 User {
2650 id: "usr_1".into(),
2651 username: "syntaqx".into(),
2652 verified: true,
2653 ..User::default()
2654 }
2655 }
2656
2657 fn agent_for(id: &str, username: &str) -> User {
2658 User {
2659 id: AGENT_ID.into(),
2660 username: "g1t".into(),
2661 kind: PrincipalKind::Agent,
2662 acting: Some(Box::new(Acting {
2663 credential_id: "tok_1".into(),
2664 agent: "g1t".into(),
2665 on_behalf_of: Principal { id: id.into(), username: username.into() },
2666 scope: AgentScope {
2667 repo: RepoPath { namespace: "acme".into(), name: "web".into() },
2668 operations: Vec::new(),
2669 run: None,
2670 },
2671 })),
2672 ..User::default()
2673 }
2674 }
2675
2676 fn pull(author: User, requested_by: Option<User>) -> Pull {
2677 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2678 "id": "pr_1", "repoId": "rep_1", "number": 14, "item": 12, "title": "Fix it", "body": null,
2679 "agent": "g1t", "runtime": "hosted", "status": "open", "fork": null, "forkRepoId": null,
2680 "branch": null, "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2681 "supersededBy": null, "checkStatus": null,
2682 "author": { "id": "x", "username": "x" },
2683 "createdAt": "", "updatedAt": ""
2684 }))
2685 .unwrap();
2686 pull.author = author;
2687 pull.requested_by = requested_by;
2688 pull
2689 }
2690
2691 #[test]
2692 fn a_pull_request_knows_where_its_change_is() {
2693 let repo = RepoPath { namespace: "flagon".into(), name: "g1t".into() };
2694 let viewer: Viewer = None;
2695 // A branch of the repository itself.
2696 let mut branch = pull(User::default(), None);
2697 branch.branch = Some("fix".into());
2698 assert_eq!((branch.source_id(), branch.source_path(&repo)), ("rep_1", repo.clone()));
2699 assert!(!branch.from_other_repo() && branch.target_id().is_none());
2700 let compared = branch.comparison(&viewer);
2701 assert_eq!((compared.repo_id.as_str(), compared.head.as_deref()), ("rep_1", Some("fix")));
2702 assert!(compared.target_id.is_none());
2703
2704 // A branch of someone's fork: compared by name in the fork, from
2705 // the repository it merges into.
2706 let fork = RepoPath { namespace: "ana".into(), name: "g1t".into() };
2707 let mut forked = branch.clone();
2708 forked.head_repo = Some(fork.clone());
2709 forked.head_repo_id = Some("rep_2".into());
2710 assert_eq!((forked.source_id(), forked.source_path(&repo)), ("rep_2", fork));
2711 assert!(forked.from_other_repo() && forked.from_head_repo());
2712 assert_eq!(forked.target_id().as_deref(), Some("rep_1"));
2713 let compared = forked.comparison(&viewer);
2714 assert_eq!((compared.repo_id.as_str(), compared.head.as_deref()), ("rep_2", Some("fix")));
2715 assert_eq!(compared.target_id.as_deref(), Some("rep_1"));
2716 // Settled, by the commit it was closed at.
2717 forked.status = PullStatus::Closed;
2718 forked.head_commit = Some("abc".into());
2719 assert_eq!(forked.comparison(&viewer).head.as_deref(), Some("abc"));
2720
2721 // A working copy: compared as a whole, and knows its own target.
2722 let mut copy = pull(User::default(), None);
2723 copy.fork = Some(RepoPath { namespace: "pulls".into(), name: "pr_1".into() });
2724 copy.fork_repo_id = Some("rep_3".into());
2725 assert_eq!(copy.source_id(), "rep_3");
2726 assert!(copy.from_other_repo() && !copy.from_head_repo() && copy.target_id().is_none());
2727 let compared = copy.comparison(&viewer);
2728 assert_eq!((compared.repo_id.as_str(), compared.head), ("rep_3", None));
2729 }
2730
2731 #[test]
2732 fn a_pull_request_from_before_forks_reads_without_a_head_repo() {
2733 let read = pull(User::default(), None);
2734 assert!(read.head_repo.is_none() && read.head_repo_id.is_none());
2735 let json = serde_json::to_value(&read).unwrap();
2736 assert!(json.get("headRepo").is_some() && json.get("headRepoId").is_some());
2737 }
2738
2739 #[test]
2740 fn what_g1t_s_agent_files_at_work_is_g1t_s_requested_by_whoever_it_works_for() {
2741 let (author, asked) = authorship(&agent_for("usr_1", "syntaqx"));
2742 assert_eq!(author.id, AGENT_ID);
2743 assert_eq!(asked.map(|user| user.username), Some("syntaqx".into()));
2744 }
2745
2746 #[test]
2747 fn nobody_asked_for_g1t_s_own_work() {
2748 // A run g1t started itself acts for g1t, not for a person.
2749 let (author, asked) = authorship(&agent_for(crate::system::ID, "g1t"));
2750 assert_eq!(author.id, AGENT_ID);
2751 assert!(asked.is_none());
2752 // A security update g1t opens is g1t's own, as before.
2753 let (author, asked) = authorship(&User::system("acme"));
2754 assert_eq!((author.id.as_str(), author.kind), (crate::system::ID, PrincipalKind::System));
2755 assert!(asked.is_none());
2756 }
2757
2758 #[test]
2759 fn anyone_else_opens_their_own() {
2760 let (author, asked) = authorship(&person());
2761 assert_eq!((author.id.as_str(), author.kind), ("usr_1", PrincipalKind::User));
2762 assert!(asked.is_none());
2763 // Nothing but who they are is kept.
2764 assert!(author.workspaces.is_empty() && author.acting.is_none());
2765 }
2766
2767 #[test]
2768 fn the_requester_owns_g1t_s_pull_request_and_an_author_their_own() {
2769 let made = pull(g1t_author(), Some(person()));
2770 assert_eq!(made.owner().id, "usr_1");
2771 assert!(made.is_owned_by("usr_1"));
2772 assert!(!made.is_owned_by(AGENT_ID), "g1t's agent does not answer for its own change");
2773 let own = pull(person(), None);
2774 assert_eq!(own.owner().id, "usr_1");
2775 assert!(own.is_owned_by("usr_1"));
2776 }
2777
2778 #[test]
2779 fn requested_by_is_null_when_nobody_asked_and_read_as_absent_from_older_senders() {
2780 let own = pull(person(), None);
2781 let sent = serde_json::to_value(&own).unwrap();
2782 assert!(sent["requestedBy"].is_null());
2783 let mut older = sent.clone();
2784 older.as_object_mut().unwrap().remove("requestedBy");
2785 let read: Pull = serde_json::from_value(older).unwrap();
2786 assert!(read.requested_by.is_none());
2787 }
2788
2789 #[test]
2790 fn a_comment_is_changed_by_its_author_or_a_maintainer() {
2791 use CommentKind::{Comment, Event};
2792 // Its author edits and deletes it; someone else needs Maintain.
2793 assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, false).is_ok());
2794 assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, true).is_ok());
2795 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, false).is_err());
2796 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, true).is_err());
2797 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, false).is_ok());
2798 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, true).is_ok());
2799 // A review's text can be edited, but its verdict stays.
2800 assert!(may_change_comment(Comment, true, "usr_1", "usr_1", false, false).is_ok());
2801 assert!(may_change_comment(Comment, true, "usr_1", "usr_1", true, true).is_err());
2802 // A note of what happened is nobody's to change.
2803 assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, false).is_err());
2804 assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, true).is_err());
2805 }
2806}