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