Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
2,339 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Docs: the inbox, what lands in it and why, its tabs and actions1//! 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
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'14// Sessions, claims and dependencies: work/coordination.rs.
15mod coordination;
16pub use coordination::*;
17
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar18/// The labels a new repository starts with, and that "Add the default
19/// labels" adds to one that is missing some: `(name, color, description)`.
20/// Colors are six hex digits, without `#`.
21pub const DEFAULT_LABELS: [(&str, &str, &str); 11] = [
22 ("bug", "d73a4a", "Something isn't working"),
23 ("documentation", "0075ca", "Improvements or additions to documentation"),
24 ("duplicate", "cfd3d7", "This issue or pull request already exists"),
25 ("enhancement", "a2eeef", "New feature or request"),
26 ("good first issue", "7057ff", "Good for newcomers"),
27 ("help wanted", "008672", "Extra attention is needed"),
28 ("invalid", "e4e669", "This doesn't seem right"),
29 ("question", "d876e3", "Further information is requested"),
30 ("wontfix", "ffffff", "This will not be worked on"),
31 ("dependencies", "0366d6", "Updates a dependency"),
32 ("security", "ee0701", "A security fix or a vulnerability"),
33];
34
35/// The color a label gets when none is given: chosen from its name, so
36/// the same name always gets the same color.
37pub fn label_color_for(name: &str) -> String {
38 const PALETTE: [&str; 12] = [
39 "b60205", "d93f0b", "fbca04", "0e8a16", "006b75", "1d76db", "0052cc", "5319e7", "e99695", "f9d0c4",
40 "c2e0c6", "bfdadc",
41 ];
42 if let Some((_, color, _)) = DEFAULT_LABELS.iter().find(|(known, _, _)| *known == name) {
43 return (*color).to_owned();
44 }
45 let hash = name.bytes().fold(0u32, |hash, byte| hash.wrapping_mul(31).wrapping_add(u32::from(byte)));
46 PALETTE[(hash as usize) % PALETTE.len()].to_owned()
47}
48
49/// A color as six lowercase hex digits, from `#A1B2C3`, `a1b2c3` or `abc`.
50pub fn tidy_color(color: &str) -> Option<String> {
51 let hex = color.trim().trim_start_matches('#').to_ascii_lowercase();
52 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
53 return None;
54 }
55 match hex.len() {
56 6 => Some(hex),
57 3 => Some(hex.chars().flat_map(|c| [c, c]).collect()),
58 _ => None,
59 }
60}
61
62/// The most labels one issue or pull request can carry, and the longest
63/// name and description of one.
64pub const MAX_LABELS: usize = 20;
65pub const MAX_LABEL_CHARS: usize = 50;
66pub const MAX_LABEL_DESCRIPTION_CHARS: usize = 100;
Docs: the inbox, what lands in it and why, its tabs and actions67
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar68/// A label of a repository: a name, a color and what it means. Issues and
69/// pull requests carry labels by name. Names are lowercase and unique in
70/// a repository.
71#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct Label {
74 pub name: String,
75 /// Six hex digits, without `#`.
76 pub color: String,
77 #[serde(default)]
78 pub description: String,
79 /// How many issues carry it, open or closed.
80 #[serde(default)]
81 pub issues: u32,
82 /// How many pull requests carry it, in any state.
83 #[serde(default)]
84 pub pulls: u32,
85}
86
87/// A milestone, as an issue or pull request names it.
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct MilestoneRef {
91 pub number: u32,
92 pub title: String,
93}
94
95/// A milestone: a goal, with an optional due date, that issues and pull
96/// requests are gathered under. Its progress is how many of them are
97/// closed (a merged pull request counts as closed).
98#[derive(Clone, Debug, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct Milestone {
101 /// Numbered from 1 in each repository, apart from issues.
102 pub number: u32,
103 pub title: String,
104 /// Markdown.
105 #[serde(default)]
106 pub description: String,
107 /// The day it is due, `YYYY-MM-DD`.
108 #[serde(default)]
109 pub due_on: Option<String>,
110 pub state: State,
111 /// Open issues and pull requests in it (drafts count as open).
112 #[serde(default)]
113 pub open_items: u32,
114 /// Closed issues, and merged or closed pull requests, in it.
115 #[serde(default)]
116 pub closed_items: u32,
117 /// RFC 3339.
118 pub created_at: String,
119 /// RFC 3339.
120 pub updated_at: String,
121 /// RFC 3339.
122 #[serde(default)]
123 pub closed_at: Option<String>,
124}
125
126impl Milestone {
127 /// How far along it is, from 0 to 100: closed items over all of them.
128 pub fn percent(&self) -> u32 {
129 let total = self.open_items + self.closed_items;
130 (self.closed_items * 100).checked_div(total).unwrap_or(0)
131 }
132}
133
134/// The most characters a milestone's title and description may have.
135pub const MAX_MILESTONE_TITLE_CHARS: usize = 100;
136pub const MAX_MILESTONE_DESCRIPTION_CHARS: usize = 4000;
137
138/// A due date as `YYYY-MM-DD`, from that or from an RFC 3339 time.
139pub fn tidy_due_on(value: &str) -> Option<String> {
140 let day = value.trim().get(..10)?;
141 let bytes = day.as_bytes();
142 let digits = |range: std::ops::Range<usize>| bytes[range].iter().all(u8::is_ascii_digit);
143 if !(digits(0..4) && bytes[4] == b'-' && digits(5..7) && bytes[7] == b'-' && digits(8..10)) {
144 return None;
145 }
146 let month: u32 = day[5..7].parse().ok()?;
147 let date: u32 = day[8..10].parse().ok()?;
148 ((1..=12).contains(&month) && (1..=31).contains(&date)).then(|| day.to_owned())
149}
150
151/// `list_labels`: a repository's labels, by name, each with how many
152/// issues and pull requests carry it. Takes `ViewArgs`; returns
153/// `Outcome<Vec<Label>>`.
154///
155/// `save_label`: creates a label, or with `name` changes one (renaming it
156/// renames it on every issue and pull request). Needs the Triage role.
157/// Returns `Outcome<Label>`.
158#[derive(Debug, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct SaveLabelArgs {
161 pub actor: User,
162 pub repo: RepoPath,
163 /// The label to change; absent to create one.
164 #[serde(default)]
165 pub name: Option<String>,
166 /// The name it should have: required to create one.
167 #[serde(default)]
168 pub new_name: Option<String>,
169 /// Six hex digits; one is chosen from the name when creating without.
170 #[serde(default)]
171 pub color: Option<String>,
172 #[serde(default)]
173 pub description: Option<String>,
174}
175
176/// `delete_label`: removes a label from the repository and from every
177/// issue and pull request that carries it. Needs the Triage role. Returns
178/// `Outcome<bool>`.
179#[derive(Debug, Serialize, Deserialize)]
180pub struct DeleteLabelArgs {
181 pub actor: User,
182 pub repo: RepoPath,
183 pub name: String,
184}
185
186/// `add_default_labels`: adds those of [`DEFAULT_LABELS`] the repository
187/// does not have yet. Needs the Triage role. Returns
188/// `Outcome<Vec<Label>>`, every label it has now.
189#[derive(Debug, Serialize, Deserialize)]
190pub struct RepoActorArgs {
191 pub actor: User,
192 pub repo: RepoPath,
193}
194
195/// How `set_labels` changes an issue's or pull request's labels.
196#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(rename_all = "lowercase")]
198pub enum LabelChange {
199 /// Replace them all.
200 #[default]
201 Set,
202 /// Add these to the ones it has.
203 Add,
204 /// Take these off.
205 Remove,
206}
207
208/// `set_labels`: changes the labels of an issue or a pull request (they
209/// share numbers). A label the repository does not have yet is created
210/// when the actor has the Triage role; anyone else may only use existing
211/// ones, on what they opened. Returns `Outcome<Vec<String>>`, its labels
212/// now.
213#[derive(Debug, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct SetLabelsArgs {
216 pub actor: User,
217 pub repo: RepoPath,
218 pub number: u32,
219 pub labels: Vec<String>,
220 #[serde(default)]
221 pub change: LabelChange,
222}
223
224/// `list_milestones`: a repository's milestones, open ones by due date
225/// (soonest first, those without one last), then closed ones most recently
226/// closed first. Returns `Outcome<Vec<Milestone>>`.
227#[derive(Debug, Serialize, Deserialize)]
228pub struct ListMilestonesArgs {
229 pub repo: RepoPath,
230 pub viewer: Viewer,
231 /// Both when absent.
232 #[serde(default)]
233 pub state: Option<State>,
234}
235
236/// `get_milestone`: one milestone and what is in it. Takes `ViewArgs`,
237/// whose `number` is the milestone's. Returns `Outcome<MilestoneDetail>`.
238#[derive(Clone, Debug, Serialize, Deserialize)]
239pub struct MilestoneDetail {
240 pub milestone: Milestone,
241 /// Newest first, open and closed.
242 pub issues: Vec<Issue>,
243 /// Newest first, in any state.
244 pub pulls: Vec<Pull>,
245}
246
247/// `save_milestone`: creates a milestone, or with `number` changes the
248/// fields given. Needs the Triage role. Returns `Outcome<Milestone>`.
249#[derive(Debug, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct SaveMilestoneArgs {
252 pub actor: User,
253 pub repo: RepoPath,
254 /// The milestone to change; absent to create one.
255 #[serde(default)]
256 pub number: Option<u32>,
257 /// Required to create one.
258 #[serde(default)]
259 pub title: Option<String>,
260 #[serde(default)]
261 pub description: Option<String>,
262 /// `YYYY-MM-DD`; an empty string clears it.
263 #[serde(default)]
264 pub due_on: Option<String>,
265 #[serde(default)]
266 pub state: Option<State>,
267}
268
269/// `delete_milestone`: removes a milestone; what was in it is in none.
270/// Needs the Triage role. Returns `Outcome<bool>`.
271#[derive(Debug, Serialize, Deserialize)]
272pub struct DeleteMilestoneArgs {
273 pub actor: User,
274 pub repo: RepoPath,
275 pub number: u32,
276}
277
Docs: the inbox, what lands in it and why, its tabs and actions278/// `open` or `closed`: the filter on lists of issues and pull requests. An
279/// open pull request is a draft or one ready for review; a closed one was
280/// merged or closed without merging.
281#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(rename_all = "lowercase")]
283pub enum State {
284 Open,
285 Closed,
286}
287
288/// Why an issue was closed.
289#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291pub enum IssueReason {
292 /// The work was done. If a pull request did it, `resolved_by` names it.
293 Completed,
294 NotPlanned,
295}
296
297impl IssueReason {
298 pub fn as_str(self) -> &'static str {
299 match self {
300 IssueReason::Completed => "completed",
301 IssueReason::NotPlanned => "not_planned",
302 }
303 }
304}
305
306/// Something that should change in a repository: a bug, a feature, a
307/// question. Opened by a person, an agent or an integration. Pull requests
308/// are made against it; the one that is merged resolves it.
309#[derive(Clone, Debug, Serialize, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct Issue {
312 pub id: String,
313 pub repo_id: String,
314 /// Shown as `#12`.
315 pub number: u32,
316 pub title: String,
317 /// Markdown. Also what an agent is given to work from.
318 pub body: String,
319 pub labels: Vec<String>,
320 pub state: State,
321 /// Set when closed.
322 pub reason: Option<IssueReason>,
323 /// The number of the pull request whose merge closed this issue.
324 pub resolved_by: Option<u32>,
325 /// Who opened it: a person, an integration, or g1t (`kind` `agent`)
326 /// for one its agent filed while at work.
327 pub author: User,
328 /// For an issue g1t's agent filed: the person it was working for. They
329 /// may manage it as its author could. See [`owner`](Issue::owner).
330 #[serde(default)]
331 pub requested_by: Option<User>,
332 /// RFC 3339.
333 pub created_at: String,
334 /// RFC 3339.
335 pub updated_at: String,
336 /// RFC 3339.
337 pub closed_at: Option<String>,
338 /// Pull requests made against this issue, in any state.
339 pub pull_count: u32,
340 pub comment_count: u32,
341 /// Usernames of the people it is assigned to.
342 #[serde(default)]
343 pub assignees: Vec<String>,
344 /// The numbers of the issues that have to be merged before this one is
345 /// worked on.
346 #[serde(default)]
347 pub blocked_by: Vec<u32>,
348 /// Whether a g1t agent takes it as soon as it can: at once, or when
349 /// what it is blocked by has merged.
350 #[serde(default)]
351 pub queued: bool,
352 /// The agent working on it now: the one behind its newest pull request
353 /// that is still in progress in a fork, such as `g1t`.
354 #[serde(default)]
355 pub agent: Option<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar356 /// The milestone it is in, if any.
357 #[serde(default)]
358 pub milestone: Option<MilestoneRef>,
Docs: the inbox, what lands in it and why, its tabs and actions359}
360
361#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
362#[serde(rename_all = "lowercase")]
363pub enum PullStatus {
364 /// Still being worked on.
365 Draft,
366 /// Ready for review.
367 Open,
368 Merged,
369 /// Closed without merging.
370 Closed,
371}
372
373impl PullStatus {
374 pub fn as_str(self) -> &'static str {
375 match self {
376 PullStatus::Draft => "draft",
377 PullStatus::Open => "open",
378 PullStatus::Merged => "merged",
379 PullStatus::Closed => "closed",
380 }
381 }
382
383 /// Whether the pull request can still be changed or merged.
384 pub fn is_active(self) -> bool {
385 matches!(self, PullStatus::Draft | PullStatus::Open)
386 }
387}
388
389/// Where the agent runs: on g1t's sandboxes, or in someone's own session.
390#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(rename_all = "lowercase")]
392pub enum Runtime {
393 Hosted,
394 External,
395}
396
397/// A proposed change. It is made either in a fork created for it, which is
398/// how agents work, or on a branch pushed to the repository itself.
399#[derive(Clone, Debug, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct Pull {
402 pub id: String,
403 pub repo_id: String,
404 /// Shown as `#12`.
405 pub number: u32,
406 /// The number of the issue this is for, if any.
407 pub issue: Option<u32>,
408 pub title: String,
409 /// Markdown: what changed and why. Set when marked ready.
410 pub body: Option<String>,
411 /// A label for the agent doing the work, e.g. `claude-code`.
412 pub agent: String,
413 pub runtime: Runtime,
414 pub status: PullStatus,
415 /// The fork holding the change, unless it is on a branch.
416 pub fork: Option<RepoPath>,
417 /// The fork's repository id.
418 pub fork_repo_id: Option<String>,
419 /// The branch of the repository holding the change, unless it is in a
420 /// fork.
421 pub branch: Option<String>,
422 pub head_commit: Option<String>,
423 /// For a merged pull request, what the branch pointed to before the
424 /// merge. Comparing against it shows what the pull request changed.
425 pub merge_base: Option<String>,
426 /// Username of whoever merged it.
427 pub merged_by: Option<String>,
428 /// RFC 3339.
429 pub merged_at: Option<String>,
430 /// Set on a pull request closed because another one for the same issue
431 /// was merged: that one's number.
432 pub superseded_by: Option<u32>,
433 /// `failed` when the merge queue took it out because its combined
434 /// state failed, until its head moves. Its checks are the statuses
435 /// workflows report on its head: see `PullDetail::statuses` and
436 /// `PullDetail::required_checks`.
437 pub check_status: Option<CheckStatus>,
438 /// The files it changes, as of its latest push.
439 #[serde(default)]
440 pub files: Vec<ChangedFile>,
441 /// Usernames of the people it is assigned to.
442 #[serde(default)]
443 pub assignees: Vec<String>,
444 /// Those whose review was asked for: usernames, and `g1t` when a
445 /// g1t agent was asked.
446 #[serde(default)]
447 pub reviewers: Vec<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar448 /// Teams whose review was asked for, as `workspace/slug`. A team stays
449 /// here after review assignment picks people from it, who are listed
450 /// in `reviewers`.
451 #[serde(default)]
452 pub team_reviewers: Vec<String>,
453 /// The labels it carries, by name.
454 #[serde(default)]
455 pub labels: Vec<String>,
456 /// The milestone it is in, if any.
457 #[serde(default)]
458 pub milestone: Option<MilestoneRef>,
459 /// The branch it merges into. Stored as null for the repository's
460 /// default branch, so that it follows a change of default; lists and
461 /// `get_pull` fill in the name. See [`base_branch`](Pull::base_branch).
462 #[serde(default)]
463 pub base: Option<String>,
Docs: the inbox, what lands in it and why, its tabs and actions464 /// Who opened it: a person, or g1t (`kind` `agent`, username `g1t`)
465 /// for a change g1t made.
466 pub author: User,
467 /// For a change g1t made: the person who asked for it, by assigning
468 /// an issue or handing g1t the work. They answer for it as its author
469 /// would. See [`owner`](Pull::owner).
470 #[serde(default)]
471 pub requested_by: Option<User>,
472 /// RFC 3339.
473 pub created_at: String,
474 /// RFC 3339.
475 pub updated_at: String,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge476 /// Who last moved its head (a user id), and when, RFC 3339: for rules
477 /// about the most recent push. Absent until a push after rulesets
478 /// arrived.
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub head_pushed_by: Option<String>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
482 pub head_pushed_at: Option<String>,
Docs: the inbox, what lands in it and why, its tabs and actions483}
484
485/// One file a pull request changes, and by how much.
486#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
487pub struct ChangedFile {
488 pub path: String,
489 pub additions: u32,
490 pub deletions: u32,
491}
492
493/// Another pull request in progress that changes some of the same files.
494/// Two for the same issue are alternatives; two for different issues are
495/// heading for a conflict.
496#[derive(Clone, Debug, Serialize, Deserialize)]
497pub struct Overlap {
498 pub number: u32,
499 pub title: String,
500 /// The number of the issue the other pull request is for.
501 pub issue: Option<u32>,
502 /// The files both change.
503 pub paths: Vec<String>,
504}
505
506impl Issue {
507 /// Who the issue is theirs to manage: the person g1t's agent filed it
508 /// for, or its author. They may edit, close and reopen it without the
509 /// Triage role.
510 pub fn owner(&self) -> &User {
511 self.requested_by.as_ref().unwrap_or(&self.author)
512 }
513}
514
515impl Pull {
516 /// Who the pull request is theirs to answer for: whoever asked g1t to
517 /// make it, or its author. Every rule that once read "its author" reads
518 /// this: they may update, close and steer it, are never asked to review
519 /// it and cannot approve it, see it as theirs, and a sandbox at work on
520 /// it acts as them.
521 pub fn owner(&self) -> &User {
522 self.requested_by.as_ref().unwrap_or(&self.author)
523 }
524
525 /// Whether `id` is its [`owner`](Pull::owner)'s.
526 pub fn is_owned_by(&self, id: &str) -> bool {
527 self.owner().id == id
528 }
529
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar530 /// The branch it merges into: its own base, or the repository's
531 /// default branch when it has none.
532 pub fn base_branch<'a>(&'a self, default_branch: &'a str) -> &'a str {
533 self.base.as_deref().filter(|base| !base.is_empty()).unwrap_or(default_branch)
534 }
535
536 /// Whether it merges into the repository's default branch.
537 pub fn targets_default(&self, default_branch: &str) -> bool {
538 self.base_branch(default_branch) == default_branch
539 }
540
Docs: the inbox, what lands in it and why, its tabs and actions541 /// What to ask the repos service to see what this pull request changes.
542 ///
543 /// A fork is compared as a whole. A branch is compared by name while
544 /// the pull request is open, and by the commit it was merged or closed
545 /// at afterwards, so later pushes to the branch do not change the record.
546 pub fn comparison(&self, viewer: &Viewer) -> CompareArgs {
547 let settled = matches!(self.status, PullStatus::Merged | PullStatus::Closed);
548 let (repo_id, head) = match &self.fork_repo_id {
549 Some(fork) => (fork.clone(), None),
550 None => (
551 self.repo_id.clone(),
552 self.head_commit
553 .clone()
554 .filter(|_| settled)
555 .or_else(|| self.branch.clone()),
556 ),
557 };
558 CompareArgs {
559 repo_id,
560 viewer: viewer.clone(),
561 base: self.merge_base.clone(),
562 head,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar563 base_branch: self.base.clone().filter(|base| !base.is_empty()),
Merge branch 'worktree-agent-a432c4ed9deb834b1'564 paths: None,
Docs: the inbox, what lands in it and why, its tabs and actions565 }
566 }
567}
568
569/// g1t's agent, as the author of what it opens: stored by
570/// [`AGENT_ID`](crate::identity::AGENT_ID), shown as `g1t`.
571pub fn g1t_author() -> User {
572 User {
573 id: crate::identity::AGENT_ID.to_owned(),
574 username: crate::identity::AGENT_NAME.to_owned(),
575 kind: crate::PrincipalKind::Agent,
576 ..User::default()
577 }
578}
579
580/// Who is recorded as opening an issue or a pull request that `actor`
581/// opens, and who asked for it: `(author, requested_by)`.
582///
583/// - g1t's agent, acting for someone through its token (an issue it files
584/// while at work): g1t, requested by that person.
585/// - A person having g1t make the change (`by_g1t`: a hosted g1t agent in
586/// a fork of its own): g1t, requested by them.
587/// - Anyone else, and g1t's own work that nobody asked for: the actor, and
588/// nobody asking.
589pub fn authorship(actor: &User, by_g1t: bool) -> (User, Option<User>) {
590 let person = |id: &str, username: &str| User {
591 id: id.to_owned(),
592 username: username.to_owned(),
593 kind: crate::PrincipalKind::User,
594 ..User::default()
595 };
596 if actor.kind == crate::PrincipalKind::Agent {
597 let asked = actor
598 .acting
599 .as_ref()
600 .map(|acting| &acting.on_behalf_of)
601 .filter(|on_behalf_of| !crate::system::is_system_id(&on_behalf_of.id))
602 .map(|on_behalf_of| person(&on_behalf_of.id, &on_behalf_of.username));
603 return (g1t_author(), asked);
604 }
605 if by_g1t && actor.kind != crate::PrincipalKind::System {
606 return (g1t_author(), Some(person(&actor.id, &actor.username)));
607 }
608 let author = User {
609 id: actor.id.clone(),
610 username: actor.username.clone(),
611 kind: actor.kind,
612 ..User::default()
613 };
614 (author, None)
615}
616
617#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
618#[serde(rename_all = "lowercase")]
619pub enum CheckStatus {
620 /// Waiting for a sandbox.
621 Queued,
622 Running,
623 Passed,
624 Failed,
625 /// The checks could not be run at all.
626 Errored,
627}
628
629impl CheckStatus {
630 pub fn as_str(self) -> &'static str {
631 match self {
632 CheckStatus::Queued => "queued",
633 CheckStatus::Running => "running",
634 CheckStatus::Passed => "passed",
635 CheckStatus::Failed => "failed",
636 CheckStatus::Errored => "errored",
637 }
638 }
639}
640
641/// How one command went. Recorded by earlier runs of commands written on
642/// issues, which g1t no longer runs; kept so their history still reads.
643#[derive(Clone, Debug, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct CheckResult {
646 pub command: String,
647 pub passed: bool,
648 /// Absent when the command was stopped for taking too long.
649 #[serde(default)]
650 pub exit_code: Option<i32>,
651 /// What the command printed, standard output and error together. The
652 /// end of it, when there was a lot.
653 #[serde(default)]
654 pub output: String,
655 #[serde(default)]
656 pub duration_ms: u64,
657}
658
659/// A record against a pull request's head: the merge queue taking it out,
660/// with why, or an earlier run of commands written on its issue.
661#[derive(Clone, Debug, Serialize, Deserialize)]
662#[serde(rename_all = "camelCase")]
663pub struct CheckRun {
664 pub id: String,
665 /// The commit that was checked.
666 pub head_commit: String,
667 pub status: CheckStatus,
668 pub results: Vec<CheckResult>,
669 /// Why the checks could not be run, when `status` is `errored`.
670 pub error: Option<String>,
671 /// RFC 3339.
672 pub created_at: String,
673 /// RFC 3339.
674 pub finished_at: Option<String>,
675}
676
677/// A reviewer's decision on a pull request.
678#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
679#[serde(rename_all = "snake_case")]
680pub enum Verdict {
681 Approve,
682 RequestChanges,
683}
684
685impl Verdict {
686 pub fn as_str(self) -> &'static str {
687 match self {
688 Verdict::Approve => "approve",
689 Verdict::RequestChanges => "request_changes",
690 }
691 }
692}
693
694/// What an entry in a conversation is.
695#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
696#[serde(rename_all = "lowercase")]
697pub enum CommentKind {
698 /// Something a person or an agent wrote.
699 #[default]
700 Comment,
701 /// Something that happened: an assignment, a review asked for, a close.
702 Event,
703}
704
705/// A comment on an issue or a pull request. On a pull request it can sit
706/// on one line of the change, and it can carry a reviewer's verdict.
707#[derive(Clone, Debug, Serialize, Deserialize)]
708#[serde(rename_all = "camelCase")]
709pub struct Comment {
710 pub id: String,
711 /// Something a person wrote, or something that happened.
712 #[serde(default)]
713 pub kind: CommentKind,
714 pub author: User,
715 /// Markdown. For an event, what its author did, as the rest of a
716 /// sentence that starts with their name: "assigned ana".
717 pub body: String,
718 /// The file commented on, for a comment on a line.
719 pub path: Option<String>,
720 /// The line of that file, as numbered after the change.
721 pub line: Option<u32>,
722 pub verdict: Option<Verdict>,
723 /// RFC 3339.
724 pub created_at: String,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts725 /// When its text was last edited, RFC 3339; absent if it never was.
726 #[serde(default, skip_serializing_if = "Option::is_none")]
727 pub edited_at: Option<String>,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar728 /// Set when one of the workspace's agents wrote it, as itself
729 /// (`author` is then the agent: its id, its handle as `username`,
730 /// kind `agent`). Absent for people and for g1t.
731 #[serde(default, skip_serializing_if = "Option::is_none")]
732 pub agent: Option<AgentRef>,
733 /// Who the agent acted for: the person whose access it had, who
734 /// answers for it as its author would. Set with `agent`.
735 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub acting_for: Option<User>,
737 /// An agent's review: shown with its verdict (or none, for a review
738 /// that only comments) but advisory. It never counts toward required
739 /// approvals or code owners, and never blocks a merge.
740 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
741 pub advisory: bool,
742}
743
744impl Comment {
745 /// The person who answers for it: whoever an agent acted for, or its author.
746 pub fn answerable(&self) -> &User {
747 self.acting_for.as_ref().unwrap_or(&self.author)
748 }
749}
750
751/// One of a workspace's own agents, as a comment or review it wrote shows
752/// it: Margo (@margo), with the pixel face drawn from `avatar_seed`. Its
753/// page is `/<workspace>/-/agents/<handle>`. Read in camelCase, like the
754/// comment it sits on; the snake_case spelling is accepted too.
755#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
756#[serde(rename_all = "camelCase")]
757pub struct AgentRef {
758 pub id: String,
759 pub handle: String,
760 #[serde(alias = "display_name")]
761 pub display_name: String,
762 #[serde(alias = "avatar_seed")]
763 pub avatar_seed: String,
764}
765
766/// An agent's review: a comment, an approval or a request for changes,
767/// all of them advisory.
768#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
769#[serde(rename_all = "snake_case")]
770pub enum AgentVerdict {
771 Comment,
772 Approve,
773 RequestChanges,
774}
775
776impl AgentVerdict {
777 pub fn as_str(self) -> &'static str {
778 match self {
779 AgentVerdict::Comment => "comment",
780 AgentVerdict::Approve => "approve",
781 AgentVerdict::RequestChanges => "request_changes",
782 }
783 }
784
785 pub fn parse(text: &str) -> Option<Self> {
786 match text {
787 "comment" => Some(AgentVerdict::Comment),
788 "approve" => Some(AgentVerdict::Approve),
789 "request_changes" => Some(AgentVerdict::RequestChanges),
790 _ => None,
791 }
792 }
793
794 /// As a person's verdict reads, for showing it; none for a comment.
795 pub fn verdict(self) -> Option<Verdict> {
796 match self {
797 AgentVerdict::Comment => None,
798 AgentVerdict::Approve => Some(Verdict::Approve),
799 AgentVerdict::RequestChanges => Some(Verdict::RequestChanges),
800 }
801 }
802}
803
Docs: the inbox, what lands in it and why, its tabs and actions804#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(rename_all = "snake_case")]
806pub enum SessionEntryKind {
807 Prompt,
808 Message,
809 ToolCall,
810 ToolResult,
811 Note,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.812 /// A skill the session used: its name only.
813 Skill,
Docs: the inbox, what lands in it and why, its tabs and actions814}
815
816/// One step of an agent's session: the "why" behind a pull request's commits.
817#[derive(Clone, Debug, Serialize, Deserialize)]
818pub struct SessionEntry {
819 pub seq: u32,
820 pub kind: SessionEntryKind,
821 pub text: String,
822 /// For tool calls and results.
823 pub tool: Option<String>,
824 /// The fork's head commit when this entry was recorded, if known.
825 pub commit: Option<String>,
826 /// RFC 3339.
827 pub at: String,
828}
829
830#[derive(Clone, Debug, Serialize, Deserialize)]
831pub struct NewSessionEntry {
832 pub kind: SessionEntryKind,
833 pub text: String,
834 #[serde(default)]
835 pub tool: Option<String>,
836 #[serde(default)]
837 pub commit: Option<String>,
838}
839
840#[derive(Clone, Debug, Serialize, Deserialize)]
841pub struct IssueDetail {
842 pub issue: Issue,
843 /// Every pull request made against it, oldest first.
844 pub pulls: Vec<Pull>,
845 pub comments: Vec<Comment>,
846}
847
848#[derive(Clone, Debug, Serialize, Deserialize)]
849pub struct PullDetail {
850 pub pull: Pull,
851 /// The issue it is for, if any.
852 pub issue: Option<Issue>,
853 pub comments: Vec<Comment>,
854 /// The latest record against its head: the merge queue taking it out,
855 /// or (from before checks were workflows) a run of its issue's commands.
856 pub checks: Option<CheckRun>,
857 /// Other pull requests in progress that change the same files.
858 #[serde(default)]
859 pub overlaps: Vec<Overlap>,
860 /// Whether the branch it would merge into has moved on without it, so
861 /// that it has to catch up before it can merge.
862 #[serde(default)]
863 pub behind: bool,
864 /// Where it stands on its way to being merged, for a pull request g1t
865 /// is seeing through. Absent on anyone else's.
866 #[serde(default)]
867 pub lifecycle: Option<Lifecycle>,
868 /// A merge was asked for while it was behind: g1t is bringing it up to
869 /// date and will then land it.
870 #[serde(default)]
871 pub landing: bool,
872 /// Why g1t stopped working on it, if it did: a catch-up that could not
873 /// be completed, for example.
874 #[serde(default)]
875 pub stalled: Option<String>,
876 /// What workflow runs said about its head commit, one per workflow.
877 #[serde(default)]
878 pub statuses: Vec<CommitStatus>,
879 /// Whether it merges cleanly into the branch it targets, worked out
880 /// ahead of time whenever either side moves.
881 #[serde(default)]
882 pub mergeable: Mergeable,
883 /// When `mergeable` is `conflicting`: the files that conflict.
884 #[serde(default)]
885 pub conflicts: Vec<String>,
886 /// Earlier records like `checks`, newest first, without their output.
887 #[serde(default)]
888 pub earlier_checks: Vec<CheckRun>,
889 /// The checks the default branch's protection requires, each as it
890 /// stands on the head commit. Empty when none are required.
891 #[serde(default, alias = "requiredChecks")]
892 pub required_checks: Vec<RequiredCheck>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge893 /// The rules of the branch it merges into that it does not meet yet,
894 /// for whoever is looking. Absent while it is not open.
895 #[serde(default, skip_serializing_if = "Option::is_none")]
896 pub rules: Option<crate::rules::MergeRules>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar897 /// Who owns the files it changes, from the CODEOWNERS file of the
898 /// branch it merges into, and whose approval is still needed. Absent
899 /// when that branch has no CODEOWNERS file.
900 #[serde(default, alias = "codeOwners")]
901 pub code_owners: Option<crate::codeowners::PullCodeOwners>,
Docs: the inbox, what lands in it and why, its tabs and actions902}
903
904/// Where a required check stands on a commit.
905#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
906#[serde(rename_all = "lowercase")]
907pub enum RequiredState {
908 Success,
909 Failure,
910 /// Reported and still running.
911 Pending,
912 /// Nothing has reported it on this commit yet.
913 Expected,
914}
915
916impl RequiredState {
917 pub fn as_str(self) -> &'static str {
918 match self {
919 RequiredState::Success => "success",
920 RequiredState::Failure => "failure",
921 RequiredState::Pending => "pending",
922 RequiredState::Expected => "expected",
923 }
924 }
925}
926
927/// One check a branch's protection requires, as it stands on a commit.
928#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
929#[serde(rename_all = "camelCase")]
930pub struct RequiredCheck {
931 /// The check's name, such as `CI` or `g1t / deploy`.
932 pub name: String,
933 pub state: RequiredState,
934 /// What the status that decided it says, if one did.
935 #[serde(default)]
936 pub description: Option<String>,
937 /// Where to see more: the workflow run, for one a workflow reported.
938 #[serde(default)]
939 pub target_url: Option<String>,
940}
941
942/// The events a workflow status's context can end in: `CI / pull_request`
943/// is the `CI` check, reported by a run for a `pull_request` event.
944const STATUS_EVENTS: &[&str] = &[
945 "push",
946 "pull_request",
947 "pull_request_target",
948 "pull_request_review",
949 "merge_group",
950 "workflow_dispatch",
951 "workflow_run",
952 "workflow_call",
953 "schedule",
954 "release",
955 "issues",
956 "issue_comment",
957 "repository_dispatch",
958];
959
960/// A status context's check name and the event it was reported for:
961/// `CI / pull_request` is `("CI", Some("pull_request"))`. A context that
962/// does not end in an event, such as `g1t / deploy`, is its own name.
963pub fn check_name(context: &str) -> (&str, Option<&str>) {
964 match context.rsplit_once(" / ") {
965 Some((name, event)) if STATUS_EVENTS.contains(&event) && !name.trim().is_empty() => (name, Some(event)),
966 _ => (context, None),
967 }
968}
969
970/// Where each required check stands among a commit's statuses. A check is
971/// met by any status of that name, for any event: one that failed fails
972/// it, one still running holds it, and with neither, one that passed
973/// passes it. Names compare without regard to case.
974pub fn required_checks(required: &[String], statuses: &[CommitStatus]) -> Vec<RequiredCheck> {
975 required
976 .iter()
977 .map(|name| {
978 let matching: Vec<&CommitStatus> = statuses
979 .iter()
980 .filter(|status| check_name(&status.context).0.eq_ignore_ascii_case(name.trim()))
981 .collect();
982 let failed = matching.iter().find(|s| s.state == "failure" || s.state == "error");
983 let pending = matching.iter().find(|s| s.state == "pending");
984 let passed = matching.iter().find(|s| s.state == "success");
985 let (state, decided) = match (failed, pending, passed) {
986 (Some(status), _, _) => (RequiredState::Failure, Some(*status)),
987 (None, Some(status), _) => (RequiredState::Pending, Some(*status)),
988 (None, None, Some(status)) => (RequiredState::Success, Some(*status)),
989 _ => (RequiredState::Expected, None),
990 };
991 RequiredCheck {
992 name: name.trim().to_owned(),
993 state,
994 description: decided.and_then(|status| status.description.clone()),
995 target_url: decided.and_then(|status| status.target_url.clone()),
996 }
997 })
998 .collect()
999}
1000
1001/// A repository's required check names, tidied: trimmed, without blanks
1002/// or repeats (ignoring case), at most [`MAX_REQUIRED_CHECKS`].
1003pub fn tidy_required(names: &[String]) -> Vec<String> {
1004 let mut out: Vec<String> = Vec::new();
1005 for name in names {
1006 let name: String = name.trim().chars().take(MAX_CHECK_NAME_CHARS).collect();
1007 if !name.is_empty() && !out.iter().any(|kept| kept.eq_ignore_ascii_case(&name)) {
1008 out.push(name);
1009 }
1010 }
1011 out.truncate(MAX_REQUIRED_CHECKS);
1012 out
1013}
1014
1015/// The most checks a branch can require, and the longest name of one.
1016pub const MAX_REQUIRED_CHECKS: usize = 20;
1017pub const MAX_CHECK_NAME_CHARS: usize = 100;
1018
1019/// A check name seen on the repository's commits recently, for choosing
1020/// required checks: what reported it, and for which events.
1021#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1022#[serde(rename_all = "camelCase")]
1023pub struct SeenCheck {
1024 pub name: String,
1025 /// The events it was reported for, such as `pull_request`; empty for a
1026 /// status that names none, such as a deployment's.
1027 pub events: Vec<String>,
1028 /// RFC 3339. The latest report.
1029 pub last_seen: String,
1030}
1031
1032/// `seen_checks`: the check names reported on a repository's commits in
1033/// the last 30 days, most recent first. Returns `Outcome<Vec<SeenCheck>>`.
1034#[derive(Debug, Serialize, Deserialize)]
1035pub struct SeenChecksArgs {
1036 pub repo: RepoPath,
1037 pub viewer: Viewer,
1038}
1039
1040/// The heading an issue's plain-words description of done goes under.
1041pub const DEFINITION_OF_DONE: &str = "## Definition of done";
1042
1043/// An issue's body with `items` added as a bulleted "Definition of done"
1044/// section, for the agent and reviewers to read. Nothing is added when
1045/// `items` is empty, or when the body already has the section, so folding
1046/// the same items twice changes nothing.
1047pub fn with_definition_of_done(body: &str, items: &[String]) -> String {
1048 let items: Vec<&str> = items.iter().map(|item| item.trim()).filter(|item| !item.is_empty()).collect();
1049 let body = body.trim();
1050 if items.is_empty() || body.contains(DEFINITION_OF_DONE) {
1051 return body.to_owned();
1052 }
1053 let list = items.iter().map(|item| format!("- {item}")).collect::<Vec<_>>().join("\n");
1054 if body.is_empty() {
1055 format!("{DEFINITION_OF_DONE}\n\n{list}")
1056 } else {
1057 format!("{body}\n\n{DEFINITION_OF_DONE}\n\n{list}")
1058 }
1059}
1060
1061/// Commands, as items of a definition of done: "`npm test` passes."
1062pub fn commands_pass(commands: &[String]) -> Vec<String> {
1063 commands
1064 .iter()
1065 .map(|command| command.trim())
1066 .filter(|command| !command.is_empty())
1067 .map(|command| format!("`{}` passes.", command.replace('`', "'")))
1068 .collect()
1069}
1070
1071/// Whether a pull request's change merges cleanly into the branch it
1072/// targets.
1073#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1074#[serde(rename_all = "lowercase")]
1075pub enum Mergeable {
1076 /// It merges without conflicts.
1077 Clean,
1078 /// Some files conflict: see `PullDetail::conflicts`.
1079 Conflicting,
1080 /// Not known: never worked out, or it could not be.
1081 #[default]
1082 Unknown,
1083 /// Being worked out now.
1084 Checking,
1085}
1086
1087impl Mergeable {
1088 pub fn as_str(self) -> &'static str {
1089 match self {
1090 Mergeable::Clean => "clean",
1091 Mergeable::Conflicting => "conflicting",
1092 Mergeable::Unknown => "unknown",
1093 Mergeable::Checking => "checking",
1094 }
1095 }
1096
1097 pub fn parse(value: Option<&str>) -> Mergeable {
1098 match value {
1099 Some("clean") => Mergeable::Clean,
1100 Some("conflicting") => Mergeable::Conflicting,
1101 Some("checking") => Mergeable::Checking,
1102 _ => Mergeable::Unknown,
1103 }
1104 }
1105}
1106
1107/// `start_mergecheck`: claims the probe of whether a pull request merges
1108/// cleanly, which the work service asked for with a `pull.mergecheck`
1109/// event. Called by the runner service, which starts the sandbox. Refused
1110/// when it is no longer wanted, or when the repository already has as many
1111/// probes running as it may. Returns `Outcome<MergecheckJob>`.
1112#[derive(Debug, Serialize, Deserialize)]
1113#[serde(rename_all = "camelCase")]
1114pub struct StartMergecheckArgs {
1115 pub pull_id: String,
1116}
1117
1118/// What a sandbox needs to find out whether a pull request merges cleanly.
1119#[derive(Debug, Serialize, Deserialize)]
1120#[serde(rename_all = "camelCase")]
1121pub struct MergecheckJob {
1122 pub pull_id: String,
1123 /// Lets the sandbox, and nothing else, report this probe.
1124 pub token: String,
1125 pub repo: RepoPath,
1126 pub number: u32,
1127 pub default_branch: String,
1128 /// The default branch's commit to merge into.
1129 pub base: String,
1130 /// The repository holding the change: its fork, or the repository.
1131 pub source: RepoPath,
1132 /// The branch of `source` holding it.
1133 pub branch: String,
1134 /// The change's commit.
1135 pub head: String,
1136 /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
1137 /// it, or its author), and so can read its source.
1138 pub author: User,
1139}
1140
1141/// `report_mergecheck`: what a sandbox found. Returns `Outcome<Mergeable>`.
1142#[derive(Debug, Serialize, Deserialize)]
1143#[serde(rename_all = "camelCase")]
1144pub struct ReportMergecheckArgs {
1145 pub pull_id: String,
1146 pub token: String,
1147 /// The files that conflict; empty when it merges cleanly.
1148 #[serde(default)]
1149 pub conflicts: Vec<String>,
1150 /// Why it could not be found out.
1151 #[serde(default)]
1152 pub error: Option<String>,
1153}
1154
1155/// What a workflow run (or another tool) says about a commit.
Merge checks: statuses and check runs on every commit1156#[derive(Clone, Debug, Default, Serialize, Deserialize)]
Docs: the inbox, what lands in it and why, its tabs and actions1157#[serde(rename_all = "camelCase")]
1158pub struct CommitStatus {
1159 /// What reported it, such as `CI / push`.
1160 pub context: String,
1161 /// `pending`, `success`, `failure` or `error`.
1162 pub state: String,
1163 pub description: Option<String>,
1164 /// Where to see more, such as the run's page.
1165 pub target_url: Option<String>,
1166 pub updated_at: String,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1167 /// The integration that reported it (`actions`, `deployments`,
1168 /// `security`, `g1t`), when recorded. A required check can insist on
1169 /// one (`rules::RequiredCheck::integration`).
1170 #[serde(default, skip_serializing_if = "Option::is_none")]
1171 pub source: Option<String>,
Merge checks: statuses and check runs on every commit1172 /// Set on the status a check run stands as (see `checks`): the check
1173 /// run's id. Such a status is listed as its check run, not again.
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub check_run_id: Option<String>,
Docs: the inbox, what lands in it and why, its tabs and actions1176}
1177
1178/// `set_commit_status`: for services only. Returns `Outcome<bool>`.
1179#[derive(Debug, Serialize, Deserialize)]
1180#[serde(rename_all = "camelCase")]
1181pub struct SetCommitStatusArgs {
1182 pub repo_id: String,
1183 pub sha: String,
1184 pub context: String,
1185 pub state: String,
1186 #[serde(default)]
1187 pub description: Option<String>,
1188 #[serde(default)]
1189 pub target_url: Option<String>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1190 /// The integration reporting it: `actions`, `deployments`, `security`
1191 /// or `g1t`.
1192 #[serde(default)]
1193 pub source: Option<String>,
Docs: the inbox, what lands in it and why, its tabs and actions1194}
1195
1196/// A step on the way from an assigned issue to a pull request that is ready
1197/// to merge. g1t takes each one without being asked.
1198#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1199#[serde(rename_all = "snake_case")]
1200pub enum Stage {
1201 /// The agent is making the change.
1202 Working,
1203 /// Waiting for the checks workflows report on its head.
1204 Checking,
1205 /// g1t is reviewing it.
1206 Reviewing,
1207 /// The agent is addressing failed checks or a review.
1208 Revising,
1209 /// The agent is merging in the branch it would land on, which moved.
1210 CatchingUp,
1211 /// Woken to answer a question another agent asked it, or a handoff.
1212 Answering,
1213 /// In the repository's merge queue, being tested with what is ahead of
1214 /// it before it lands.
1215 Queued,
1216 /// Required checks passed, reviewed and approved, up to date. A
1217 /// person merges.
1218 Ready,
1219 /// g1t has stopped and a person has to decide what happens next.
1220 NeedsYou,
1221}
1222
1223/// Where a pull request made by a g1t agent stands. See [`Stage`].
1224#[derive(Clone, Debug, Serialize, Deserialize)]
1225pub struct Lifecycle {
1226 pub stage: Stage,
1227 /// One sentence saying what is happening, or why it stopped.
1228 pub detail: String,
1229 /// How many times the agent has been sent back to revise it.
1230 pub revisions: u32,
1231}
1232
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.1233/// `advance`: merges a pull request g1t is seeing through once it is ready,
1234/// where the repository's settings say ready pull requests land by
1235/// themselves. Called by the runner service. Returns `Advance`.
Docs: the inbox, what lands in it and why, its tabs and actions1236#[derive(Debug, Serialize, Deserialize)]
1237#[serde(rename_all = "camelCase")]
1238pub struct AdvanceArgs {
1239 pub pull_id: String,
1240}
1241
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.1242/// What the runner is to do next for a pull request: always nothing now,
1243/// since no step of a pull request is carried out in a sandbox any more.
Docs: the inbox, what lands in it and why, its tabs and actions1244#[derive(Debug, Serialize, Deserialize)]
1245#[serde(tag = "action", rename_all = "snake_case")]
1246pub enum Advance {
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.1247 /// Nothing to do.
Docs: the inbox, what lands in it and why, its tabs and actions1248 None,
1249}
1250
1251/// How a repository wants its pull requests handled. A repository that has
1252/// changed nothing has the defaults.
1253#[derive(Clone, Debug, Serialize, Deserialize)]
1254#[serde(rename_all = "camelCase", default)]
1255pub struct RepoSettings {
1256 /// Land a g1t agent's pull request without a person once it is ready:
1257 /// required checks passed and approved as the settings below require.
1258 pub auto_merge: bool,
1259 /// The checks that must pass on a pull request's head before it may
1260 /// merge into the default branch, by name: a workflow's name (`CI`), or
1261 /// the context of another status (`g1t / deploy`). The same for a
1262 /// person's pull request and an agent's, and for the merge queue.
1263 pub required_checks: Vec<String>,
1264 /// Refuse to merge a pull request that does not contain the default
1265 /// branch's latest commits, so that what merges is what was checked.
1266 /// When off, merging one that is behind brings it up to date first.
1267 pub require_up_to_date: bool,
1268 /// How many approving reviews a pull request needs before it may
1269 /// merge. A reviewer who has since asked for changes blocks it.
1270 pub required_approvals: u32,
1271 /// Whether a g1t agent's approval counts towards `required_approvals`.
1272 pub count_agent_approvals: bool,
1273 /// Whether someone who may merge can bypass required checks that have
1274 /// not passed, by saying so as they merge.
1275 pub allow_ignoring_checks: bool,
1276 /// Merge through a queue: pull requests are tested together with those
1277 /// ahead of them, and only a combination that passed reaches the default
1278 /// branch.
1279 pub merge_queue: bool,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1280 /// Refuse to merge until the code owners of every file it changes
1281 /// (its CODEOWNERS file) have approved, as many as each section asks.
1282 /// Only people count, and `g1t` only where the file names `@g1t`.
1283 pub require_code_owner_review: bool,
Docs: the inbox, what lands in it and why, its tabs and actions1284 /// Username of the member who last changed the settings, if anyone has.
1285 pub updated_by: Option<String>,
1286 /// RFC 3339.
1287 pub updated_at: Option<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1288}
1289
Docs: the inbox, what lands in it and why, its tabs and actions1290impl Default for RepoSettings {
1291 fn default() -> Self {
1292 RepoSettings {
1293 auto_merge: false,
1294 required_checks: Vec::new(),
1295 require_up_to_date: false,
1296 required_approvals: 0,
1297 count_agent_approvals: true,
1298 allow_ignoring_checks: true,
1299 merge_queue: false,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1300 require_code_owner_review: false,
Docs: the inbox, what lands in it and why, its tabs and actions1301 updated_by: None,
1302 updated_at: None,
1303 }
1304 }
1305}
1306
1307/// `update_settings`: replaces a repository's settings. Members of its
1308/// workspace only. Returns `Outcome<RepoSettings>`. `get_settings` takes
1309/// `ViewArgs` and returns the same.
1310#[derive(Debug, Serialize, Deserialize)]
1311pub struct UpdateSettingsArgs {
1312 pub actor: User,
1313 pub repo: RepoPath,
1314 /// Who changed them and when are filled in by the service.
1315 pub settings: RepoSettings,
1316}
1317
1318/// `stall`: records that a step could not be carried out, so that g1t
1319/// stops and a person is asked. Returns `bool`.
1320#[derive(Debug, Serialize, Deserialize)]
1321#[serde(rename_all = "camelCase")]
1322pub struct StallArgs {
1323 pub pull_id: String,
1324 pub reason: String,
Events: review requests, assignments, stops and deployments are published1325 /// The person who stopped it, by id, when someone did: they are not
1326 /// told it needs them.
1327 #[serde(default)]
1328 pub by: Option<String>,
Docs: the inbox, what lands in it and why, its tabs and actions1329}
1330
1331/// `managed_pulls`: ids of the open pull requests g1t is seeing through,
1332/// in one repository or in all of them. Returns `Vec<String>`.
1333#[derive(Debug, Default, Serialize, Deserialize)]
1334#[serde(rename_all = "camelCase")]
1335pub struct ManagedPullsArgs {
1336 #[serde(default)]
1337 pub repo_id: Option<String>,
1338}
1339
1340/// `open_issue`. Returns `Outcome<Issue>`.
1341#[derive(Debug, Serialize, Deserialize)]
1342pub struct OpenIssueArgs {
1343 pub actor: User,
1344 pub repo: RepoPath,
1345 pub title: String,
1346 #[serde(default)]
1347 pub body: String,
1348 #[serde(default)]
1349 pub labels: Vec<String>,
1350 /// Deprecated: commands, added to the body under "Definition of done".
1351 /// Checks are the workflows the branch's protection requires.
1352 #[serde(default)]
1353 pub checks: Vec<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1354 /// The number of the milestone to put it in. Needs the Triage role.
1355 #[serde(default)]
1356 pub milestone: Option<u32>,
Docs: the inbox, what lands in it and why, its tabs and actions1357}
1358
1359/// `list_issues`, newest first. Returns `Outcome<Vec<Issue>>`.
1360#[derive(Debug, Serialize, Deserialize)]
1361pub struct ListIssuesArgs {
1362 pub repo: RepoPath,
1363 pub viewer: Viewer,
1364 #[serde(default)]
1365 pub state: Option<State>,
1366 /// Only issues carrying this label.
1367 #[serde(default)]
1368 pub label: Option<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1369 /// Only issues in the milestone of this number.
1370 #[serde(default)]
1371 pub milestone: Option<u32>,
Docs: the inbox, what lands in it and why, its tabs and actions1372}
1373
1374/// `list_pulls`, newest first. Returns `Outcome<Vec<Pull>>`.
1375#[derive(Debug, Serialize, Deserialize)]
1376pub struct ListPullsArgs {
1377 pub repo: RepoPath,
1378 pub viewer: Viewer,
1379 #[serde(default)]
1380 pub state: Option<State>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1381 /// Only pull requests carrying this label.
1382 #[serde(default)]
1383 pub label: Option<String>,
1384 /// Only pull requests in the milestone of this number.
1385 #[serde(default)]
1386 pub milestone: Option<u32>,
1387 /// Only pull requests into this branch.
1388 #[serde(default)]
1389 pub base: Option<String>,
Docs: the inbox, what lands in it and why, its tabs and actions1390}
1391
1392/// `pulls_for_repos`: the newest open and the newest closed pull requests
1393/// of many repositories, in one call, for pages that show several projects
1394/// at once. Repositories the viewer cannot read are left out, as are forks
1395/// (ask those with `list_pulls`). Returns `Vec<RepoPulls>`.
1396#[derive(Debug, Serialize, Deserialize)]
1397#[serde(rename_all = "camelCase")]
1398pub struct PullsForReposArgs {
1399 /// At most [`MAX_PULLS_FOR_REPOS`] are looked at.
1400 pub repo_ids: Vec<String>,
1401 pub viewer: Viewer,
1402 /// How many of each, open and closed, per repository (at most 100).
1403 pub limit: u32,
1404}
1405
1406/// The most repositories one `pulls_for_repos` call looks at.
1407pub const MAX_PULLS_FOR_REPOS: usize = 50;
1408
1409/// One repository's pull requests from `pulls_for_repos`, newest first.
1410#[derive(Debug, Serialize, Deserialize)]
1411#[serde(rename_all = "camelCase")]
1412pub struct RepoPulls {
1413 pub repo_id: String,
1414 /// Draft and open.
1415 pub open: Vec<Pull>,
1416 /// Merged and closed.
1417 pub closed: Vec<Pull>,
1418}
1419
1420/// `get_issue` (`Outcome<IssueDetail>`), `get_pull` (`Outcome<PullDetail>`),
1421/// `read_session` (`Outcome<Vec<SessionEntry>>`), `list_labels`
1422/// (`Outcome<Vec<String>>`) and `counts` (`Outcome<Counts>`). The last two
1423/// ignore `number`.
1424#[derive(Debug, Serialize, Deserialize)]
1425#[serde(rename_all = "camelCase")]
1426pub struct ViewArgs {
1427 pub repo: RepoPath,
1428 #[serde(default)]
1429 pub number: u32,
1430 pub viewer: Viewer,
1431 /// For `read_session`: only entries after this sequence number.
1432 #[serde(default)]
1433 pub after_seq: u32,
1434}
1435
1436/// How many issues and pull requests are open on a repository.
1437#[derive(Debug, Serialize, Deserialize)]
1438pub struct Counts {
1439 pub issues: u32,
1440 pub pulls: u32,
1441}
1442
1443/// `update_issue`: changes whichever fields are given. The author or a
1444/// member of the workspace may. Returns `Outcome<Issue>`.
1445#[derive(Debug, Serialize, Deserialize)]
1446pub struct UpdateIssueArgs {
1447 pub actor: User,
1448 pub repo: RepoPath,
1449 pub number: u32,
1450 #[serde(default)]
1451 pub title: Option<String>,
1452 #[serde(default)]
1453 pub body: Option<String>,
1454 #[serde(default)]
1455 pub labels: Option<Vec<String>>,
1456 /// Usernames of the people it is assigned to; replaces the whole set.
1457 /// Assigning it to g1t is the runner's `run`, not this.
1458 #[serde(default)]
1459 pub assignees: Option<Vec<String>>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1460 /// The number of the milestone to put it in; 0 takes it out of its
1461 /// milestone. Needs the Triage role.
1462 #[serde(default)]
1463 pub milestone: Option<u32>,
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'1464 /// The numbers of the issues of the same repository that have to land
1465 /// first; replaces the whole set. Refused when one is not an issue, is
1466 /// the issue itself, or would make a cycle.
1467 #[serde(default)]
1468 pub blocked_by: Option<Vec<u32>>,
Docs: the inbox, what lands in it and why, its tabs and actions1469}
1470
1471/// `close_issue` and `reopen_issue`. Each returns `Outcome<Issue>`.
1472#[derive(Debug, Serialize, Deserialize)]
1473pub struct IssueActionArgs {
1474 pub actor: User,
1475 pub repo: RepoPath,
1476 pub number: u32,
1477 /// For `close_issue`; `completed` if left out.
1478 #[serde(default)]
1479 pub reason: Option<IssueReason>,
1480}
1481
1482/// `add_comment`, on an issue or a pull request. On a pull request it may
1483/// name a line of the change, and may carry a verdict; nobody can give a
1484/// verdict on their own pull request. Returns `Outcome<Comment>`.
1485#[derive(Debug, Serialize, Deserialize)]
1486pub struct AddCommentArgs {
1487 pub actor: User,
1488 pub repo: RepoPath,
1489 pub number: u32,
1490 /// May be empty when approving.
1491 #[serde(default)]
1492 pub body: String,
1493 #[serde(default)]
1494 pub path: Option<String>,
1495 #[serde(default)]
1496 pub line: Option<u32>,
1497 #[serde(default)]
1498 pub verdict: Option<Verdict>,
1499}
1500
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1501/// `edit_comment` and `delete_comment`: change the text of a comment on an
1502/// issue or a pull request, or delete it. Its author may, and so may anyone
1503/// with the Maintain role or higher ([`may_change_comment`]). Notes of what
1504/// happened (`CommentKind::Event`) cannot be changed, and a review that
1505/// gave a verdict cannot be deleted, only edited. `edit_comment` returns
1506/// `Outcome<Comment>`; `delete_comment` returns `Outcome<()>`.
1507#[derive(Debug, Serialize, Deserialize)]
1508#[serde(rename_all = "camelCase")]
1509pub struct CommentActionArgs {
1510 pub actor: User,
1511 pub repo: RepoPath,
1512 pub comment_id: String,
1513 /// For `edit_comment`: the new text, in Markdown.
1514 #[serde(default)]
1515 pub body: String,
1516}
1517
1518/// Why `actor_id` may not edit (or, with `deleting`, delete) a comment by
1519/// `author_id`, or none when they may: its author may, and so may someone
1520/// who `maintains` the repository (the Maintain role or higher). A note of
1521/// something that happened is never changed, and a review's verdict stays,
1522/// so a comment that gave one can be edited but not deleted.
1523pub fn may_change_comment(
1524 kind: CommentKind,
1525 has_verdict: bool,
1526 author_id: &str,
1527 actor_id: &str,
1528 maintains: bool,
1529 deleting: bool,
1530) -> Result<(), &'static str> {
1531 if kind == CommentKind::Event {
1532 return Err("That is a record of something that happened, not a comment; it cannot be changed.");
1533 }
1534 if author_id != actor_id && !maintains {
1535 return Err(if deleting {
1536 "Only its author, or someone with the Maintain role or higher, can delete this comment."
1537 } else {
1538 "Only its author, or someone with the Maintain role or higher, can edit this comment."
1539 });
1540 }
1541 if deleting && has_verdict {
1542 return Err("A review cannot be deleted. Edit its text instead.");
1543 }
1544 Ok(())
1545}
1546
Docs: the inbox, what lands in it and why, its tabs and actions1547/// `open_pull`. Without `branch`, forks the repo and returns a draft pull
1548/// request to push to. With it, opens a pull request, ready for review,
1549/// for a branch already pushed to the repo. Returns `Outcome<Pull>`.
1550#[derive(Debug, Serialize, Deserialize)]
1551pub struct OpenPullArgs {
1552 pub actor: User,
1553 pub repo: RepoPath,
1554 /// The issue this is for.
1555 #[serde(default)]
1556 pub issue: Option<u32>,
1557 /// Defaults to the issue's title; required without an issue.
1558 #[serde(default)]
1559 pub title: String,
1560 /// What changed and why. Usually set later, when a draft is marked ready.
1561 #[serde(default)]
1562 pub body: String,
1563 /// A branch of the repository that already holds the change.
1564 #[serde(default)]
1565 pub branch: Option<String>,
1566 #[serde(default)]
1567 pub agent: String,
1568 pub runtime: Runtime,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1569 /// The branch to merge into: the default branch when absent.
1570 #[serde(default)]
1571 pub base: Option<String>,
Open a pull request as a draft: it can't merge, and agents' review routines wait, until it is marked ready1572 /// Opened from a branch as a draft, still being worked on: it can't
1573 /// merge, and agents' review routines wait, until it is marked ready.
1574 #[serde(default)]
1575 pub draft: bool,
Docs: the inbox, what lands in it and why, its tabs and actions1576}
1577
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1578/// `ready_pull`, `close_pull`, `reopen_pull`, `convert_pull_to_draft` and
1579/// `merge_pull`. Each returns `Outcome<Pull>`. `reopen_pull` opens a closed
1580/// pull request again (never a merged one), as the draft it was if it was
1581/// closed as one; `convert_pull_to_draft` turns one that is open back into
1582/// a draft, which leaves the merge queue.
Docs: the inbox, what lands in it and why, its tabs and actions1583///
1584/// Also `catch_up_pull`: brings the pull request up to date with the
1585/// default branch without a sandbox where that is safe, as the repos
1586/// service's `update_pull_branch` does, after checking that `actor` may
1587/// update it: its owner ([`Pull::owner`]) for a fork, any member for a
1588/// branch.
1589/// Returns `Outcome<repos::PullBranchUpdate>`; on `needs_agent` nothing was
1590/// pushed and the runner's `update` is the way on.
1591#[derive(Debug, Serialize, Deserialize)]
1592#[serde(rename_all = "camelCase")]
1593pub struct PullActionArgs {
1594 pub actor: User,
1595 pub repo: RepoPath,
1596 pub number: u32,
1597 /// For `ready_pull`: what changed and why.
1598 #[serde(default)]
1599 pub summary: String,
1600 /// For `merge_pull`: leave the issue open and the other pull requests
1601 /// for it untouched, because this one is only part of the work.
1602 #[serde(default)]
1603 pub keep_issue_open: bool,
1604 /// For `merge_pull`: merge although required checks have not passed,
1605 /// where the repository lets members bypass them.
1606 #[serde(default)]
1607 pub ignore_checks: bool,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1608 /// For `merge_pull`: merge although rules are not met, where a ruleset
1609 /// lists the actor as one who may bypass it. Recorded as a bypass.
1610 #[serde(default)]
1611 pub bypass_rules: bool,
Docs: the inbox, what lands in it and why, its tabs and actions1612}
1613
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1614/// `update_pull`: changes who a pull request is assigned to, whose review
1615/// is asked for, its labels, its milestone and the branch it merges into.
1616/// Each list given replaces the whole set. Whoever opened it, or someone
1617/// with the Triage role, may; changing the base needs the Write role.
1618/// Returns `Outcome<Pull>`.
Docs: the inbox, what lands in it and why, its tabs and actions1619#[derive(Debug, Serialize, Deserialize)]
1620pub struct UpdatePullArgs {
1621 pub actor: User,
1622 pub repo: RepoPath,
1623 pub number: u32,
1624 #[serde(default)]
1625 pub assignees: Option<Vec<String>>,
1626 /// May include `g1t`. Asking for its review does not by itself
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1627 /// start one; the runner's `review` does. A team is named
1628 /// `workspace/team` (or `@workspace/team`): the whole list, people and
1629 /// teams, replaces who is asked.
Docs: the inbox, what lands in it and why, its tabs and actions1630 #[serde(default)]
1631 pub reviewers: Option<Vec<String>>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1632 /// Its labels; replaces the whole set, as `set_labels` does.
1633 #[serde(default)]
1634 pub labels: Option<Vec<String>>,
1635 /// The number of the milestone to put it in; 0 takes it out. Needs the
1636 /// Triage role.
1637 #[serde(default)]
1638 pub milestone: Option<u32>,
1639 /// The branch it merges into: an existing branch other than its own.
1640 /// Needs the Write role.
1641 #[serde(default)]
1642 pub base: Option<String>,
Docs: the inbox, what lands in it and why, its tabs and actions1643}
1644
1645/// `start_checks`: always refused now; a pull request's checks are the
1646/// workflows run on it. Kept so that a runner from before is answered.
1647/// Returns `Outcome<CheckJob>`.
1648#[derive(Debug, Serialize, Deserialize)]
1649#[serde(rename_all = "camelCase")]
1650pub struct StartChecksArgs {
1651 pub pull_id: String,
1652}
1653
1654/// What a sandbox needs to carry out a check run.
1655#[derive(Debug, Serialize, Deserialize)]
1656#[serde(rename_all = "camelCase")]
1657pub struct CheckJob {
1658 pub run_id: String,
1659 /// Lets the sandbox, and nothing else, report this run's results.
1660 pub token: String,
1661 pub commands: Vec<String>,
1662 /// The repository holding the commit: the fork, or the repository itself.
1663 pub source: RepoPath,
1664 pub commit: String,
1665 /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
1666 /// it, or its author), and so can read its source.
1667 pub author: User,
1668 /// Username of whoever wrote the checks: the issue's author.
1669 pub requested_by: String,
1670 pub repo: RepoPath,
1671 pub number: u32,
1672}
1673
1674/// `report_checks`: what a sandbox says about its run. With no results and
1675/// no error it has started. `skip` forgets the run, for one that will not
1676/// be carried out. Returns `Outcome<CheckRun>`.
1677#[derive(Debug, Serialize, Deserialize)]
1678#[serde(rename_all = "camelCase")]
1679pub struct ReportChecksArgs {
1680 pub run_id: String,
1681 pub token: String,
1682 #[serde(default)]
1683 pub results: Vec<CheckResult>,
1684 #[serde(default)]
1685 pub error: Option<String>,
1686 #[serde(default)]
1687 pub skip: bool,
1688}
1689
1690/// A pull request in progress, with where it lives.
1691#[derive(Debug, Serialize, Deserialize)]
1692pub struct ActivePull {
1693 pub pull: Pull,
1694 pub issue: Option<Issue>,
1695 /// Where it stands, for one g1t is seeing through.
1696 #[serde(default)]
1697 pub lifecycle: Option<Lifecycle>,
1698}
1699
1700/// `list_active_pulls`: drafts and open pull requests the viewer started,
1701/// most recently active first. Returns `Vec<ActivePull>`. Also
1702/// `list_assigned_issues`: open issues assigned to the viewer, most
1703/// recently changed first. Returns `Vec<Issue>`.
1704#[derive(Debug, Serialize, Deserialize)]
1705pub struct ViewerArgs {
1706 pub viewer: Viewer,
1707}
1708
1709/// `append_session`. Returns `Outcome<Appended>`.
1710#[derive(Debug, Serialize, Deserialize)]
1711pub struct AppendSessionArgs {
1712 pub actor: User,
1713 pub repo: RepoPath,
1714 pub number: u32,
1715 pub entries: Vec<NewSessionEntry>,
1716}
1717
1718#[derive(Debug, Serialize, Deserialize)]
1719pub struct Appended {
1720 pub count: u32,
1721}
1722
1723/// Lowercases, trims and de-duplicates labels, dropping empty ones.
1724/// Returns `None` if there are too many or one is too long.
1725pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> {
1726 let mut normalized: Vec<String> = Vec::new();
1727 for label in labels {
1728 let label = label
1729 .split_whitespace()
1730 .collect::<Vec<_>>()
1731 .join(" ")
1732 .to_lowercase();
1733 if label.is_empty() || normalized.contains(&label) {
1734 continue;
1735 }
1736 if label.chars().count() > MAX_LABEL_CHARS {
1737 return None;
1738 }
1739 normalized.push(label);
1740 }
1741 (normalized.len() <= MAX_LABELS).then_some(normalized)
1742}
1743
1744#[cfg(test)]
1745mod tests {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1746 use super::*;
Docs: the inbox, what lands in it and why, its tabs and actions1747
1748 fn labels(names: &[&str]) -> Vec<String> {
1749 names.iter().map(|name| (*name).to_owned()).collect()
1750 }
1751
1752 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1753 fn colors_and_due_dates_are_tidied() {
1754 assert_eq!(tidy_color("#A1B2C3").as_deref(), Some("a1b2c3"));
1755 assert_eq!(tidy_color("fc0").as_deref(), Some("ffcc00"));
1756 assert_eq!(tidy_color("red"), None);
1757 assert_eq!(tidy_color("12345"), None);
1758 assert_eq!(label_color_for("bug"), "d73a4a");
1759 assert_eq!(label_color_for("area: web"), label_color_for("area: web"));
1760 assert_eq!(tidy_due_on("2026-10-14").as_deref(), Some("2026-10-14"));
1761 assert_eq!(tidy_due_on("2026-10-14T00:00:00Z").as_deref(), Some("2026-10-14"));
1762 assert_eq!(tidy_due_on("2026-13-01"), None);
1763 assert_eq!(tidy_due_on("soon"), None);
1764 }
1765
1766 #[test]
1767 fn a_milestone_is_as_far_along_as_its_closed_items() {
1768 let mut milestone: Milestone = serde_json::from_value(serde_json::json!({
1769 "number": 1, "title": "Launch", "state": "open", "createdAt": "", "updatedAt": ""
1770 }))
1771 .unwrap();
1772 assert_eq!(milestone.percent(), 0);
1773 milestone.open_items = 3;
1774 milestone.closed_items = 1;
1775 assert_eq!(milestone.percent(), 25);
1776 }
1777
1778 #[test]
Docs: the inbox, what lands in it and why, its tabs and actions1779 fn labels_are_lowercased_trimmed_and_unique() {
1780 assert_eq!(
1781 normalize_labels(&labels(&[" Bug ", "bug", "", "Good First Issue"])),
1782 Some(labels(&["bug", "good first issue"]))
1783 );
1784 }
1785
1786 #[test]
1787 fn too_long_or_too_many_labels_are_refused() {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1788 assert_eq!(normalize_labels(&["x".repeat(51)]), None);
1789 assert!(normalize_labels(&["x".repeat(50)]).is_some());
1790 let many: Vec<String> = (0..21).map(|i| format!("label-{i}")).collect();
Docs: the inbox, what lands in it and why, its tabs and actions1791 assert_eq!(normalize_labels(&many), None);
1792 }
1793}
1794
1795
1796// --- Merge queue ----------------------------------------------------------
1797
1798/// Where a pull request in a merge queue stands.
1799#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1800#[serde(rename_all = "snake_case")]
1801pub enum QueueState {
1802 /// Waiting for its turn to be tested.
1803 Waiting,
1804 /// Its combined state is being built and checked.
1805 Testing,
1806 /// Its combined state passed; it lands once everything ahead has.
1807 Passed,
1808 /// Its combined state failed, or would not merge. It left the queue.
1809 Failed,
1810 /// On the default branch.
1811 Landed,
1812 /// Taken out of the queue by a person, or closed.
1813 Removed,
1814}
1815
1816impl QueueState {
1817 pub fn as_str(self) -> &'static str {
1818 match self {
1819 QueueState::Waiting => "waiting",
1820 QueueState::Testing => "testing",
1821 QueueState::Passed => "passed",
1822 QueueState::Failed => "failed",
1823 QueueState::Landed => "landed",
1824 QueueState::Removed => "removed",
1825 }
1826 }
1827
1828 /// Still in the queue.
1829 pub fn is_active(self) -> bool {
1830 matches!(
1831 self,
1832 QueueState::Waiting | QueueState::Testing | QueueState::Passed
1833 )
1834 }
1835}
1836
1837/// One pull request's place in a merge queue.
1838#[derive(Clone, Debug, Serialize, Deserialize)]
1839#[serde(rename_all = "camelCase")]
1840pub struct QueueEntry {
1841 pub id: String,
1842 pub number: u32,
1843 pub title: String,
1844 /// Who or what made the pull request, e.g. `g1t`.
1845 pub agent: String,
1846 pub state: QueueState,
1847 /// The pull requests merged ahead of it in the state being tested, in
1848 /// queue order. Empty when it was tested on the default branch alone.
1849 pub ahead: Vec<u32>,
1850 /// The default branch's commit the tested state was built on.
1851 pub base_commit: Option<String>,
1852 /// The tested state: the default branch with everything ahead and this.
1853 pub combined_commit: Option<String>,
1854 /// Why it failed: a merge conflict or what could not be run.
1855 pub error: Option<String>,
1856 /// Commands run against the tested state by queues from before its
1857 /// checks were workflows. Empty since.
1858 pub results: Vec<CheckResult>,
1859 /// Username of whoever merged it into the queue: a person, or `g1t`.
1860 pub enqueued_by: String,
1861 /// RFC 3339.
1862 pub created_at: String,
1863 /// RFC 3339. When it landed or left.
1864 pub finished_at: Option<String>,
1865}
1866
1867/// A repository's merge queue: what is in it, in order, and what recently
1868/// left it.
1869#[derive(Clone, Debug, Serialize, Deserialize)]
1870#[serde(rename_all = "camelCase")]
1871pub struct QueueView {
1872 /// Whether the repository merges through the queue.
1873 pub enabled: bool,
1874 pub active: Vec<QueueEntry>,
1875 /// Newest first.
1876 pub recent: Vec<QueueEntry>,
1877}
1878
1879/// `queue`: a repository's merge queue. Returns `Outcome<QueueView>`.
1880#[derive(Debug, Serialize, Deserialize)]
1881pub struct QueueArgs {
1882 pub repo: RepoPath,
1883 pub viewer: Viewer,
1884}
1885
1886/// `queue_build`: the next batch to test for a repository, if nothing is
1887/// being tested now. Returns `Vec<QueueJob>`, one per entry, each testing
1888/// the default branch with that entry and everything ahead of it.
1889#[derive(Debug, Serialize, Deserialize)]
1890#[serde(rename_all = "camelCase")]
1891pub struct QueueBuildArgs {
1892 pub repo_id: String,
1893}
1894
1895/// One pull request in a state being tested: where its change is.
1896#[derive(Clone, Debug, Serialize, Deserialize)]
1897#[serde(rename_all = "camelCase")]
1898pub struct QueueStackItem {
1899 pub number: u32,
1900 pub title: String,
1901 /// The repository holding the change: its fork, or the repository.
1902 pub source: RepoPath,
1903 /// The branch of `source` holding it.
1904 pub branch: String,
1905 pub commit: String,
1906}
1907
1908/// What a sandbox needs to build and check one combined state.
1909#[derive(Clone, Debug, Serialize, Deserialize)]
1910#[serde(rename_all = "camelCase")]
1911pub struct QueueJob {
1912 pub entry_id: String,
1913 /// Lets the sandbox, and nothing else, report this state's result.
1914 pub token: String,
1915 pub repo: RepoPath,
1916 pub default_branch: String,
1917 /// The default branch's commit to build on.
1918 pub base_commit: String,
1919 /// Where to push the tested state, in the repository itself.
1920 pub branch: String,
1921 /// The pull requests to merge in, in order; the last is the entry.
1922 pub stack: Vec<QueueStackItem>,
1923 /// Commands to run on the built state. Always empty: the state is
1924 /// checked by the `merge_group` workflows run on it, and the default
1925 /// branch's required checks must pass there.
1926 pub checks: Vec<String>,
1927 /// Always empty, as `checks`.
1928 #[serde(default)]
1929 pub contract_checks: Vec<String>,
1930 /// Who the sandbox acts as: a member who can push the tested state.
1931 pub actor: User,
1932}
1933
1934/// `report_queue`: a sandbox's result for one combined state. Returns
1935/// `Outcome<QueueState>`.
1936#[derive(Debug, Serialize, Deserialize)]
1937#[serde(rename_all = "camelCase")]
1938pub struct ReportQueueArgs {
1939 pub entry_id: String,
1940 pub token: String,
1941 #[serde(default)]
1942 pub combined_commit: Option<String>,
1943 #[serde(default)]
1944 pub results: Vec<CheckResult>,
1945 /// Set when the state could not be built or checked.
1946 #[serde(default)]
1947 pub error: Option<String>,
1948 /// For a merge conflict: the pull request whose change it collided with.
1949 #[serde(default)]
1950 pub conflict_with: Option<u32>,
1951 /// For a merge conflict: the files that conflicted.
1952 #[serde(default)]
1953 pub conflicts: Vec<String>,
1954}
1955
1956
1957/// `locate_pull`: where a pull request lives, by its id, for a tool that
1958/// knows only the fork it is working in (`g1t.sh/pulls/<id>`). Returns
1959/// `Outcome<LocatedPull>`; not found for anyone who cannot see it.
1960#[derive(Debug, Serialize, Deserialize)]
1961pub struct LocatePullArgs {
1962 pub id: String,
1963 pub viewer: Viewer,
1964}
1965
1966#[derive(Clone, Debug, Serialize, Deserialize)]
1967#[serde(rename_all = "camelCase")]
1968pub struct LocatedPull {
1969 pub repo: RepoPath,
1970 pub number: u32,
1971 pub title: String,
1972 pub status: PullStatus,
1973}
1974
1975// --- A person's work -------------------------------------------------------
1976
1977/// Issues or pull requests, on a person's profile.
1978#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1979#[serde(rename_all = "lowercase")]
1980pub enum AuthoredKind {
1981 Issue,
1982 Pull,
1983}
1984
1985/// The state filter on a person's work. `Closed` takes in merged pull
1986/// requests too; `Merged` is only those.
1987#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1988#[serde(rename_all = "lowercase")]
1989pub enum AuthoredState {
1990 Open,
1991 Closed,
1992 Merged,
1993}
1994
1995/// How a person's work is ordered.
1996#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1997#[serde(rename_all = "lowercase")]
1998pub enum AuthoredSort {
1999 /// Newest first.
2000 #[default]
2001 Created,
2002 /// Most recently changed first.
2003 Updated,
2004 /// Oldest first.
2005 Oldest,
2006}
2007
2008/// The most items one `by_author` page holds.
2009pub const AUTHORED_PAGE: u32 = 25;
2010
2011/// `by_author`: the issues and pull requests a person opened, only on
2012/// repositories `viewer` may read, so a private title never reaches anyone
2013/// who could not open it. Returns `Outcome<Authored>`; not found for an
2014/// account that does not exist.
2015#[derive(Debug, Serialize, Deserialize)]
2016pub struct ByAuthorArgs {
2017 pub username: String,
2018 pub viewer: Viewer,
2019 #[serde(default)]
2020 pub kind: Option<AuthoredKind>,
2021 #[serde(default)]
2022 pub state: Option<AuthoredState>,
2023 /// Only work on this repository: `namespace/name`.
2024 #[serde(default)]
2025 pub repo: Option<String>,
2026 #[serde(default)]
2027 pub sort: AuthoredSort,
2028 /// The `next` of the page before, to read on from there.
2029 #[serde(default)]
2030 pub before: Option<String>,
2031 /// At most [`AUTHORED_PAGE`]; that when absent.
2032 #[serde(default)]
2033 pub limit: Option<u32>,
2034}
2035
2036/// One issue or pull request a person opened.
2037#[derive(Clone, Debug, Serialize, Deserialize)]
2038#[serde(rename_all = "camelCase")]
2039pub struct AuthoredItem {
2040 pub kind: AuthoredKind,
2041 pub repo: RepoPath,
2042 pub number: u32,
2043 pub title: String,
2044 /// Open or closed; a merged pull request is closed.
2045 pub state: State,
2046 /// A pull request's own status.
2047 pub status: Option<PullStatus>,
2048 /// Why an issue was closed.
2049 pub reason: Option<IssueReason>,
2050 pub draft: bool,
2051 pub merged: bool,
2052 /// RFC 3339.
2053 pub created_at: String,
2054 /// RFC 3339.
2055 pub updated_at: String,
2056 /// When a pull request was merged. RFC 3339.
2057 pub merged_at: Option<String>,
2058}
2059
2060/// What a person has done, as far as the viewer may see.
2061#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2062#[serde(rename_all = "camelCase")]
2063pub struct AuthoredCounts {
2064 pub pulls_merged: u32,
2065 pub pulls_open: u32,
2066 pub pulls: u32,
2067 pub issues: u32,
2068 pub issues_open: u32,
2069}
2070
2071/// A repository a person has opened work on, with how much.
2072#[derive(Clone, Debug, Serialize, Deserialize)]
2073pub struct AuthoredRepo {
2074 pub repo: RepoPath,
2075 pub count: u32,
2076}
2077
2078/// A page of a person's work.
2079#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2080#[serde(rename_all = "camelCase")]
2081pub struct Authored {
2082 pub items: Vec<AuthoredItem>,
2083 /// Pass as `before` for the next page; null on the last.
2084 pub next: Option<String>,
2085 /// Over every repository the viewer may read, whatever the filters.
2086 pub counts: AuthoredCounts,
2087 /// Those repositories, most work first.
2088 pub repos: Vec<AuthoredRepo>,
2089}
2090
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2091/// How many days `contributions` covers: today and the 364 before it.
2092pub const CONTRIBUTION_DAYS: u64 = 365;
2093
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2094/// `contributions`: what a person did each day of the last year (commits
2095/// they pushed to a default branch, issues and pull requests opened,
2096/// reviews given), only on repositories `viewer` may read, for the calendar on their profile. Returns
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2097/// `Outcome<Contributions>`; not found for an account that does not exist.
2098#[derive(Debug, Serialize, Deserialize)]
2099pub struct ContributionsArgs {
2100 pub username: String,
2101 pub viewer: Viewer,
2102}
2103
2104/// One day with something on it. Days with nothing are left out.
2105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2106pub struct ContributionDay {
2107 /// `YYYY-MM-DD`, UTC.
2108 pub date: String,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2109 /// Everything that day, commits included.
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2110 pub count: u32,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2111 /// How many of `count` are commits they pushed (repos' `commit_days`).
2112 #[serde(default)]
2113 pub commits: u32,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2114}
2115
2116/// A person's year, as far as the viewer may see.
2117#[derive(Clone, Debug, Default, Serialize, Deserialize)]
2118pub struct Contributions {
2119 /// Oldest first.
2120 pub days: Vec<ContributionDay>,
2121 pub total: u32,
2122 /// The first day counted, `YYYY-MM-DD`; the last is today.
2123 pub from: String,
2124}
2125
Docs: the inbox, what lands in it and why, its tabs and actions2126#[cfg(test)]
2127mod required_tests {
2128 use super::*;
2129
2130 fn status(context: &str, state: &str) -> CommitStatus {
2131 CommitStatus {
2132 context: context.into(),
2133 state: state.into(),
2134 description: Some(format!("{context} {state}")),
2135 target_url: None,
2136 updated_at: String::new(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2137 source: None,
Merge checks: statuses and check runs on every commit2138 check_run_id: None,
Docs: the inbox, what lands in it and why, its tabs and actions2139 }
2140 }
2141
2142 #[test]
2143 fn a_context_names_its_check_and_event() {
2144 assert_eq!(check_name("CI / pull_request"), ("CI", Some("pull_request")));
2145 assert_eq!(check_name("Build and test / merge_group"), ("Build and test", Some("merge_group")));
2146 assert_eq!(check_name("g1t / deploy"), ("g1t / deploy", None));
2147 assert_eq!(check_name("g1t / deploy (docs)"), ("g1t / deploy (docs)", None));
2148 assert_eq!(check_name("lint"), ("lint", None));
2149 }
2150
2151 #[test]
2152 fn required_checks_are_missing_pending_failed_or_passed() {
2153 let required = vec!["CI".to_owned(), "Lint".to_owned(), "g1t / deploy".to_owned(), "Docs".to_owned()];
2154 let statuses = [
2155 status("CI / pull_request", "success"),
2156 status("Lint / pull_request", "pending"),
2157 status("g1t / deploy", "failure"),
2158 ];
2159 let states: Vec<RequiredState> = required_checks(&required, &statuses).into_iter().map(|c| c.state).collect();
2160 assert_eq!(
2161 states,
2162 [RequiredState::Success, RequiredState::Pending, RequiredState::Failure, RequiredState::Expected]
2163 );
2164 }
2165
2166 #[test]
2167 fn any_event_reports_a_check_and_a_failure_wins() {
2168 let required = vec!["ci".to_owned()];
2169 let both = [status("CI / push", "failure"), status("CI / pull_request", "success")];
2170 let check = &required_checks(&required, &both)[0];
2171 assert_eq!(check.state, RequiredState::Failure);
2172 assert_eq!(check.description.as_deref(), Some("CI / push failure"));
2173 let queue = [status("CI / merge_group", "success")];
2174 assert_eq!(required_checks(&required, &queue)[0].state, RequiredState::Success);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2175 }
2176
2177 #[test]
2178 fn a_pull_request_merges_into_its_base_or_the_default_branch() {
2179 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2180 "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": null, "title": "Fix it", "body": null,
2181 "agent": "agent", "runtime": "external", "status": "open", "fork": null, "forkRepoId": null,
2182 "branch": "fix", "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2183 "supersededBy": null, "checkStatus": null, "author": { "id": "x", "username": "x" },
2184 "createdAt": "", "updatedAt": ""
2185 }))
2186 .unwrap();
2187 assert_eq!(pull.base_branch("main"), "main");
2188 assert!(pull.targets_default("main"));
2189 assert_eq!(pull.comparison(&None).base_branch, None);
2190 pull.base = Some("release".into());
2191 assert_eq!(pull.base_branch("main"), "release");
2192 assert!(!pull.targets_default("main"));
2193 assert_eq!(pull.comparison(&None).base_branch.as_deref(), Some("release"));
2194 }
2195
2196 #[test]
Docs: the inbox, what lands in it and why, its tabs and actions2197 fn required_names_are_tidied() {
2198 let names = vec![" CI ".to_owned(), "ci".to_owned(), String::new(), "Lint".to_owned()];
2199 assert_eq!(tidy_required(&names), ["CI", "Lint"]);
2200 }
2201
2202 #[test]
2203 fn a_definition_of_done_is_added_once() {
2204 let items = commands_pass(&["cargo test".to_owned(), " ".to_owned()]);
2205 assert_eq!(items, ["`cargo test` passes."]);
2206 let body = with_definition_of_done("Fix the greeting.", &items);
2207 assert_eq!(body, "Fix the greeting.\n\n## Definition of done\n\n- `cargo test` passes.");
2208 assert_eq!(with_definition_of_done(&body, &items), body);
2209 assert_eq!(with_definition_of_done("", &items), "## Definition of done\n\n- `cargo test` passes.");
2210 assert_eq!(with_definition_of_done(" Text ", &[]), "Text");
2211 }
2212}
2213
2214#[cfg(test)]
2215mod authorship_tests {
2216 use super::*;
2217 use crate::PrincipalKind;
2218 use crate::credentials::{Acting, Principal};
2219 use crate::identity::{AGENT_ID, AgentScope};
2220
2221 fn person() -> User {
2222 User {
2223 id: "usr_1".into(),
2224 username: "syntaqx".into(),
2225 verified: true,
2226 ..User::default()
2227 }
2228 }
2229
2230 fn agent_for(id: &str, username: &str) -> User {
2231 User {
2232 id: AGENT_ID.into(),
2233 username: "g1t".into(),
2234 kind: PrincipalKind::Agent,
2235 acting: Some(Box::new(Acting {
2236 credential_id: "tok_1".into(),
2237 agent: "g1t".into(),
2238 on_behalf_of: Principal { id: id.into(), username: username.into() },
2239 scope: AgentScope {
2240 repo: RepoPath { namespace: "acme".into(), name: "web".into() },
2241 operations: Vec::new(),
2242 run: None,
2243 },
2244 })),
2245 ..User::default()
2246 }
2247 }
2248
2249 fn pull(author: User, requested_by: Option<User>) -> Pull {
2250 let mut pull: Pull = serde_json::from_value(serde_json::json!({
2251 "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": 12, "title": "Fix it", "body": null,
2252 "agent": "g1t", "runtime": "hosted", "status": "open", "fork": null, "forkRepoId": null,
2253 "branch": null, "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2254 "supersededBy": null, "checkStatus": null,
2255 "author": { "id": "x", "username": "x" },
2256 "createdAt": "", "updatedAt": ""
2257 }))
2258 .unwrap();
2259 pull.author = author;
2260 pull.requested_by = requested_by;
2261 pull
2262 }
2263
2264 #[test]
2265 fn a_change_a_person_has_g1t_make_is_g1t_s_requested_by_them() {
2266 let (author, asked) = authorship(&person(), true);
2267 assert_eq!((author.id.as_str(), author.username.as_str(), author.kind), (AGENT_ID, "g1t", PrincipalKind::Agent));
2268 let asked = asked.expect("the person asked for it");
2269 assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
2270 }
2271
2272 #[test]
2273 fn what_g1t_s_agent_files_at_work_is_g1t_s_requested_by_whoever_it_works_for() {
2274 let (author, asked) = authorship(&agent_for("usr_1", "syntaqx"), false);
2275 assert_eq!(author.id, AGENT_ID);
2276 assert_eq!(asked.map(|user| user.username), Some("syntaqx".into()));
2277 }
2278
2279 #[test]
2280 fn nobody_asked_for_g1t_s_own_work() {
2281 // A run g1t started itself acts for g1t, not for a person.
2282 let (author, asked) = authorship(&agent_for(crate::system::ID, "g1t"), false);
2283 assert_eq!(author.id, AGENT_ID);
2284 assert!(asked.is_none());
2285 // A security update g1t opens is g1t's own, as before.
2286 let (author, asked) = authorship(&User::system("acme"), true);
2287 assert_eq!((author.id.as_str(), author.kind), (crate::system::ID, PrincipalKind::System));
2288 assert!(asked.is_none());
2289 }
2290
2291 #[test]
2292 fn anyone_else_opens_their_own() {
2293 let (author, asked) = authorship(&person(), false);
2294 assert_eq!((author.id.as_str(), author.kind), ("usr_1", PrincipalKind::User));
2295 assert!(asked.is_none());
2296 // Nothing but who they are is kept.
2297 assert!(author.workspaces.is_empty() && author.acting.is_none());
2298 }
2299
2300 #[test]
2301 fn the_requester_owns_g1t_s_pull_request_and_an_author_their_own() {
2302 let made = pull(g1t_author(), Some(person()));
2303 assert_eq!(made.owner().id, "usr_1");
2304 assert!(made.is_owned_by("usr_1"));
2305 assert!(!made.is_owned_by(AGENT_ID), "g1t's agent does not answer for its own change");
2306 let own = pull(person(), None);
2307 assert_eq!(own.owner().id, "usr_1");
2308 assert!(own.is_owned_by("usr_1"));
2309 }
2310
2311 #[test]
2312 fn requested_by_is_null_when_nobody_asked_and_read_as_absent_from_older_senders() {
2313 let own = pull(person(), None);
2314 let sent = serde_json::to_value(&own).unwrap();
2315 assert!(sent["requestedBy"].is_null());
2316 let mut older = sent.clone();
2317 older.as_object_mut().unwrap().remove("requestedBy");
2318 let read: Pull = serde_json::from_value(older).unwrap();
2319 assert!(read.requested_by.is_none());
2320 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2321
2322 #[test]
2323 fn a_comment_is_changed_by_its_author_or_a_maintainer() {
2324 use CommentKind::{Comment, Event};
2325 // Its author edits and deletes it; someone else needs Maintain.
2326 assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, false).is_ok());
2327 assert!(may_change_comment(Comment, false, "usr_1", "usr_1", false, true).is_ok());
2328 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, false).is_err());
2329 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", false, true).is_err());
2330 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, false).is_ok());
2331 assert!(may_change_comment(Comment, false, "usr_1", "usr_2", true, true).is_ok());
2332 // A review's text can be edited, but its verdict stays.
2333 assert!(may_change_comment(Comment, true, "usr_1", "usr_1", false, false).is_ok());
2334 assert!(may_change_comment(Comment, true, "usr_1", "usr_1", true, true).is_err());
2335 // A note of what happened is nobody's to change.
2336 assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, false).is_err());
2337 assert!(may_change_comment(Event, false, "usr_1", "usr_1", true, true).is_err());
2338 }
Docs: the inbox, what lands in it and why, its tabs and actions2339}

This file's history is long; its oldest lines are credited to the oldest commit read.