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