flagon-io/g1t

public

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/crates/contracts/src/work.rs

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