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