Skip to content

g1t/crates/contracts/src/work.rs

2,692 lines95,565 bytesCodeBlame
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}
1138
1139/// `set_commit_status`: for services only. Returns `Outcome<bool>`.
1140#[derive(Debug, Serialize, Deserialize)]
1141#[serde(rename_all = "camelCase")]
1142pub struct SetCommitStatusArgs {
1143 pub repo_id: String,
1144 pub sha: String,
1145 pub context: String,
1146 pub state: String,
1147 #[serde(default)]
1148 pub description: Option<String>,
1149 #[serde(default)]
1150 pub target_url: Option<String>,
1151}
1152
1153/// A message a person sent an agent at work on a pull request. The agent
1154/// receives it at its next step.
1155#[derive(Clone, Debug, Serialize, Deserialize)]
1156#[serde(rename_all = "camelCase")]
1157pub struct AgentMessage {
1158 pub id: String,
1159 pub author: String,
1160 pub body: String,
1161 /// RFC 3339.
1162 pub created_at: String,
1163 /// RFC 3339. When the agent received it; null until then.
1164 pub delivered_at: Option<String>,
1165 /// `message` from a person, or from another pull request's agent a
1166 /// `question`, a `handoff` of work, or the `answer` to one.
1167 #[serde(default = "message_kind")]
1168 pub kind: String,
1169 /// The pull request whose agent sent it, when an agent did.
1170 #[serde(default)]
1171 pub from_number: Option<u32>,
1172 /// The pull request it was sent to.
1173 #[serde(default)]
1174 pub to_number: u32,
1175 /// For a question or handoff: the reply, once there is one.
1176 #[serde(default)]
1177 pub answer: Option<String>,
1178 /// For a handoff: whether it was declined.
1179 #[serde(default)]
1180 pub declined: bool,
1181 /// For the agent that sent it: what to expect, when the agent it asked
1182 /// is not at work and will not answer soon.
1183 #[serde(default, skip_serializing_if = "Option::is_none")]
1184 pub hint: Option<String>,
1185}
1186
1187fn message_kind() -> String {
1188 "message".to_owned()
1189}
1190
1191/// `message_agent`: sends the agent working on a pull request a message.
1192/// The pull request's owner ([`Pull::owner`]: whoever asked g1t for it, or
1193/// its author) and members of the workspace may. Returns
1194/// `Outcome<AgentMessage>`.
1195#[derive(Debug, Serialize, Deserialize)]
1196pub struct MessageAgentArgs {
1197 pub actor: User,
1198 pub repo: RepoPath,
1199 pub number: u32,
1200 pub body: String,
1201 /// For an agent: `question` or `handoff`; a person's is a `message`.
1202 #[serde(default)]
1203 pub kind: Option<String>,
1204 /// For an agent: the pull request it is working on, which the reply
1205 /// goes back to.
1206 #[serde(default)]
1207 pub from_number: Option<u32>,
1208}
1209
1210/// `answer_message`: replies to a question or a handoff an agent received,
1211/// accepting or declining a handoff. The reply reaches the asking agent at
1212/// its next step. Returns `Outcome<AgentMessage>`, the message answered.
1213#[derive(Debug, Serialize, Deserialize)]
1214pub struct AnswerMessageArgs {
1215 pub actor: User,
1216 pub repo: RepoPath,
1217 pub id: String,
1218 pub body: String,
1219 #[serde(default)]
1220 pub decline: bool,
1221}
1222
1223/// `take_messages`: the messages not yet delivered to the agent working on
1224/// a pull request, marked delivered. Only g1t's agents may. Returns
1225/// `Outcome<Vec<AgentMessage>>`.
1226#[derive(Debug, Serialize, Deserialize)]
1227pub struct TakeMessagesArgs {
1228 pub actor: User,
1229 pub repo: RepoPath,
1230 pub number: u32,
1231}
1232
1233/// A step on the way from an assigned issue to a pull request that is ready
1234/// to merge. g1t takes each one without being asked.
1235#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1236#[serde(rename_all = "snake_case")]
1237pub enum Stage {
1238 /// The agent is making the change.
1239 Working,
1240 /// Waiting for the checks workflows report on its head.
1241 Checking,
1242 /// g1t is reviewing it.
1243 Reviewing,
1244 /// The agent is addressing failed checks or a review.
1245 Revising,
1246 /// The agent is merging in the branch it would land on, which moved.
1247 CatchingUp,
1248 /// Woken to answer a question another agent asked it, or a handoff.
1249 Answering,
1250 /// In the repository's merge queue, being tested with what is ahead of
1251 /// it before it lands.
1252 Queued,
1253 /// Required checks passed, reviewed and approved, up to date. A
1254 /// person merges.
1255 Ready,
1256 /// g1t has stopped and a person has to decide what happens next.
1257 NeedsYou,
1258}
1259
1260/// Where a pull request made by a g1t agent stands. See [`Stage`].
1261#[derive(Clone, Debug, Serialize, Deserialize)]
1262pub struct Lifecycle {
1263 pub stage: Stage,
1264 /// One sentence saying what is happening, or why it stopped.
1265 pub detail: String,
1266 /// How many times the agent has been sent back to revise it.
1267 pub revisions: u32,
1268}
1269
1270/// `advance`: works out the next step for a pull request g1t is seeing
1271/// through and, if there is one to take now, claims it, so that it is
1272/// taken once however many times this is called. Called by the runner
1273/// service, which carries the step out. Returns `Advance`.
1274#[derive(Debug, Serialize, Deserialize)]
1275#[serde(rename_all = "camelCase")]
1276pub struct AdvanceArgs {
1277 pub pull_id: String,
1278}
1279
1280#[derive(Debug, Serialize, Deserialize)]
1281#[serde(tag = "action", rename_all = "snake_case")]
1282pub enum Advance {
1283 /// Nothing to do now: a step is under way, or it is a person's turn.
1284 None,
1285 /// Have a g1t agent review it.
1286 Review { job: LifecycleJob },
1287 /// Send the agent back to address `job.feedback`.
1288 Revise { job: LifecycleJob },
1289 /// Merge in the branch it would land on.
1290 CatchUp { job: LifecycleJob },
1291}
1292
1293/// What the runner needs to carry out a step of a pull request's lifecycle.
1294#[derive(Debug, Serialize, Deserialize)]
1295#[serde(rename_all = "camelCase")]
1296pub struct LifecycleJob {
1297 pub pull_id: String,
1298 pub repo: RepoPath,
1299 pub number: u32,
1300 /// Who the pull request belongs to ([`Pull::owner`]: whoever asked g1t
1301 /// for it, or its author). Sandboxes act as them.
1302 pub author: User,
1303 /// The repository holding the change: its fork, or the repository
1304 /// itself for one made on a branch.
1305 pub source: RepoPath,
1306 /// The branch of the source holding the change; its default branch
1307 /// when absent.
1308 #[serde(default)]
1309 pub branch: Option<String>,
1310 pub default_branch: String,
1311 pub title: String,
1312 pub description: String,
1313 pub issue: Option<Issue>,
1314 /// For a revision: the failed checks or the review to address.
1315 pub feedback: String,
1316 /// For a revision: which one this is, from 1.
1317 pub round: u32,
1318}
1319
1320/// How a repository wants its pull requests handled. A repository that has
1321/// changed nothing has the defaults.
1322#[derive(Clone, Debug, Serialize, Deserialize)]
1323#[serde(rename_all = "camelCase", default)]
1324pub struct RepoSettings {
1325 /// Land a g1t agent's pull request without a person once it is ready:
1326 /// required checks passed and approved as the settings below require.
1327 pub auto_merge: bool,
1328 /// The checks that must pass on a pull request's head before it may
1329 /// merge into the default branch, by name: a workflow's name (`CI`), or
1330 /// the context of another status (`g1t / deploy`). The same for a
1331 /// person's pull request and an agent's, and for the merge queue.
1332 pub required_checks: Vec<String>,
1333 /// Refuse to merge a pull request that does not contain the default
1334 /// branch's latest commits, so that what merges is what was checked.
1335 /// When off, merging one that is behind brings it up to date first.
1336 pub require_up_to_date: bool,
1337 /// How many approving reviews a pull request needs before it may
1338 /// merge. A reviewer who has since asked for changes blocks it.
1339 pub required_approvals: u32,
1340 /// Whether a g1t agent's approval counts towards `required_approvals`.
1341 pub count_agent_approvals: bool,
1342 /// Whether someone who may merge can bypass required checks that have
1343 /// not passed, by saying so as they merge.
1344 pub allow_ignoring_checks: bool,
1345 /// Whether a g1t agent's pull request is reviewed by a second agent
1346 /// without being asked.
1347 pub agent_review: bool,
1348 /// How many times a g1t agent is sent back to its pull request before
1349 /// a person is asked instead.
1350 pub max_revisions: u32,
1351 /// Merge through a queue: pull requests are tested together with those
1352 /// ahead of them, and only a combination that passed reaches the default
1353 /// branch.
1354 pub merge_queue: bool,
1355 /// Ask a person before merging a g1t agent's change whose confidence is
1356 /// low: auto-merge and the merge queue leave it, and it needs someone,
1357 /// until a person approves it.
1358 pub hold_low_confidence: bool,
1359 /// Refuse to merge until the code owners of every file it changes
1360 /// (its CODEOWNERS file) have approved, as many as each section asks.
1361 /// Only people count, and `g1t` only where the file names `@g1t`.
1362 pub require_code_owner_review: bool,
1363 /// Username of the member who last changed the settings, if anyone has.
1364 pub updated_by: Option<String>,
1365 /// RFC 3339.
1366 pub updated_at: Option<String>,
1367}
1368
1369impl RepoSettings {
1370 /// What holds for a pull request into `base`. The settings are the
1371 /// default branch's protection: a pull request into another branch
1372 /// needs no required checks or approvals, need not be up to date, and
1373 /// never goes through the merge queue, which lands on the default
1374 /// branch only. How g1t's agents review, revise and merge holds for
1375 /// every branch.
1376 pub fn for_base(&self, base: &str, default_branch: &str) -> RepoSettings {
1377 if base == default_branch {
1378 return self.clone();
1379 }
1380 RepoSettings {
1381 required_checks: Vec::new(),
1382 require_up_to_date: false,
1383 required_approvals: 0,
1384 merge_queue: false,
1385 ..self.clone()
1386 }
1387 }
1388}
1389
1390impl Default for RepoSettings {
1391 fn default() -> Self {
1392 RepoSettings {
1393 auto_merge: false,
1394 required_checks: Vec::new(),
1395 require_up_to_date: false,
1396 required_approvals: 0,
1397 count_agent_approvals: true,
1398 allow_ignoring_checks: true,
1399 agent_review: true,
1400 max_revisions: 2,
1401 merge_queue: false,
1402 hold_low_confidence: true,
1403 require_code_owner_review: false,
1404 updated_by: None,
1405 updated_at: None,
1406 }
1407 }
1408}
1409
1410/// `update_settings`: replaces a repository's settings. Members of its
1411/// workspace only. Returns `Outcome<RepoSettings>`. `get_settings` takes
1412/// `ViewArgs` and returns the same.
1413#[derive(Debug, Serialize, Deserialize)]
1414pub struct UpdateSettingsArgs {
1415 pub actor: User,
1416 pub repo: RepoPath,
1417 /// Who changed them and when are filled in by the service.
1418 pub settings: RepoSettings,
1419}
1420
1421/// `catch_up_job`: what the runner needs to bring a pull request up to date
1422/// because a merge of it was asked for. Null if none was. Returns
1423/// `Option<LifecycleJob>`.
1424#[derive(Debug, Serialize, Deserialize)]
1425#[serde(rename_all = "camelCase")]
1426pub struct CatchUpJobArgs {
1427 pub pull_id: String,
1428}
1429
1430/// `wake_for_messages`: the agent on a pull request was asked a question
1431/// or handed work while it was not at work. Claims a short step for it to
1432/// answer, and hands over what it was sent, marked read. Null when there
1433/// is nothing waiting, or the pull request cannot take a step now.
1434/// Returns `Option<Wake>`.
1435#[derive(Debug, Serialize, Deserialize)]
1436#[serde(rename_all = "camelCase")]
1437pub struct WakeForMessagesArgs {
1438 pub pull_id: String,
1439}
1440
1441/// What an agent woken to answer needs: its pull request, and what it was
1442/// sent, oldest first.
1443#[derive(Debug, Serialize, Deserialize)]
1444#[serde(rename_all = "camelCase")]
1445pub struct Wake {
1446 pub job: LifecycleJob,
1447 pub messages: Vec<AgentMessage>,
1448}
1449
1450/// `stall`: records that a step could not be carried out, so that g1t
1451/// stops and a person is asked. Returns `bool`.
1452#[derive(Debug, Serialize, Deserialize)]
1453#[serde(rename_all = "camelCase")]
1454pub struct StallArgs {
1455 pub pull_id: String,
1456 pub reason: String,
1457 /// The person who stopped it, by id, when someone did: they are not
1458 /// told it needs them.
1459 #[serde(default)]
1460 pub by: Option<String>,
1461}
1462
1463/// `managed_pulls`: ids of the open pull requests g1t is seeing through,
1464/// in one repository or in all of them. Returns `Vec<String>`.
1465#[derive(Debug, Default, Serialize, Deserialize)]
1466#[serde(rename_all = "camelCase")]
1467pub struct ManagedPullsArgs {
1468 #[serde(default)]
1469 pub repo_id: Option<String>,
1470}
1471
1472/// `open_issue`. Returns `Outcome<Issue>`.
1473#[derive(Debug, Serialize, Deserialize)]
1474pub struct OpenIssueArgs {
1475 pub actor: User,
1476 pub repo: RepoPath,
1477 pub title: String,
1478 #[serde(default)]
1479 pub body: String,
1480 #[serde(default)]
1481 pub labels: Vec<String>,
1482 /// Deprecated: commands, added to the body under "Definition of done".
1483 /// Checks are the workflows the branch's protection requires.
1484 #[serde(default)]
1485 pub checks: Vec<String>,
1486 /// The number of the milestone to put it in. Needs the Triage role.
1487 #[serde(default)]
1488 pub milestone: Option<u32>,
1489}
1490
1491/// `delegate_issue`: opens an issue to put g1t on at once, refused
1492/// before anything is opened unless `actor` may put agents to work in the
1493/// repository (Run, which the Write role has). The runner service's
1494/// `delegate` calls it and then starts the agent. Returns `Outcome<Issue>`.
1495#[derive(Debug, Serialize, Deserialize)]
1496pub struct DelegateIssueArgs {
1497 pub actor: User,
1498 pub repo: RepoPath,
1499 pub title: String,
1500 #[serde(default)]
1501 pub body: String,
1502 #[serde(default)]
1503 pub labels: Vec<String>,
1504 /// Deprecated, as on `OpenIssueArgs`.
1505 #[serde(default)]
1506 pub checks: Vec<String>,
1507}
1508
1509/// What became of the agent when an issue was opened and handed to it in
1510/// one step.
1511#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1512#[serde(rename_all = "snake_case")]
1513pub enum AgentStartStatus {
1514 /// It is at work on the issue's pull request.
1515 Started,
1516 /// Every agent slot of the workspace is busy: it starts on its own when
1517 /// one frees up.
1518 Queued,
1519 /// It did not start, and will not until someone fixes what `code` says.
1520 NotStarted,
1521}
1522
1523/// Whether the agent started, and if not, why and what fixes it.
1524#[derive(Clone, Debug, Serialize, Deserialize)]
1525#[serde(rename_all = "camelCase")]
1526pub struct AgentStart {
1527 pub status: AgentStartStatus,
1528 /// Why it did not start: `not_paid`, `trial_used`, `limit`, `paused`,
1529 /// `issue_cap`, `billing_unavailable` or `no_model`; `waiting` when
1530 /// queued.
1531 #[serde(default)]
1532 pub code: Option<String>,
1533 /// What happened, in a sentence or two, with what to do.
1534 #[serde(default)]
1535 pub message: Option<String>,
1536 /// Where the fix is: the workspace's billing or model settings.
1537 #[serde(default)]
1538 pub fix_url: Option<String>,
1539}
1540
1541/// The runner service's `delegate`: the issue opened, and the agent put on
1542/// it. The issue exists whatever became of the agent.
1543#[derive(Clone, Debug, Serialize, Deserialize)]
1544pub struct Delegated {
1545 pub issue: Issue,
1546 /// The pull request the agent opened, when it started.
1547 #[serde(default)]
1548 pub pull: Option<Pull>,
1549 pub agent: AgentStart,
1550}
1551
1552/// `report_confidence`: what the agent of a run says of its own change,
1553/// with the run's own token. Kept with the run, and the pull request's
1554/// confidence is worked out again with it. Returns `Outcome<bool>`.
1555#[derive(Debug, Serialize, Deserialize)]
1556#[serde(rename_all = "camelCase")]
1557pub struct ReportConfidenceArgs {
1558 pub run_id: String,
1559 pub token: String,
1560 /// `high`, `medium` or `low`.
1561 pub confidence: String,
1562 #[serde(default)]
1563 pub uncertain_about: Vec<String>,
1564}
1565
1566/// `list_issues`, newest first. Returns `Outcome<Vec<Issue>>`.
1567#[derive(Debug, Serialize, Deserialize)]
1568pub struct ListIssuesArgs {
1569 pub repo: RepoPath,
1570 pub viewer: Viewer,
1571 #[serde(default)]
1572 pub state: Option<State>,
1573 /// Only issues carrying this label.
1574 #[serde(default)]
1575 pub label: Option<String>,
1576 /// Only issues in the milestone of this number.
1577 #[serde(default)]
1578 pub milestone: Option<u32>,
1579}
1580
1581/// `list_pulls`, newest first. Returns `Outcome<Vec<Pull>>`.
1582#[derive(Debug, Serialize, Deserialize)]
1583pub struct ListPullsArgs {
1584 pub repo: RepoPath,
1585 pub viewer: Viewer,
1586 #[serde(default)]
1587 pub state: Option<State>,
1588 /// Only pull requests carrying this label.
1589 #[serde(default)]
1590 pub label: Option<String>,
1591 /// Only pull requests in the milestone of this number.
1592 #[serde(default)]
1593 pub milestone: Option<u32>,
1594 /// Only pull requests into this branch.
1595 #[serde(default)]
1596 pub base: Option<String>,
1597}
1598
1599/// `pulls_for_repos`: the newest open and the newest closed pull requests
1600/// of many repositories, in one call, for pages that show several projects
1601/// at once. Repositories the viewer cannot read are left out, as are forks
1602/// (ask those with `list_pulls`). Returns `Vec<RepoPulls>`.
1603#[derive(Debug, Serialize, Deserialize)]
1604#[serde(rename_all = "camelCase")]
1605pub struct PullsForReposArgs {
1606 /// At most [`MAX_PULLS_FOR_REPOS`] are looked at.
1607 pub repo_ids: Vec<String>,
1608 pub viewer: Viewer,
1609 /// How many of each, open and closed, per repository (at most 100).
1610 pub limit: u32,
1611}
1612
1613/// The most repositories one `pulls_for_repos` call looks at.
1614pub const MAX_PULLS_FOR_REPOS: usize = 50;
1615
1616/// One repository's pull requests from `pulls_for_repos`, newest first.
1617#[derive(Debug, Serialize, Deserialize)]
1618#[serde(rename_all = "camelCase")]
1619pub struct RepoPulls {
1620 pub repo_id: String,
1621 /// Draft and open.
1622 pub open: Vec<Pull>,
1623 /// Merged and closed.
1624 pub closed: Vec<Pull>,
1625}
1626
1627/// `get_issue` (`Outcome<IssueDetail>`), `get_pull` (`Outcome<PullDetail>`),
1628/// `read_session` (`Outcome<Vec<SessionEntry>>`), `list_labels`
1629/// (`Outcome<Vec<String>>`) and `counts` (`Outcome<Counts>`). The last two
1630/// ignore `number`.
1631#[derive(Debug, Serialize, Deserialize)]
1632#[serde(rename_all = "camelCase")]
1633pub struct ViewArgs {
1634 pub repo: RepoPath,
1635 #[serde(default)]
1636 pub number: u32,
1637 pub viewer: Viewer,
1638 /// For `read_session`: only entries after this sequence number.
1639 #[serde(default)]
1640 pub after_seq: u32,
1641}
1642
1643/// How many issues and pull requests are open on a repository.
1644#[derive(Debug, Serialize, Deserialize)]
1645pub struct Counts {
1646 pub issues: u32,
1647 pub pulls: u32,
1648}
1649
1650/// `update_issue`: changes whichever fields are given. The author or a
1651/// member of the workspace may. Returns `Outcome<Issue>`.
1652#[derive(Debug, Serialize, Deserialize)]
1653pub struct UpdateIssueArgs {
1654 pub actor: User,
1655 pub repo: RepoPath,
1656 pub number: u32,
1657 #[serde(default)]
1658 pub title: Option<String>,
1659 #[serde(default)]
1660 pub body: Option<String>,
1661 #[serde(default)]
1662 pub labels: Option<Vec<String>>,
1663 /// Usernames of the people it is assigned to; replaces the whole set.
1664 /// Assigning it to g1t is the runner's `run`, not this.
1665 #[serde(default)]
1666 pub assignees: Option<Vec<String>>,
1667 /// The number of the milestone to put it in; 0 takes it out of its
1668 /// milestone. Needs the Triage role.
1669 #[serde(default)]
1670 pub milestone: Option<u32>,
1671}
1672
1673/// `close_issue` and `reopen_issue`. Each returns `Outcome<Issue>`.
1674#[derive(Debug, Serialize, Deserialize)]
1675pub struct IssueActionArgs {
1676 pub actor: User,
1677 pub repo: RepoPath,
1678 pub number: u32,
1679 /// For `close_issue`; `completed` if left out.
1680 #[serde(default)]
1681 pub reason: Option<IssueReason>,
1682}
1683
1684/// `add_comment`, on an issue or a pull request. On a pull request it may
1685/// name a line of the change, and may carry a verdict; nobody can give a
1686/// verdict on their own pull request. Returns `Outcome<Comment>`.
1687#[derive(Debug, Serialize, Deserialize)]
1688pub struct AddCommentArgs {
1689 pub actor: User,
1690 pub repo: RepoPath,
1691 pub number: u32,
1692 /// May be empty when approving.
1693 #[serde(default)]
1694 pub body: String,
1695 #[serde(default)]
1696 pub path: Option<String>,
1697 #[serde(default)]
1698 pub line: Option<u32>,
1699 #[serde(default)]
1700 pub verdict: Option<Verdict>,
1701}
1702
1703/// `open_pull`. Without `branch`, forks the repo and returns a draft pull
1704/// request to push to. With it, opens a pull request, ready for review,
1705/// for a branch already pushed to the repo. Returns `Outcome<Pull>`.
1706#[derive(Debug, Serialize, Deserialize)]
1707pub struct OpenPullArgs {
1708 pub actor: User,
1709 pub repo: RepoPath,
1710 /// The issue this is for.
1711 #[serde(default)]
1712 pub issue: Option<u32>,
1713 /// Defaults to the issue's title; required without an issue.
1714 #[serde(default)]
1715 pub title: String,
1716 /// What changed and why. Usually set later, when a draft is marked ready.
1717 #[serde(default)]
1718 pub body: String,
1719 /// A branch of the repository that already holds the change.
1720 #[serde(default)]
1721 pub branch: Option<String>,
1722 #[serde(default)]
1723 pub agent: String,
1724 pub runtime: Runtime,
1725 /// The branch to merge into: the default branch when absent.
1726 #[serde(default)]
1727 pub base: Option<String>,
1728}
1729
1730/// `ready_pull`, `close_pull` and `merge_pull`. Each returns `Outcome<Pull>`.
1731///
1732/// Also `catch_up_pull`: brings the pull request up to date with the
1733/// default branch without a sandbox where that is safe, as the repos
1734/// service's `update_pull_branch` does, after checking that `actor` may
1735/// update it: its owner ([`Pull::owner`]) for a fork, any member for a
1736/// branch.
1737/// Returns `Outcome<repos::PullBranchUpdate>`; on `needs_agent` nothing was
1738/// pushed and the runner's `update` is the way on.
1739#[derive(Debug, Serialize, Deserialize)]
1740#[serde(rename_all = "camelCase")]
1741pub struct PullActionArgs {
1742 pub actor: User,
1743 pub repo: RepoPath,
1744 pub number: u32,
1745 /// For `ready_pull`: what changed and why.
1746 #[serde(default)]
1747 pub summary: String,
1748 /// For `merge_pull`: leave the issue open and the other pull requests
1749 /// for it untouched, because this one is only part of the work.
1750 #[serde(default)]
1751 pub keep_issue_open: bool,
1752 /// For `merge_pull`: merge although required checks have not passed,
1753 /// where the repository lets members bypass them.
1754 #[serde(default)]
1755 pub ignore_checks: bool,
1756}
1757
1758/// Where a plan stands.
1759#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1760#[serde(rename_all = "lowercase")]
1761pub enum PlanStatus {
1762 /// An agent is reading the repository and writing it.
1763 Planning,
1764 /// Written, and waiting for a person to read and apply it.
1765 Ready,
1766 /// It could not be written.
1767 Failed,
1768 /// Its issues have been opened.
1769 Applied,
1770}
1771
1772/// One issue a plan proposes.
1773#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1774#[serde(rename_all = "camelCase", default)]
1775pub struct PlannedIssue {
1776 pub title: String,
1777 /// Markdown: what to change, where, and why.
1778 pub body: String,
1779 pub labels: Vec<String>,
1780 /// What is true once it is done, in plain words. Added to the issue's
1781 /// body under "Definition of done". Plans written before this was
1782 /// called `done` named it `checks`.
1783 #[serde(alias = "checks")]
1784 pub done: Vec<String>,
1785 /// The files it will most likely change.
1786 pub files: Vec<String>,
1787 /// The positions, counting from 1, of earlier issues in the plan that
1788 /// have to be merged first. An agent writes this as `depends_on`.
1789 #[serde(alias = "depends_on")]
1790 pub depends_on: Vec<u32>,
1791 /// Its number, once the plan has been applied and it was kept.
1792 pub number: Option<u32>,
1793}
1794
1795/// An outcome someone wrote, and the issues an agent proposes to get there.
1796#[derive(Clone, Debug, Serialize, Deserialize)]
1797#[serde(rename_all = "camelCase")]
1798pub struct Plan {
1799 pub id: String,
1800 pub repo_id: String,
1801 /// The outcome wanted, as written.
1802 pub brief: String,
1803 pub status: PlanStatus,
1804 /// The agent's account of how it split the work.
1805 pub summary: String,
1806 pub issues: Vec<PlannedIssue>,
1807 /// Why it could not be written, when `status` is `failed`.
1808 pub error: Option<String>,
1809 pub author: User,
1810 /// RFC 3339.
1811 pub created_at: String,
1812 /// RFC 3339.
1813 pub finished_at: Option<String>,
1814 /// Once applied: where each issue it opened stands now, in plan order.
1815 /// Filled in by `get_plan` only.
1816 #[serde(default)]
1817 pub progress: Vec<IssueProgress>,
1818 /// Questions and handoffs between the agents on its pull requests,
1819 /// newest first. Filled in by `get_plan` only.
1820 #[serde(default)]
1821 pub exchanges: Vec<AgentMessage>,
1822}
1823
1824/// Where one issue of an applied plan stands.
1825#[derive(Clone, Debug, Serialize, Deserialize)]
1826#[serde(rename_all = "camelCase")]
1827pub struct IssueProgress {
1828 pub number: u32,
1829 pub title: String,
1830 /// `blocked` (waiting on issues it depends on), `waiting` (for an
1831 /// agent), `open` (nobody on it), one of the lifecycle stages
1832 /// (`working`, `checking`, `reviewing`, `revising`, `catching_up`,
1833 /// `answering`, `queued`, `ready`, `needs_you`), `landed` or `closed`.
1834 pub state: String,
1835 /// One sentence about where it stands.
1836 pub detail: String,
1837 /// The issues it is waiting on that are still open.
1838 pub blocked_by: Vec<u32>,
1839 /// The pull request carrying it, the newest if several.
1840 pub pull: Option<u32>,
1841 /// Who or what is working on it, e.g. `g1t`.
1842 pub agent: Option<String>,
1843}
1844
1845/// `start_plan`: records an outcome to plan for. Members of the
1846/// repository's workspace only. Called by the runner service, which starts
1847/// the sandbox. Returns `Outcome<PlanJob>`.
1848#[derive(Debug, Serialize, Deserialize)]
1849pub struct StartPlanArgs {
1850 pub actor: User,
1851 pub repo: RepoPath,
1852 pub brief: String,
1853}
1854
1855/// What a sandbox needs to write a plan.
1856#[derive(Debug, Serialize, Deserialize)]
1857#[serde(rename_all = "camelCase")]
1858pub struct PlanJob {
1859 pub plan_id: String,
1860 /// Lets the sandbox, and nothing else, report this plan.
1861 pub token: String,
1862 pub brief: String,
1863 pub repo: RepoPath,
1864}
1865
1866/// `report_plan`: the plan a sandbox's agent wrote, or why it could not
1867/// write one. Returns `Outcome<bool>`.
1868#[derive(Debug, Serialize, Deserialize)]
1869#[serde(rename_all = "camelCase")]
1870pub struct ReportPlanArgs {
1871 pub plan_id: String,
1872 pub token: String,
1873 #[serde(default)]
1874 pub summary: String,
1875 #[serde(default)]
1876 pub issues: Vec<PlannedIssue>,
1877 #[serde(default)]
1878 pub error: Option<String>,
1879}
1880
1881/// `get_plan`. Members only. Returns `Outcome<Plan>`. `list_plans` takes
1882/// `ViewArgs` and returns `Outcome<Vec<Plan>>`, newest first.
1883#[derive(Debug, Serialize, Deserialize)]
1884pub struct PlanArgs {
1885 pub repo: RepoPath,
1886 pub viewer: Viewer,
1887 pub id: String,
1888}
1889
1890/// `apply_plan`: opens a plan's issues, each blocked by the ones it depends
1891/// on. Members only, and once. Returns `Outcome<Plan>`, its issues now
1892/// carrying their numbers.
1893#[derive(Debug, Serialize, Deserialize)]
1894pub struct ApplyPlanArgs {
1895 pub actor: User,
1896 pub repo: RepoPath,
1897 pub id: String,
1898 /// Queue every issue for a g1t agent.
1899 #[serde(default)]
1900 pub assign: bool,
1901 /// The positions, counting from 1, of the issues to open. All of them
1902 /// when absent.
1903 #[serde(default)]
1904 pub keep: Option<Vec<u32>>,
1905}
1906
1907/// `queue_issue`: asks for a g1t agent to take an issue as soon as it can,
1908/// or withdraws that. The author or a member may. Returns `Outcome<bool>`.
1909#[derive(Debug, Serialize, Deserialize)]
1910pub struct QueueIssueArgs {
1911 pub actor: User,
1912 pub repo: RepoPath,
1913 pub number: u32,
1914 pub queued: bool,
1915}
1916
1917/// `ready_issues`: issues waiting for a g1t agent that can be given one
1918/// now, in one repository or in all. Called by the runner service. Returns
1919/// `Vec<ReadyIssue>`.
1920#[derive(Debug, Default, Serialize, Deserialize)]
1921#[serde(rename_all = "camelCase")]
1922pub struct ReadyIssuesArgs {
1923 #[serde(default)]
1924 pub repo_id: Option<String>,
1925}
1926
1927#[derive(Debug, Serialize, Deserialize)]
1928pub struct ReadyIssue {
1929 pub repo: RepoPath,
1930 pub number: u32,
1931 /// Who queued it, on whose say-so the agent works.
1932 pub actor: User,
1933}
1934
1935/// `update_pull`: changes who a pull request is assigned to, whose review
1936/// is asked for, its labels, its milestone and the branch it merges into.
1937/// Each list given replaces the whole set. Whoever opened it, or someone
1938/// with the Triage role, may; changing the base needs the Write role.
1939/// Returns `Outcome<Pull>`.
1940#[derive(Debug, Serialize, Deserialize)]
1941pub struct UpdatePullArgs {
1942 pub actor: User,
1943 pub repo: RepoPath,
1944 pub number: u32,
1945 #[serde(default)]
1946 pub assignees: Option<Vec<String>>,
1947 /// May include `g1t`. Asking for its review does not by itself
1948 /// start one; the runner's `review` does. A team is named
1949 /// `workspace/team` (or `@workspace/team`): the whole list, people and
1950 /// teams, replaces who is asked.
1951 #[serde(default)]
1952 pub reviewers: Option<Vec<String>>,
1953 /// Its labels; replaces the whole set, as `set_labels` does.
1954 #[serde(default)]
1955 pub labels: Option<Vec<String>>,
1956 /// The number of the milestone to put it in; 0 takes it out. Needs the
1957 /// Triage role.
1958 #[serde(default)]
1959 pub milestone: Option<u32>,
1960 /// The branch it merges into: an existing branch other than its own.
1961 /// Needs the Write role.
1962 #[serde(default)]
1963 pub base: Option<String>,
1964}
1965
1966/// `start_checks`: always refused now; a pull request's checks are the
1967/// workflows run on it. Kept so that a runner from before is answered.
1968/// Returns `Outcome<CheckJob>`.
1969#[derive(Debug, Serialize, Deserialize)]
1970#[serde(rename_all = "camelCase")]
1971pub struct StartChecksArgs {
1972 pub pull_id: String,
1973}
1974
1975/// What a sandbox needs to carry out a check run.
1976#[derive(Debug, Serialize, Deserialize)]
1977#[serde(rename_all = "camelCase")]
1978pub struct CheckJob {
1979 pub run_id: String,
1980 /// Lets the sandbox, and nothing else, report this run's results.
1981 pub token: String,
1982 pub commands: Vec<String>,
1983 /// The repository holding the commit: the fork, or the repository itself.
1984 pub source: RepoPath,
1985 pub commit: String,
1986 /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
1987 /// it, or its author), and so can read its source.
1988 pub author: User,
1989 /// Username of whoever wrote the checks: the issue's author.
1990 pub requested_by: String,
1991 pub repo: RepoPath,
1992 pub number: u32,
1993}
1994
1995/// `report_checks`: what a sandbox says about its run. With no results and
1996/// no error it has started. `skip` forgets the run, for one that will not
1997/// be carried out. Returns `Outcome<CheckRun>`.
1998#[derive(Debug, Serialize, Deserialize)]
1999#[serde(rename_all = "camelCase")]
2000pub struct ReportChecksArgs {
2001 pub run_id: String,
2002 pub token: String,
2003 #[serde(default)]
2004 pub results: Vec<CheckResult>,
2005 #[serde(default)]
2006 pub error: Option<String>,
2007 #[serde(default)]
2008 pub skip: bool,
2009}
2010
2011/// A pull request in progress, with where it lives.
2012#[derive(Debug, Serialize, Deserialize)]
2013pub struct ActivePull {
2014 pub pull: Pull,
2015 pub issue: Option<Issue>,
2016 /// Where it stands, for one g1t is seeing through.
2017 #[serde(default)]
2018 pub lifecycle: Option<Lifecycle>,
2019}
2020
2021/// `list_active_pulls`: drafts and open pull requests the viewer started,
2022/// most recently active first. Returns `Vec<ActivePull>`. Also
2023/// `list_assigned_issues`: open issues assigned to the viewer, most
2024/// recently changed first. Returns `Vec<Issue>`.
2025#[derive(Debug, Serialize, Deserialize)]
2026pub struct ViewerArgs {
2027 pub viewer: Viewer,
2028}
2029
2030/// `append_session`. Returns `Outcome<Appended>`.
2031#[derive(Debug, Serialize, Deserialize)]
2032pub struct AppendSessionArgs {
2033 pub actor: User,
2034 pub repo: RepoPath,
2035 pub number: u32,
2036 pub entries: Vec<NewSessionEntry>,
2037}
2038
2039#[derive(Debug, Serialize, Deserialize)]
2040pub struct Appended {
2041 pub count: u32,
2042}
2043
2044/// `start_review`: begins a review of a pull request by a g1t agent. Called
2045/// by the runner service, which starts the sandbox. Returns
2046/// `Outcome<ReviewJob>`.
2047#[derive(Debug, Serialize, Deserialize)]
2048#[serde(rename_all = "camelCase")]
2049pub struct StartReviewArgs {
2050 pub pull_id: String,
2051}
2052
2053/// What a sandbox needs to review a pull request.
2054#[derive(Debug, Serialize, Deserialize)]
2055#[serde(rename_all = "camelCase")]
2056pub struct ReviewJob {
2057 pub run_id: String,
2058 /// Lets the sandbox, and nothing else, report this review.
2059 pub token: String,
2060 /// The repository holding the commit: the fork, or the repository itself.
2061 pub source: RepoPath,
2062 pub commit: String,
2063 pub repo: RepoPath,
2064 pub default_branch: String,
2065 pub number: u32,
2066 pub title: String,
2067 pub description: String,
2068 /// The issue the pull request is for, which says what it should achieve.
2069 pub issue: Option<Issue>,
2070 /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
2071 /// it, or its author), and so can read its source.
2072 pub author: User,
2073 /// The files it changes, as of its latest push: how large the change
2074 /// is, which decides the model that reviews it.
2075 #[serde(default)]
2076 pub files: Vec<ChangedFile>,
2077 /// What among them runs, configures or guards things (CI workflows,
2078 /// secrets, infrastructure), once each. Any sends the review to the
2079 /// larger model.
2080 #[serde(default)]
2081 pub sensitive: Vec<String>,
2082}
2083
2084/// A comment on one line, as a reviewing agent reports it.
2085#[derive(Debug, Serialize, Deserialize)]
2086pub struct ReviewComment {
2087 pub path: String,
2088 #[serde(default)]
2089 pub line: u32,
2090 pub body: String,
2091}
2092
2093/// `report_review`: the review a sandbox's agent wrote, or why it could not
2094/// write one. Returns `Outcome<bool>`.
2095#[derive(Debug, Serialize, Deserialize)]
2096#[serde(rename_all = "camelCase")]
2097pub struct ReportReviewArgs {
2098 pub run_id: String,
2099 pub token: String,
2100 #[serde(default)]
2101 pub verdict: Option<Verdict>,
2102 #[serde(default)]
2103 pub body: String,
2104 #[serde(default)]
2105 pub comments: Vec<ReviewComment>,
2106 /// The model that wrote it, by its public name.
2107 #[serde(default)]
2108 pub model: Option<String>,
2109 #[serde(default)]
2110 pub error: Option<String>,
2111}
2112
2113/// Lowercases, trims and de-duplicates labels, dropping empty ones.
2114/// Returns `None` if there are too many or one is too long.
2115pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> {
2116 let mut normalized: Vec<String> = Vec::new();
2117 for label in labels {
2118 let label = label
2119 .split_whitespace()
2120 .collect::<Vec<_>>()
2121 .join(" ")
2122 .to_lowercase();
2123 if label.is_empty() || normalized.contains(&label) {
2124 continue;
2125 }
2126 if label.chars().count() > MAX_LABEL_CHARS {
2127 return None;
2128 }
2129 normalized.push(label);
2130 }
2131 (normalized.len() <= MAX_LABELS).then_some(normalized)
2132}
2133
2134#[cfg(test)]
2135mod tests {
2136 use super::*;
2137
2138 fn labels(names: &[&str]) -> Vec<String> {
2139 names.iter().map(|name| (*name).to_owned()).collect()
2140 }
2141
2142 #[test]
2143 fn colors_and_due_dates_are_tidied() {
2144 assert_eq!(tidy_color("#A1B2C3").as_deref(), Some("a1b2c3"));
2145 assert_eq!(tidy_color("fc0").as_deref(), Some("ffcc00"));
2146 assert_eq!(tidy_color("red"), None);
2147 assert_eq!(tidy_color("12345"), None);
2148 assert_eq!(label_color_for("bug"), "d73a4a");
2149 assert_eq!(label_color_for("area: web"), label_color_for("area: web"));
2150 assert_eq!(tidy_due_on("2026-10-14").as_deref(), Some("2026-10-14"));
2151 assert_eq!(tidy_due_on("2026-10-14T00:00:00Z").as_deref(), Some("2026-10-14"));
2152 assert_eq!(tidy_due_on("2026-13-01"), None);
2153 assert_eq!(tidy_due_on("soon"), None);
2154 }
2155
2156 #[test]
2157 fn a_milestone_is_as_far_along_as_its_closed_items() {
2158 let mut milestone: Milestone = serde_json::from_value(serde_json::json!({
2159 "number": 1, "title": "Launch", "state": "open", "createdAt": "", "updatedAt": ""
2160 }))
2161 .unwrap();
2162 assert_eq!(milestone.percent(), 0);
2163 milestone.open_items = 3;
2164 milestone.closed_items = 1;
2165 assert_eq!(milestone.percent(), 25);
2166 }
2167
2168 #[test]
2169 fn labels_are_lowercased_trimmed_and_unique() {
2170 assert_eq!(
2171 normalize_labels(&labels(&[" Bug ", "bug", "", "Good First Issue"])),
2172 Some(labels(&["bug", "good first issue"]))
2173 );
2174 }
2175
2176 #[test]
2177 fn too_long_or_too_many_labels_are_refused() {
2178 assert_eq!(normalize_labels(&["x".repeat(51)]), None);
2179 assert!(normalize_labels(&["x".repeat(50)]).is_some());
2180 let many: Vec<String> = (0..21).map(|i| format!("label-{i}")).collect();
2181 assert_eq!(normalize_labels(&many), None);
2182 }
2183}
2184
2185
2186// --- Merge queue ----------------------------------------------------------
2187
2188/// Where a pull request in a merge queue stands.
2189#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2190#[serde(rename_all = "snake_case")]
2191pub enum QueueState {
2192 /// Waiting for its turn to be tested.
2193 Waiting,
2194 /// Its combined state is being built and checked.
2195 Testing,
2196 /// Its combined state passed; it lands once everything ahead has.
2197 Passed,
2198 /// Its combined state failed, or would not merge. It left the queue.
2199 Failed,
2200 /// On the default branch.
2201 Landed,
2202 /// Taken out of the queue by a person, or closed.
2203 Removed,
2204}
2205
2206impl QueueState {
2207 pub fn as_str(self) -> &'static str {
2208 match self {
2209 QueueState::Waiting => "waiting",
2210 QueueState::Testing => "testing",
2211 QueueState::Passed => "passed",
2212 QueueState::Failed => "failed",
2213 QueueState::Landed => "landed",
2214 QueueState::Removed => "removed",
2215 }
2216 }
2217
2218 /// Still in the queue.
2219 pub fn is_active(self) -> bool {
2220 matches!(
2221 self,
2222 QueueState::Waiting | QueueState::Testing | QueueState::Passed
2223 )
2224 }
2225}
2226
2227/// One pull request's place in a merge queue.
2228#[derive(Clone, Debug, Serialize, Deserialize)]
2229#[serde(rename_all = "camelCase")]
2230pub struct QueueEntry {
2231 pub id: String,
2232 pub number: u32,
2233 pub title: String,
2234 /// Who or what made the pull request, e.g. `g1t`.
2235 pub agent: String,
2236 pub state: QueueState,
2237 /// The pull requests merged ahead of it in the state being tested, in
2238 /// queue order. Empty when it was tested on the default branch alone.
2239 pub ahead: Vec<u32>,
2240 /// The default branch's commit the tested state was built on.
2241 pub base_commit: Option<String>,
2242 /// The tested state: the default branch with everything ahead and this.
2243 pub combined_commit: Option<String>,
2244 /// Why it failed: a merge conflict or what could not be run.
2245 pub error: Option<String>,
2246 /// Commands run against the tested state by queues from before its
2247 /// checks were workflows. Empty since.
2248 pub results: Vec<CheckResult>,
2249 /// Username of whoever merged it into the queue: a person, or `g1t`.
2250 pub enqueued_by: String,
2251 /// RFC 3339.
2252 pub created_at: String,
2253 /// RFC 3339. When it landed or left.
2254 pub finished_at: Option<String>,
2255}
2256
2257/// A repository's merge queue: what is in it, in order, and what recently
2258/// left it.
2259#[derive(Clone, Debug, Serialize, Deserialize)]
2260#[serde(rename_all = "camelCase")]
2261pub struct QueueView {
2262 /// Whether the repository merges through the queue.
2263 pub enabled: bool,
2264 pub active: Vec<QueueEntry>,
2265 /// Newest first.
2266 pub recent: Vec<QueueEntry>,
2267}
2268
2269/// `queue`: a repository's merge queue. Returns `Outcome<QueueView>`.
2270#[derive(Debug, Serialize, Deserialize)]
2271pub struct QueueArgs {
2272 pub repo: RepoPath,
2273 pub viewer: Viewer,
2274}
2275
2276/// `queue_build`: the next batch to test for a repository, if nothing is
2277/// being tested now. Returns `Vec<QueueJob>`, one per entry, each testing
2278/// the default branch with that entry and everything ahead of it.
2279#[derive(Debug, Serialize, Deserialize)]
2280#[serde(rename_all = "camelCase")]
2281pub struct QueueBuildArgs {
2282 pub repo_id: String,
2283}
2284
2285/// One pull request in a state being tested: where its change is.
2286#[derive(Clone, Debug, Serialize, Deserialize)]
2287#[serde(rename_all = "camelCase")]
2288pub struct QueueStackItem {
2289 pub number: u32,
2290 pub title: String,
2291 /// The repository holding the change: its fork, or the repository.
2292 pub source: RepoPath,
2293 /// The branch of `source` holding it.
2294 pub branch: String,
2295 pub commit: String,
2296}
2297
2298/// What a sandbox needs to build and check one combined state.
2299#[derive(Clone, Debug, Serialize, Deserialize)]
2300#[serde(rename_all = "camelCase")]
2301pub struct QueueJob {
2302 pub entry_id: String,
2303 /// Lets the sandbox, and nothing else, report this state's result.
2304 pub token: String,
2305 pub repo: RepoPath,
2306 pub default_branch: String,
2307 /// The default branch's commit to build on.
2308 pub base_commit: String,
2309 /// Where to push the tested state, in the repository itself.
2310 pub branch: String,
2311 /// The pull requests to merge in, in order; the last is the entry.
2312 pub stack: Vec<QueueStackItem>,
2313 /// Commands to run on the built state. Always empty: the state is
2314 /// checked by the `merge_group` workflows run on it, and the default
2315 /// branch's required checks must pass there.
2316 pub checks: Vec<String>,
2317 /// Always empty, as `checks`.
2318 #[serde(default)]
2319 pub contract_checks: Vec<String>,
2320 /// Who the sandbox acts as: a member who can push the tested state.
2321 pub actor: User,
2322}
2323
2324/// `report_queue`: a sandbox's result for one combined state. Returns
2325/// `Outcome<QueueState>`.
2326#[derive(Debug, Serialize, Deserialize)]
2327#[serde(rename_all = "camelCase")]
2328pub struct ReportQueueArgs {
2329 pub entry_id: String,
2330 pub token: String,
2331 #[serde(default)]
2332 pub combined_commit: Option<String>,
2333 #[serde(default)]
2334 pub results: Vec<CheckResult>,
2335 /// Set when the state could not be built or checked.
2336 #[serde(default)]
2337 pub error: Option<String>,
2338 /// For a merge conflict: the pull request whose change it collided with.
2339 #[serde(default)]
2340 pub conflict_with: Option<u32>,
2341 /// For a merge conflict: the files that conflicted.
2342 #[serde(default)]
2343 pub conflicts: Vec<String>,
2344}
2345
2346
2347/// `locate_pull`: where a pull request lives, by its id, for a tool that
2348/// knows only the fork it is working in (`g1t.sh/pulls/<id>`). Returns
2349/// `Outcome<LocatedPull>`; not found for anyone who cannot see it.
2350#[derive(Debug, Serialize, Deserialize)]
2351pub struct LocatePullArgs {
2352 pub id: String,
2353 pub viewer: Viewer,
2354}
2355
2356#[derive(Clone, Debug, Serialize, Deserialize)]
2357#[serde(rename_all = "camelCase")]
2358pub struct LocatedPull {
2359 pub repo: RepoPath,
2360 pub number: u32,
2361 pub title: String,
2362 pub status: PullStatus,
2363}
2364
2365// --- A person's work -------------------------------------------------------
2366
2367/// Issues or pull requests, on a person's profile.
2368#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2369#[serde(rename_all = "lowercase")]
2370pub enum AuthoredKind {
2371 Issue,
2372 Pull,
2373}
2374
2375/// The state filter on a person's work. `Closed` takes in merged pull
2376/// requests too; `Merged` is only those.
2377#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2378#[serde(rename_all = "lowercase")]
2379pub enum AuthoredState {
2380 Open,
2381 Closed,
2382 Merged,
2383}
2384
2385/// How a person's work is ordered.
2386#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
2387#[serde(rename_all = "lowercase")]
2388pub enum AuthoredSort {
2389 /// Newest first.
2390 #[default]
2391 Created,
2392 /// Most recently changed first.
2393 Updated,
2394 /// Oldest first.
2395 Oldest,
2396}
2397
2398/// The most items one `by_author` page holds.
2399pub const AUTHORED_PAGE: u32 = 25;
2400
2401/// `by_author`: the issues and pull requests a person opened, only on
2402/// repositories `viewer` may read, so a private title never reaches anyone
2403/// who could not open it. Returns `Outcome<Authored>`; not found for an
2404/// account that does not exist.
2405#[derive(Debug, Serialize, Deserialize)]
2406pub struct ByAuthorArgs {
2407 pub username: String,
2408 pub viewer: Viewer,
2409 #[serde(default)]
2410 pub kind: Option<AuthoredKind>,
2411 #[serde(default)]
2412 pub state: Option<AuthoredState>,
2413 /// Only work on this repository: `namespace/name`.
2414 #[serde(default)]
2415 pub repo: Option<String>,
2416 #[serde(default)]
2417 pub sort: AuthoredSort,
2418 /// The `next` of the page before, to read on from there.
2419 #[serde(default)]
2420 pub before: Option<String>,
2421 /// At most [`AUTHORED_PAGE`]; that when absent.
2422 #[serde(default)]
2423 pub limit: Option<u32>,
2424}
2425
2426/// One issue or pull request a person opened.
2427#[derive(Clone, Debug, Serialize, Deserialize)]
2428#[serde(rename_all = "camelCase")]
2429pub struct AuthoredItem {
2430 pub kind: AuthoredKind,
2431 pub repo: RepoPath,
2432 pub number: u32,
2433 pub title: String,
2434 /// Open or closed; a merged pull request is closed.
2435 pub state: State,
2436 /// A pull request's own status.
2437 pub status: Option<PullStatus>,
2438 /// Why an issue was closed.
2439 pub reason: Option<IssueReason>,
2440 pub draft: bool,
2441 pub merged: bool,
2442 /// RFC 3339.
2443 pub created_at: String,
2444 /// RFC 3339.
2445 pub updated_at: String,
2446 /// When a pull request was merged. RFC 3339.
2447 pub merged_at: Option<String>,
2448}
2449
2450/// What a person has done, as far as the viewer may see.
2451#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2452#[serde(rename_all = "camelCase")]
2453pub struct AuthoredCounts {
2454 pub pulls_merged: u32,
2455 pub pulls_open: u32,
2456 pub pulls: u32,
2457 pub issues: u32,
2458 pub issues_open: u32,
2459}
2460
2461/// A repository a person has opened work on, with how much.
2462#[derive(Clone, Debug, Serialize, Deserialize)]
2463pub struct AuthoredRepo {
2464 pub repo: RepoPath,
2465 pub count: u32,
2466}
2467
2468/// A page of a person's work.
2469#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2470#[serde(rename_all = "camelCase")]
2471pub struct Authored {
2472 pub items: Vec<AuthoredItem>,
2473 /// Pass as `before` for the next page; null on the last.
2474 pub next: Option<String>,
2475 /// Over every repository the viewer may read, whatever the filters.
2476 pub counts: AuthoredCounts,
2477 /// Those repositories, most work first.
2478 pub repos: Vec<AuthoredRepo>,
2479}
2480
2481#[cfg(test)]
2482mod required_tests {
2483 use super::*;
2484
2485 fn status(context: &str, state: &str) -> CommitStatus {
2486 CommitStatus {
2487 context: context.into(),
2488 state: state.into(),
2489 description: Some(format!("{context} {state}")),
2490 target_url: None,
2491 updated_at: String::new(),
2492 }
2493 }
2494
2495 #[test]
2496 fn a_context_names_its_check_and_event() {
2497 assert_eq!(check_name("CI / pull_request"), ("CI", Some("pull_request")));
2498 assert_eq!(check_name("Build and test / merge_group"), ("Build and test", Some("merge_group")));
2499 assert_eq!(check_name("g1t / deploy"), ("g1t / deploy", None));
2500 assert_eq!(check_name("g1t / deploy (docs)"), ("g1t / deploy (docs)", None));
2501 assert_eq!(check_name("lint"), ("lint", None));
2502 }
2503
2504 #[test]
2505 fn required_checks_are_missing_pending_failed_or_passed() {
2506 let required = vec!["CI".to_owned(), "Lint".to_owned(), "g1t / deploy".to_owned(), "Docs".to_owned()];
2507 let statuses = [
2508 status("CI / pull_request", "success"),
2509 status("Lint / pull_request", "pending"),
2510 status("g1t / deploy", "failure"),
2511 ];
2512 let states: Vec<RequiredState> = required_checks(&required, &statuses).into_iter().map(|c| c.state).collect();
2513 assert_eq!(
2514 states,
2515 [RequiredState::Success, RequiredState::Pending, RequiredState::Failure, RequiredState::Expected]
2516 );
2517 }
2518
2519 #[test]
2520 fn any_event_reports_a_check_and_a_failure_wins() {
2521 let required = vec!["ci".to_owned()];
2522 let both = [status("CI / push", "failure"), status("CI / pull_request", "success")];
2523 let check = &required_checks(&required, &both)[0];
2524 assert_eq!(check.state, RequiredState::Failure);
2525 assert_eq!(check.description.as_deref(), Some("CI / push failure"));
2526 let queue = [status("CI / merge_group", "success")];
2527 assert_eq!(required_checks(&required, &queue)[0].state, RequiredState::Success);
2528 }
2529
2530 #[test]
2531 fn only_the_default_branch_is_protected() {
2532 let settings = RepoSettings {
2533 required_checks: vec!["CI".into()],
2534 require_up_to_date: true,
2535 required_approvals: 2,
2536 merge_queue: true,
2537 auto_merge: true,
2538 ..RepoSettings::default()
2539 };
2540 let main = settings.for_base("main", "main");
2541 assert_eq!((main.required_checks.len(), main.required_approvals, main.merge_queue), (1, 2, true));
2542 let release = settings.for_base("release/1.x", "main");
2543 assert!(release.required_checks.is_empty() && !release.require_up_to_date && !release.merge_queue);
2544 assert_eq!(release.required_approvals, 0);
2545 assert!(release.auto_merge, "how g1t's agents merge holds for every branch");
2546 }
2547
2548 #[test]
2549 fn a_pull_request_merges_into_its_base_or_the_default_branch() {
2550 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2551 "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": null, "title": "Fix it", "body": null,
2552 "agent": "agent", "runtime": "external", "status": "open", "fork": null, "forkRepoId": null,
2553 "branch": "fix", "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2554 "supersededBy": null, "checkStatus": null, "author": { "id": "x", "username": "x" },
2555 "createdAt": "", "updatedAt": ""
2556 }))
2557 .unwrap();
2558 assert_eq!(pull.base_branch("main"), "main");
2559 assert!(pull.targets_default("main"));
2560 assert_eq!(pull.comparison(&None).base_branch, None);
2561 pull.base = Some("release".into());
2562 assert_eq!(pull.base_branch("main"), "release");
2563 assert!(!pull.targets_default("main"));
2564 assert_eq!(pull.comparison(&None).base_branch.as_deref(), Some("release"));
2565 }
2566
2567 #[test]
2568 fn required_names_are_tidied() {
2569 let names = vec![" CI ".to_owned(), "ci".to_owned(), String::new(), "Lint".to_owned()];
2570 assert_eq!(tidy_required(&names), ["CI", "Lint"]);
2571 }
2572
2573 #[test]
2574 fn a_definition_of_done_is_added_once() {
2575 let items = commands_pass(&["cargo test".to_owned(), " ".to_owned()]);
2576 assert_eq!(items, ["`cargo test` passes."]);
2577 let body = with_definition_of_done("Fix the greeting.", &items);
2578 assert_eq!(body, "Fix the greeting.\n\n## Definition of done\n\n- `cargo test` passes.");
2579 assert_eq!(with_definition_of_done(&body, &items), body);
2580 assert_eq!(with_definition_of_done("", &items), "## Definition of done\n\n- `cargo test` passes.");
2581 assert_eq!(with_definition_of_done(" Text ", &[]), "Text");
2582 }
2583}
2584
2585#[cfg(test)]
2586mod authorship_tests {
2587 use super::*;
2588 use crate::PrincipalKind;
2589 use crate::credentials::{Acting, Principal};
2590 use crate::identity::{AGENT_ID, AgentScope};
2591
2592 fn person() -> User {
2593 User {
2594 id: "usr_1".into(),
2595 username: "syntaqx".into(),
2596 verified: true,
2597 ..User::default()
2598 }
2599 }
2600
2601 fn agent_for(id: &str, username: &str) -> User {
2602 User {
2603 id: AGENT_ID.into(),
2604 username: "g1t".into(),
2605 kind: PrincipalKind::Agent,
2606 acting: Some(Box::new(Acting {
2607 credential_id: "tok_1".into(),
2608 agent: "g1t".into(),
2609 on_behalf_of: Principal { id: id.into(), username: username.into() },
2610 scope: AgentScope {
2611 repo: RepoPath { namespace: "acme".into(), name: "web".into() },
2612 operations: Vec::new(),
2613 run: None,
2614 },
2615 })),
2616 ..User::default()
2617 }
2618 }
2619
2620 fn pull(author: User, requested_by: Option<User>) -> Pull {
2621 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2622 "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": 12, "title": "Fix it", "body": null,
2623 "agent": "g1t", "runtime": "hosted", "status": "open", "fork": null, "forkRepoId": null,
2624 "branch": null, "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2625 "supersededBy": null, "checkStatus": null,
2626 "author": { "id": "x", "username": "x" },
2627 "createdAt": "", "updatedAt": ""
2628 }))
2629 .unwrap();
2630 pull.author = author;
2631 pull.requested_by = requested_by;
2632 pull
2633 }
2634
2635 #[test]
2636 fn a_change_a_person_has_g1t_make_is_g1t_s_requested_by_them() {
2637 let (author, asked) = authorship(&person(), true);
2638 assert_eq!((author.id.as_str(), author.username.as_str(), author.kind), (AGENT_ID, "g1t", PrincipalKind::Agent));
2639 let asked = asked.expect("the person asked for it");
2640 assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
2641 }
2642
2643 #[test]
2644 fn what_g1t_s_agent_files_at_work_is_g1t_s_requested_by_whoever_it_works_for() {
2645 let (author, asked) = authorship(&agent_for("usr_1", "syntaqx"), false);
2646 assert_eq!(author.id, AGENT_ID);
2647 assert_eq!(asked.map(|user| user.username), Some("syntaqx".into()));
2648 }
2649
2650 #[test]
2651 fn nobody_asked_for_g1t_s_own_work() {
2652 // A run g1t started itself acts for g1t, not for a person.
2653 let (author, asked) = authorship(&agent_for(crate::system::ID, "g1t"), false);
2654 assert_eq!(author.id, AGENT_ID);
2655 assert!(asked.is_none());
2656 // A security update g1t opens is g1t's own, as before.
2657 let (author, asked) = authorship(&User::system("acme"), true);
2658 assert_eq!((author.id.as_str(), author.kind), (crate::system::ID, PrincipalKind::System));
2659 assert!(asked.is_none());
2660 }
2661
2662 #[test]
2663 fn anyone_else_opens_their_own() {
2664 let (author, asked) = authorship(&person(), false);
2665 assert_eq!((author.id.as_str(), author.kind), ("usr_1", PrincipalKind::User));
2666 assert!(asked.is_none());
2667 // Nothing but who they are is kept.
2668 assert!(author.workspaces.is_empty() && author.acting.is_none());
2669 }
2670
2671 #[test]
2672 fn the_requester_owns_g1t_s_pull_request_and_an_author_their_own() {
2673 let made = pull(g1t_author(), Some(person()));
2674 assert_eq!(made.owner().id, "usr_1");
2675 assert!(made.is_owned_by("usr_1"));
2676 assert!(!made.is_owned_by(AGENT_ID), "g1t's agent does not answer for its own change");
2677 let own = pull(person(), None);
2678 assert_eq!(own.owner().id, "usr_1");
2679 assert!(own.is_owned_by("usr_1"));
2680 }
2681
2682 #[test]
2683 fn requested_by_is_null_when_nobody_asked_and_read_as_absent_from_older_senders() {
2684 let own = pull(person(), None);
2685 let sent = serde_json::to_value(&own).unwrap();
2686 assert!(sent["requestedBy"].is_null());
2687 let mut older = sent.clone();
2688 older.as_object_mut().unwrap().remove("requestedBy");
2689 let read: Pull = serde_json::from_value(older).unwrap();
2690 assert!(read.requested_by.is_none());
2691 }
2692}