flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/crates/contracts/src/work.rs

1,637 lines53,741 bytesCodeBlame
//! The work service: issues, pull requests, comments and sessions.
//!
//! Each `*Args` struct is the argument of the method of the same name,
//! served at `POST /rpc/<method>`.
//!
//! Issues and pull requests share one sequence of numbers per repository,
//! so `#12` names exactly one of them.

use serde::{Deserialize, Serialize};

use crate::repos::{CompareArgs, RepoPath};
use crate::{User, Viewer};

/// Labels every repository starts with. Any other label comes into being
/// the first time it is put on an issue.
pub const DEFAULT_LABELS: [&str; 5] = ["bug", "feature", "docs", "chore", "question"];

/// `open` or `closed`: the filter on lists of issues and pull requests. An
/// open pull request is a draft or one ready for review; a closed one was
/// merged or closed without merging.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum State {
    Open,
    Closed,
}

/// Why an issue was closed.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IssueReason {
    /// The work was done. If a pull request did it, `resolved_by` names it.
    Completed,
    NotPlanned,
}

impl IssueReason {
    pub fn as_str(self) -> &'static str {
        match self {
            IssueReason::Completed => "completed",
            IssueReason::NotPlanned => "not_planned",
        }
    }
}

/// Something that should change in a repository: a bug, a feature, a
/// question. Opened by a person, an agent or an integration. Pull requests
/// are made against it; the one that is merged resolves it.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Issue {
    pub id: String,
    pub repo_id: String,
    /// Shown as `#12`.
    pub number: u32,
    pub title: String,
    /// Markdown. Also what an agent is given to work from.
    pub body: String,
    pub labels: Vec<String>,
    /// Commands that must pass for a pull request to be accepted.
    pub checks: Vec<String>,
    pub state: State,
    /// Set when closed.
    pub reason: Option<IssueReason>,
    /// The number of the pull request whose merge closed this issue.
    pub resolved_by: Option<u32>,
    pub author: User,
    /// RFC 3339.
    pub created_at: String,
    /// RFC 3339.
    pub updated_at: String,
    /// RFC 3339.
    pub closed_at: Option<String>,
    /// Pull requests made against this issue, in any state.
    pub pull_count: u32,
    pub comment_count: u32,
    /// Usernames of the people it is assigned to.
    #[serde(default)]
    pub assignees: Vec<String>,
    /// The numbers of the issues that have to be merged before this one is
    /// worked on.
    #[serde(default)]
    pub blocked_by: Vec<u32>,
    /// Whether a g1t agent takes it as soon as it can: at once, or when
    /// what it is blocked by has merged.
    #[serde(default)]
    pub queued: bool,
    /// The agent working on it now: the one behind its newest pull request
    /// that is still in progress in a fork, such as `g1t-agent`.
    #[serde(default)]
    pub agent: Option<String>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PullStatus {
    /// Still being worked on.
    Draft,
    /// Ready for review.
    Open,
    Merged,
    /// Closed without merging.
    Closed,
}

impl PullStatus {
    pub fn as_str(self) -> &'static str {
        match self {
            PullStatus::Draft => "draft",
            PullStatus::Open => "open",
            PullStatus::Merged => "merged",
            PullStatus::Closed => "closed",
        }
    }

    /// Whether the pull request can still be changed or merged.
    pub fn is_active(self) -> bool {
        matches!(self, PullStatus::Draft | PullStatus::Open)
    }
}

/// Where the agent runs: on g1t's sandboxes, or in someone's own session.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Runtime {
    Hosted,
    External,
}

/// A proposed change. It is made either in a fork created for it, which is
/// how agents work, or on a branch pushed to the repository itself.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Pull {
    pub id: String,
    pub repo_id: String,
    /// Shown as `#12`.
    pub number: u32,
    /// The number of the issue this is for, if any.
    pub issue: Option<u32>,
    pub title: String,
    /// Markdown: what changed and why. Set when marked ready.
    pub body: Option<String>,
    /// A label for the agent doing the work, e.g. `claude-code`.
    pub agent: String,
    pub runtime: Runtime,
    pub status: PullStatus,
    /// The fork holding the change, unless it is on a branch.
    pub fork: Option<RepoPath>,
    /// The fork's repository id.
    pub fork_repo_id: Option<String>,
    /// The branch of the repository holding the change, unless it is in a
    /// fork.
    pub branch: Option<String>,
    pub head_commit: Option<String>,
    /// For a merged pull request, what the branch pointed to before the
    /// merge. Comparing against it shows what the pull request changed.
    pub merge_base: Option<String>,
    /// Username of whoever merged it.
    pub merged_by: Option<String>,
    /// RFC 3339.
    pub merged_at: Option<String>,
    /// Set on a pull request closed because another one for the same issue
    /// was merged: that one's number.
    pub superseded_by: Option<u32>,
    /// Where the latest run of the issue's acceptance checks stands, if
    /// there has been one against the current head.
    pub check_status: Option<CheckStatus>,
    /// The files it changes, as of its latest push.
    #[serde(default)]
    pub files: Vec<ChangedFile>,
    /// Usernames of the people it is assigned to.
    #[serde(default)]
    pub assignees: Vec<String>,
    /// Those whose review was asked for: usernames, and `g1t-agent` when a
    /// g1t agent was asked.
    #[serde(default)]
    pub reviewers: Vec<String>,
    pub author: User,
    /// RFC 3339.
    pub created_at: String,
    /// RFC 3339.
    pub updated_at: String,
}

/// One file a pull request changes, and by how much.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangedFile {
    pub path: String,
    pub additions: u32,
    pub deletions: u32,
}

/// Another pull request in progress that changes some of the same files.
/// Two for the same issue are alternatives; two for different issues are
/// heading for a conflict.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Overlap {
    pub number: u32,
    pub title: String,
    /// The number of the issue the other pull request is for.
    pub issue: Option<u32>,
    /// The files both change.
    pub paths: Vec<String>,
}

impl Pull {
    /// What to ask the repos service to see what this pull request changes.
    ///
    /// A fork is compared as a whole. A branch is compared by name while
    /// the pull request is open, and by the commit it was merged or closed
    /// at afterwards, so later pushes to the branch do not change the record.
    pub fn comparison(&self, viewer: &Viewer) -> CompareArgs {
        let settled = matches!(self.status, PullStatus::Merged | PullStatus::Closed);
        let (repo_id, head) = match &self.fork_repo_id {
            Some(fork) => (fork.clone(), None),
            None => (
                self.repo_id.clone(),
                self.head_commit
                    .clone()
                    .filter(|_| settled)
                    .or_else(|| self.branch.clone()),
            ),
        };
        CompareArgs {
            repo_id,
            viewer: viewer.clone(),
            base: self.merge_base.clone(),
            head,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CheckStatus {
    /// Waiting for a sandbox.
    Queued,
    Running,
    Passed,
    Failed,
    /// The checks could not be run at all.
    Errored,
}

impl CheckStatus {
    pub fn as_str(self) -> &'static str {
        match self {
            CheckStatus::Queued => "queued",
            CheckStatus::Running => "running",
            CheckStatus::Passed => "passed",
            CheckStatus::Failed => "failed",
            CheckStatus::Errored => "errored",
        }
    }
}

/// How one acceptance check went.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckResult {
    pub command: String,
    pub passed: bool,
    /// Absent when the command was stopped for taking too long.
    #[serde(default)]
    pub exit_code: Option<i32>,
    /// What the command printed, standard output and error together. The
    /// end of it, when there was a lot.
    #[serde(default)]
    pub output: String,
    #[serde(default)]
    pub duration_ms: u64,
}

/// One run of an issue's acceptance checks against a pull request's head,
/// in a sandbox that holds nothing but that commit.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckRun {
    pub id: String,
    /// The commit that was checked.
    pub head_commit: String,
    pub status: CheckStatus,
    pub results: Vec<CheckResult>,
    /// Why the checks could not be run, when `status` is `errored`.
    pub error: Option<String>,
    /// RFC 3339.
    pub created_at: String,
    /// RFC 3339.
    pub finished_at: Option<String>,
}

/// A reviewer's decision on a pull request.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
    Approve,
    RequestChanges,
}

impl Verdict {
    pub fn as_str(self) -> &'static str {
        match self {
            Verdict::Approve => "approve",
            Verdict::RequestChanges => "request_changes",
        }
    }
}

/// What an entry in a conversation is.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CommentKind {
    /// Something a person or an agent wrote.
    #[default]
    Comment,
    /// Something that happened: an assignment, a review asked for, a close.
    Event,
}

/// A comment on an issue or a pull request. On a pull request it can sit
/// on one line of the change, and it can carry a reviewer's verdict.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Comment {
    pub id: String,
    /// Something a person wrote, or something that happened.
    #[serde(default)]
    pub kind: CommentKind,
    pub author: User,
    /// Markdown. For an event, what its author did, as the rest of a
    /// sentence that starts with their name: "assigned ana".
    pub body: String,
    /// The file commented on, for a comment on a line.
    pub path: Option<String>,
    /// The line of that file, as numbered after the change.
    pub line: Option<u32>,
    pub verdict: Option<Verdict>,
    /// RFC 3339.
    pub created_at: String,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionEntryKind {
    Prompt,
    Message,
    ToolCall,
    ToolResult,
    Note,
}

/// One step of an agent's session: the "why" behind a pull request's commits.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SessionEntry {
    pub seq: u32,
    pub kind: SessionEntryKind,
    pub text: String,
    /// For tool calls and results.
    pub tool: Option<String>,
    /// The fork's head commit when this entry was recorded, if known.
    pub commit: Option<String>,
    /// RFC 3339.
    pub at: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NewSessionEntry {
    pub kind: SessionEntryKind,
    pub text: String,
    #[serde(default)]
    pub tool: Option<String>,
    #[serde(default)]
    pub commit: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IssueDetail {
    pub issue: Issue,
    /// Every pull request made against it, oldest first.
    pub pulls: Vec<Pull>,
    pub comments: Vec<Comment>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PullDetail {
    pub pull: Pull,
    /// The issue it is for, if any.
    pub issue: Option<Issue>,
    pub comments: Vec<Comment>,
    /// The latest run of the issue's acceptance checks.
    pub checks: Option<CheckRun>,
    /// Other pull requests in progress that change the same files.
    #[serde(default)]
    pub overlaps: Vec<Overlap>,
    /// Whether the branch it would merge into has moved on without it, so
    /// that it has to catch up before it can merge.
    #[serde(default)]
    pub behind: bool,
    /// Whether a g1t agent is reviewing it right now.
    #[serde(default)]
    pub review_pending: bool,
    /// Where it stands on its way to being merged, for a pull request g1t
    /// is seeing through. Absent on anyone else's.
    #[serde(default)]
    pub lifecycle: Option<Lifecycle>,
    /// A merge was asked for while it was behind: g1t is bringing it up to
    /// date and will then land it.
    #[serde(default)]
    pub landing: bool,
    /// Why g1t stopped working on it, if it did: a catch-up that could not
    /// be completed, for example.
    #[serde(default)]
    pub stalled: Option<String>,
    /// Messages people sent the agent while it worked, oldest first.
    #[serde(default)]
    pub messages: Vec<AgentMessage>,
    /// What workflow runs said about its head commit, one per workflow.
    #[serde(default)]
    pub statuses: Vec<CommitStatus>,
    /// Whether it merges cleanly into the branch it targets, worked out
    /// ahead of time whenever either side moves.
    #[serde(default)]
    pub mergeable: Mergeable,
    /// When `mergeable` is `conflicting`: the files that conflict.
    #[serde(default)]
    pub conflicts: Vec<String>,
    /// Earlier runs of its acceptance checks, newest first, without their
    /// output.
    #[serde(default)]
    pub earlier_checks: Vec<CheckRun>,
}

/// Whether a pull request's change merges cleanly into the branch it
/// targets.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mergeable {
    /// It merges without conflicts.
    Clean,
    /// Some files conflict: see `PullDetail::conflicts`.
    Conflicting,
    /// Not known: never worked out, or it could not be.
    #[default]
    Unknown,
    /// Being worked out now.
    Checking,
}

impl Mergeable {
    pub fn as_str(self) -> &'static str {
        match self {
            Mergeable::Clean => "clean",
            Mergeable::Conflicting => "conflicting",
            Mergeable::Unknown => "unknown",
            Mergeable::Checking => "checking",
        }
    }

    pub fn parse(value: Option<&str>) -> Mergeable {
        match value {
            Some("clean") => Mergeable::Clean,
            Some("conflicting") => Mergeable::Conflicting,
            Some("checking") => Mergeable::Checking,
            _ => Mergeable::Unknown,
        }
    }
}

/// `start_mergecheck`: claims the probe of whether a pull request merges
/// cleanly, which the work service asked for with a `pull.mergecheck`
/// event. Called by the runner service, which starts the sandbox. Refused
/// when it is no longer wanted, or when the repository already has as many
/// probes running as it may. Returns `Outcome<MergecheckJob>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartMergecheckArgs {
    pub pull_id: String,
}

/// What a sandbox needs to find out whether a pull request merges cleanly.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MergecheckJob {
    pub pull_id: String,
    /// Lets the sandbox, and nothing else, report this probe.
    pub token: String,
    pub repo: RepoPath,
    pub number: u32,
    pub default_branch: String,
    /// The default branch's commit to merge into.
    pub base: String,
    /// The repository holding the change: its fork, or the repository.
    pub source: RepoPath,
    /// The branch of `source` holding it.
    pub branch: String,
    /// The change's commit.
    pub head: String,
    /// Who opened the pull request, and so can read its source.
    pub author: User,
}

/// `report_mergecheck`: what a sandbox found. Returns `Outcome<Mergeable>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportMergecheckArgs {
    pub pull_id: String,
    pub token: String,
    /// The files that conflict; empty when it merges cleanly.
    #[serde(default)]
    pub conflicts: Vec<String>,
    /// Why it could not be found out.
    #[serde(default)]
    pub error: Option<String>,
}

/// What a workflow run (or another tool) says about a commit.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommitStatus {
    /// What reported it, such as `CI / push`.
    pub context: String,
    /// `pending`, `success`, `failure` or `error`.
    pub state: String,
    pub description: Option<String>,
    /// Where to see more, such as the run's page.
    pub target_url: Option<String>,
    pub updated_at: String,
}

/// `set_commit_status`: for services only. Returns `Outcome<bool>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetCommitStatusArgs {
    pub repo_id: String,
    pub sha: String,
    pub context: String,
    pub state: String,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub target_url: Option<String>,
}

/// A message a person sent an agent at work on a pull request. The agent
/// receives it at its next step.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentMessage {
    pub id: String,
    pub author: String,
    pub body: String,
    /// RFC 3339.
    pub created_at: String,
    /// RFC 3339. When the agent received it; null until then.
    pub delivered_at: Option<String>,
    /// `message` from a person, or from another pull request's agent a
    /// `question`, a `handoff` of work, or the `answer` to one.
    #[serde(default = "message_kind")]
    pub kind: String,
    /// The pull request whose agent sent it, when an agent did.
    #[serde(default)]
    pub from_number: Option<u32>,
    /// The pull request it was sent to.
    #[serde(default)]
    pub to_number: u32,
    /// For a question or handoff: the reply, once there is one.
    #[serde(default)]
    pub answer: Option<String>,
    /// For a handoff: whether it was declined.
    #[serde(default)]
    pub declined: bool,
    /// For the agent that sent it: what to expect, when the agent it asked
    /// is not at work and will not answer soon.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub hint: Option<String>,
}

fn message_kind() -> String {
    "message".to_owned()
}

/// `message_agent`: sends the agent working on a pull request a message.
/// The pull request's author and members of the workspace may. Returns
/// `Outcome<AgentMessage>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct MessageAgentArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
    pub body: String,
    /// For an agent: `question` or `handoff`; a person's is a `message`.
    #[serde(default)]
    pub kind: Option<String>,
    /// For an agent: the pull request it is working on, which the reply
    /// goes back to.
    #[serde(default)]
    pub from_number: Option<u32>,
}

/// `answer_message`: replies to a question or a handoff an agent received,
/// accepting or declining a handoff. The reply reaches the asking agent at
/// its next step. Returns `Outcome<AgentMessage>`, the message answered.
#[derive(Debug, Serialize, Deserialize)]
pub struct AnswerMessageArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub id: String,
    pub body: String,
    #[serde(default)]
    pub decline: bool,
}

/// `take_messages`: the messages not yet delivered to the agent working on
/// a pull request, marked delivered. Only g1t's agents may. Returns
/// `Outcome<Vec<AgentMessage>>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct TakeMessagesArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
}

/// A step on the way from an assigned issue to a pull request that is ready
/// to merge. g1t takes each one without being asked.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Stage {
    /// The agent is making the change.
    Working,
    /// The issue's acceptance checks are running against it.
    Checking,
    /// A g1t agent is reviewing it.
    Reviewing,
    /// The agent is addressing failed checks or a review.
    Revising,
    /// The agent is merging in the branch it would land on, which moved.
    CatchingUp,
    /// Woken to answer a question another agent asked it, or a handoff.
    Answering,
    /// In the repository's merge queue, being tested with what is ahead of
    /// it before it lands.
    Queued,
    /// Checks passed, reviewed and approved, up to date. A person merges.
    Ready,
    /// g1t has stopped and a person has to decide what happens next.
    NeedsYou,
}

/// Where a pull request made by a g1t agent stands. See [`Stage`].
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Lifecycle {
    pub stage: Stage,
    /// One sentence saying what is happening, or why it stopped.
    pub detail: String,
    /// How many times the agent has been sent back to revise it.
    pub revisions: u32,
}

/// `advance`: works out the next step for a pull request g1t is seeing
/// through and, if there is one to take now, claims it, so that it is
/// taken once however many times this is called. Called by the runner
/// service, which carries the step out. Returns `Advance`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdvanceArgs {
    pub pull_id: String,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum Advance {
    /// Nothing to do now: a step is under way, or it is a person's turn.
    None,
    /// Have a g1t agent review it.
    Review { job: LifecycleJob },
    /// Send the agent back to address `job.feedback`.
    Revise { job: LifecycleJob },
    /// Merge in the branch it would land on.
    CatchUp { job: LifecycleJob },
}

/// What the runner needs to carry out a step of a pull request's lifecycle.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LifecycleJob {
    pub pull_id: String,
    pub repo: RepoPath,
    pub number: u32,
    /// Who the pull request belongs to. Sandboxes act as them.
    pub author: User,
    /// The repository holding the change: its fork, or the repository
    /// itself for one made on a branch.
    pub source: RepoPath,
    /// The branch of the source holding the change; its default branch
    /// when absent.
    #[serde(default)]
    pub branch: Option<String>,
    pub default_branch: String,
    pub title: String,
    pub description: String,
    pub issue: Option<Issue>,
    /// For a revision: the failed checks or the review to address.
    pub feedback: String,
    /// For a revision: which one this is, from 1.
    pub round: u32,
}

/// How a repository wants its pull requests handled. A repository that has
/// changed nothing has the defaults.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct RepoSettings {
    /// Land a g1t agent's pull request without a person once it is ready:
    /// checks passed and approved as the settings below require.
    pub auto_merge: bool,
    /// Refuse to merge a pull request that does not contain the default
    /// branch's latest commits, so that what merges is what was checked.
    /// When off, merging one that is behind brings it up to date first.
    pub require_up_to_date: bool,
    /// How many approving reviews a pull request needs before it may
    /// merge. A reviewer who has since asked for changes blocks it.
    pub required_approvals: u32,
    /// Whether a g1t agent's approval counts towards `required_approvals`.
    pub count_agent_approvals: bool,
    /// Whether a member may merge although the acceptance checks did not
    /// pass.
    pub allow_ignoring_checks: bool,
    /// Whether a g1t agent's pull request is reviewed by a second agent
    /// without being asked.
    pub agent_review: bool,
    /// How many times a g1t agent is sent back to its pull request before
    /// a person is asked instead.
    pub max_revisions: u32,
    /// Merge through a queue: pull requests are tested together with those
    /// ahead of them, and only a combination that passed reaches the default
    /// branch.
    pub merge_queue: bool,
    /// Username of the member who last changed the settings, if anyone has.
    pub updated_by: Option<String>,
    /// RFC 3339.
    pub updated_at: Option<String>,
}

impl Default for RepoSettings {
    fn default() -> Self {
        RepoSettings {
            auto_merge: false,
            require_up_to_date: false,
            required_approvals: 0,
            count_agent_approvals: true,
            allow_ignoring_checks: true,
            agent_review: true,
            max_revisions: 2,
            merge_queue: false,
            updated_by: None,
            updated_at: None,
        }
    }
}

/// `update_settings`: replaces a repository's settings. Members of its
/// workspace only. Returns `Outcome<RepoSettings>`. `get_settings` takes
/// `ViewArgs` and returns the same.
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateSettingsArgs {
    pub actor: User,
    pub repo: RepoPath,
    /// Who changed them and when are filled in by the service.
    pub settings: RepoSettings,
}

/// `catch_up_job`: what the runner needs to bring a pull request up to date
/// because a merge of it was asked for. Null if none was. Returns
/// `Option<LifecycleJob>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CatchUpJobArgs {
    pub pull_id: String,
}

/// `wake_for_messages`: the agent on a pull request was asked a question
/// or handed work while it was not at work. Claims a short step for it to
/// answer, and hands over what it was sent, marked read. Null when there
/// is nothing waiting, or the pull request cannot take a step now.
/// Returns `Option<Wake>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WakeForMessagesArgs {
    pub pull_id: String,
}

/// What an agent woken to answer needs: its pull request, and what it was
/// sent, oldest first.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Wake {
    pub job: LifecycleJob,
    pub messages: Vec<AgentMessage>,
}

/// `stall`: records that a step could not be carried out, so that g1t
/// stops and a person is asked. Returns `bool`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StallArgs {
    pub pull_id: String,
    pub reason: String,
}

/// `managed_pulls`: ids of the open pull requests g1t is seeing through,
/// in one repository or in all of them. Returns `Vec<String>`.
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManagedPullsArgs {
    #[serde(default)]
    pub repo_id: Option<String>,
}

/// `open_issue`. Returns `Outcome<Issue>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct OpenIssueArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub title: String,
    #[serde(default)]
    pub body: String,
    #[serde(default)]
    pub labels: Vec<String>,
    #[serde(default)]
    pub checks: Vec<String>,
}

/// `list_issues`, newest first. Returns `Outcome<Vec<Issue>>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct ListIssuesArgs {
    pub repo: RepoPath,
    pub viewer: Viewer,
    #[serde(default)]
    pub state: Option<State>,
    /// Only issues carrying this label.
    #[serde(default)]
    pub label: Option<String>,
}

/// `list_pulls`, newest first. Returns `Outcome<Vec<Pull>>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct ListPullsArgs {
    pub repo: RepoPath,
    pub viewer: Viewer,
    #[serde(default)]
    pub state: Option<State>,
}

/// `get_issue` (`Outcome<IssueDetail>`), `get_pull` (`Outcome<PullDetail>`),
/// `read_session` (`Outcome<Vec<SessionEntry>>`), `list_labels`
/// (`Outcome<Vec<String>>`) and `counts` (`Outcome<Counts>`). The last two
/// ignore `number`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ViewArgs {
    pub repo: RepoPath,
    #[serde(default)]
    pub number: u32,
    pub viewer: Viewer,
    /// For `read_session`: only entries after this sequence number.
    #[serde(default)]
    pub after_seq: u32,
}

/// How many issues and pull requests are open on a repository.
#[derive(Debug, Serialize, Deserialize)]
pub struct Counts {
    pub issues: u32,
    pub pulls: u32,
}

/// `update_issue`: changes whichever fields are given. The author or a
/// member of the workspace may. Returns `Outcome<Issue>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateIssueArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub body: Option<String>,
    #[serde(default)]
    pub labels: Option<Vec<String>>,
    /// Usernames of the people it is assigned to; replaces the whole set.
    /// Assigning it to the g1t agent is the runner's `run`, not this.
    #[serde(default)]
    pub assignees: Option<Vec<String>>,
}

/// `close_issue` and `reopen_issue`. Each returns `Outcome<Issue>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct IssueActionArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
    /// For `close_issue`; `completed` if left out.
    #[serde(default)]
    pub reason: Option<IssueReason>,
}

/// `add_comment`, on an issue or a pull request. On a pull request it may
/// name a line of the change, and may carry a verdict; nobody can give a
/// verdict on their own pull request. Returns `Outcome<Comment>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AddCommentArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
    /// May be empty when approving.
    #[serde(default)]
    pub body: String,
    #[serde(default)]
    pub path: Option<String>,
    #[serde(default)]
    pub line: Option<u32>,
    #[serde(default)]
    pub verdict: Option<Verdict>,
}

/// `open_pull`. Without `branch`, forks the repo and returns a draft pull
/// request to push to. With it, opens a pull request, ready for review,
/// for a branch already pushed to the repo. Returns `Outcome<Pull>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct OpenPullArgs {
    pub actor: User,
    pub repo: RepoPath,
    /// The issue this is for.
    #[serde(default)]
    pub issue: Option<u32>,
    /// Defaults to the issue's title; required without an issue.
    #[serde(default)]
    pub title: String,
    /// What changed and why. Usually set later, when a draft is marked ready.
    #[serde(default)]
    pub body: String,
    /// A branch of the repository that already holds the change.
    #[serde(default)]
    pub branch: Option<String>,
    #[serde(default)]
    pub agent: String,
    pub runtime: Runtime,
}

/// `ready_pull`, `close_pull` and `merge_pull`. Each returns `Outcome<Pull>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PullActionArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
    /// For `ready_pull`: what changed and why.
    #[serde(default)]
    pub summary: String,
    /// For `merge_pull`: leave the issue open and the other pull requests
    /// for it untouched, because this one is only part of the work.
    #[serde(default)]
    pub keep_issue_open: bool,
    /// For `merge_pull`: merge although the acceptance checks have not
    /// passed.
    #[serde(default)]
    pub ignore_checks: bool,
}

/// Where a plan stands.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PlanStatus {
    /// An agent is reading the repository and writing it.
    Planning,
    /// Written, and waiting for a person to read and apply it.
    Ready,
    /// It could not be written.
    Failed,
    /// Its issues have been opened.
    Applied,
}

/// One issue a plan proposes.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct PlannedIssue {
    pub title: String,
    /// Markdown: what to change, where, and why.
    pub body: String,
    pub labels: Vec<String>,
    /// Commands that must pass once the change is made.
    pub checks: Vec<String>,
    /// The files it will most likely change.
    pub files: Vec<String>,
    /// The positions, counting from 1, of earlier issues in the plan that
    /// have to be merged first. An agent writes this as `depends_on`.
    #[serde(alias = "depends_on")]
    pub depends_on: Vec<u32>,
    /// Its number, once the plan has been applied and it was kept.
    pub number: Option<u32>,
}

/// An outcome someone wrote, and the issues an agent proposes to get there.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Plan {
    pub id: String,
    pub repo_id: String,
    /// The outcome wanted, as written.
    pub brief: String,
    pub status: PlanStatus,
    /// The agent's account of how it split the work.
    pub summary: String,
    pub issues: Vec<PlannedIssue>,
    /// Why it could not be written, when `status` is `failed`.
    pub error: Option<String>,
    pub author: User,
    /// RFC 3339.
    pub created_at: String,
    /// RFC 3339.
    pub finished_at: Option<String>,
    /// Once applied: where each issue it opened stands now, in plan order.
    /// Filled in by `get_plan` only.
    #[serde(default)]
    pub progress: Vec<IssueProgress>,
    /// Questions and handoffs between the agents on its pull requests,
    /// newest first. Filled in by `get_plan` only.
    #[serde(default)]
    pub exchanges: Vec<AgentMessage>,
}

/// Where one issue of an applied plan stands.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueProgress {
    pub number: u32,
    pub title: String,
    /// `blocked` (waiting on issues it depends on), `waiting` (for an
    /// agent), `open` (nobody on it), one of the lifecycle stages
    /// (`working`, `checking`, `reviewing`, `revising`, `catching_up`,
    /// `answering`, `queued`, `ready`, `needs_you`), `landed` or `closed`.
    pub state: String,
    /// One sentence about where it stands.
    pub detail: String,
    /// The issues it is waiting on that are still open.
    pub blocked_by: Vec<u32>,
    /// The pull request carrying it, the newest if several.
    pub pull: Option<u32>,
    /// Who or what is working on it, e.g. `g1t-agent`.
    pub agent: Option<String>,
}

/// `start_plan`: records an outcome to plan for. Members of the
/// repository's workspace only. Called by the runner service, which starts
/// the sandbox. Returns `Outcome<PlanJob>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct StartPlanArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub brief: String,
}

/// What a sandbox needs to write a plan.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanJob {
    pub plan_id: String,
    /// Lets the sandbox, and nothing else, report this plan.
    pub token: String,
    pub brief: String,
    pub repo: RepoPath,
}

/// `report_plan`: the plan a sandbox's agent wrote, or why it could not
/// write one. Returns `Outcome<bool>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportPlanArgs {
    pub plan_id: String,
    pub token: String,
    #[serde(default)]
    pub summary: String,
    #[serde(default)]
    pub issues: Vec<PlannedIssue>,
    #[serde(default)]
    pub error: Option<String>,
}

/// `get_plan`. Members only. Returns `Outcome<Plan>`. `list_plans` takes
/// `ViewArgs` and returns `Outcome<Vec<Plan>>`, newest first.
#[derive(Debug, Serialize, Deserialize)]
pub struct PlanArgs {
    pub repo: RepoPath,
    pub viewer: Viewer,
    pub id: String,
}

/// `apply_plan`: opens a plan's issues, each blocked by the ones it depends
/// on. Members only, and once. Returns `Outcome<Plan>`, its issues now
/// carrying their numbers.
#[derive(Debug, Serialize, Deserialize)]
pub struct ApplyPlanArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub id: String,
    /// Queue every issue for a g1t agent.
    #[serde(default)]
    pub assign: bool,
    /// The positions, counting from 1, of the issues to open. All of them
    /// when absent.
    #[serde(default)]
    pub keep: Option<Vec<u32>>,
}

/// `queue_issue`: asks for a g1t agent to take an issue as soon as it can,
/// or withdraws that. The author or a member may. Returns `Outcome<bool>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct QueueIssueArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
    pub queued: bool,
}

/// `ready_issues`: issues waiting for a g1t agent that can be given one
/// now, in one repository or in all. Called by the runner service. Returns
/// `Vec<ReadyIssue>`.
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadyIssuesArgs {
    #[serde(default)]
    pub repo_id: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ReadyIssue {
    pub repo: RepoPath,
    pub number: u32,
    /// Who queued it, on whose say-so the agent works.
    pub actor: User,
}

/// `update_pull`: changes who a pull request is assigned to and whose
/// review is asked for. Each list given replaces the whole set. Whoever
/// opened it, or a member of the workspace, may. Returns `Outcome<Pull>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdatePullArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
    #[serde(default)]
    pub assignees: Option<Vec<String>>,
    /// May include `g1t-agent`. Asking for its review does not by itself
    /// start one; the runner's `review` does.
    #[serde(default)]
    pub reviewers: Option<Vec<String>>,
}

/// `start_checks`: begins a run of the acceptance checks for a pull request
/// that is ready for review. Called by the runner service, which starts the
/// sandbox. Returns `Outcome<CheckJob>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartChecksArgs {
    pub pull_id: String,
}

/// What a sandbox needs to carry out a check run.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckJob {
    pub run_id: String,
    /// Lets the sandbox, and nothing else, report this run's results.
    pub token: String,
    pub commands: Vec<String>,
    /// The repository holding the commit: the fork, or the repository itself.
    pub source: RepoPath,
    pub commit: String,
    /// Who opened the pull request, and so can read its source.
    pub author: User,
    /// Username of whoever wrote the checks: the issue's author.
    pub requested_by: String,
    pub repo: RepoPath,
    pub number: u32,
}

/// `report_checks`: what a sandbox says about its run. With no results and
/// no error it has started. `skip` forgets the run, for one that will not
/// be carried out. Returns `Outcome<CheckRun>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportChecksArgs {
    pub run_id: String,
    pub token: String,
    #[serde(default)]
    pub results: Vec<CheckResult>,
    #[serde(default)]
    pub error: Option<String>,
    #[serde(default)]
    pub skip: bool,
}

/// A pull request in progress, with where it lives.
#[derive(Debug, Serialize, Deserialize)]
pub struct ActivePull {
    pub pull: Pull,
    pub issue: Option<Issue>,
    /// Where it stands, for one g1t is seeing through.
    #[serde(default)]
    pub lifecycle: Option<Lifecycle>,
}

/// `list_active_pulls`: drafts and open pull requests the viewer started,
/// most recently active first. Returns `Vec<ActivePull>`. Also
/// `list_assigned_issues`: open issues assigned to the viewer, most
/// recently changed first. Returns `Vec<Issue>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct ViewerArgs {
    pub viewer: Viewer,
}

/// `append_session`. Returns `Outcome<Appended>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AppendSessionArgs {
    pub actor: User,
    pub repo: RepoPath,
    pub number: u32,
    pub entries: Vec<NewSessionEntry>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Appended {
    pub count: u32,
}

/// `start_review`: begins a review of a pull request by a g1t agent. Called
/// by the runner service, which starts the sandbox. Returns
/// `Outcome<ReviewJob>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartReviewArgs {
    pub pull_id: String,
}

/// What a sandbox needs to review a pull request.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReviewJob {
    pub run_id: String,
    /// Lets the sandbox, and nothing else, report this review.
    pub token: String,
    /// The repository holding the commit: the fork, or the repository itself.
    pub source: RepoPath,
    pub commit: String,
    pub repo: RepoPath,
    pub default_branch: String,
    pub number: u32,
    pub title: String,
    pub description: String,
    /// The issue the pull request is for, which says what it should achieve.
    pub issue: Option<Issue>,
    /// Who opened the pull request, and so can read its source.
    pub author: User,
}

/// A comment on one line, as a reviewing agent reports it.
#[derive(Debug, Serialize, Deserialize)]
pub struct ReviewComment {
    pub path: String,
    #[serde(default)]
    pub line: u32,
    pub body: String,
}

/// `report_review`: the review a sandbox's agent wrote, or why it could not
/// write one. Returns `Outcome<bool>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportReviewArgs {
    pub run_id: String,
    pub token: String,
    #[serde(default)]
    pub verdict: Option<Verdict>,
    #[serde(default)]
    pub body: String,
    #[serde(default)]
    pub comments: Vec<ReviewComment>,
    /// The model that wrote it, by its public name.
    #[serde(default)]
    pub model: Option<String>,
    #[serde(default)]
    pub error: Option<String>,
}

/// Lowercases, trims and de-duplicates labels, dropping empty ones.
/// Returns `None` if there are too many or one is too long.
pub fn normalize_labels(labels: &[String]) -> Option<Vec<String>> {
    const MAX_LABELS: usize = 10;
    const MAX_LABEL_CHARS: usize = 40;
    let mut normalized: Vec<String> = Vec::new();
    for label in labels {
        let label = label
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ")
            .to_lowercase();
        if label.is_empty() || normalized.contains(&label) {
            continue;
        }
        if label.chars().count() > MAX_LABEL_CHARS {
            return None;
        }
        normalized.push(label);
    }
    (normalized.len() <= MAX_LABELS).then_some(normalized)
}

#[cfg(test)]
mod tests {
    use super::normalize_labels;

    fn labels(names: &[&str]) -> Vec<String> {
        names.iter().map(|name| (*name).to_owned()).collect()
    }

    #[test]
    fn labels_are_lowercased_trimmed_and_unique() {
        assert_eq!(
            normalize_labels(&labels(&[" Bug ", "bug", "", "Good  First Issue"])),
            Some(labels(&["bug", "good first issue"]))
        );
    }

    #[test]
    fn too_long_or_too_many_labels_are_refused() {
        assert_eq!(normalize_labels(&["x".repeat(41)]), None);
        let many: Vec<String> = (0..11).map(|i| format!("label-{i}")).collect();
        assert_eq!(normalize_labels(&many), None);
    }
}


// --- Merge queue ----------------------------------------------------------

/// Where a pull request in a merge queue stands.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QueueState {
    /// Waiting for its turn to be tested.
    Waiting,
    /// Its combined state is being built and checked.
    Testing,
    /// Its combined state passed; it lands once everything ahead has.
    Passed,
    /// Its combined state failed, or would not merge. It left the queue.
    Failed,
    /// On the default branch.
    Landed,
    /// Taken out of the queue by a person, or closed.
    Removed,
}

impl QueueState {
    pub fn as_str(self) -> &'static str {
        match self {
            QueueState::Waiting => "waiting",
            QueueState::Testing => "testing",
            QueueState::Passed => "passed",
            QueueState::Failed => "failed",
            QueueState::Landed => "landed",
            QueueState::Removed => "removed",
        }
    }

    /// Still in the queue.
    pub fn is_active(self) -> bool {
        matches!(
            self,
            QueueState::Waiting | QueueState::Testing | QueueState::Passed
        )
    }
}

/// One pull request's place in a merge queue.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueueEntry {
    pub id: String,
    pub number: u32,
    pub title: String,
    /// Who or what made the pull request, e.g. `g1t-agent`.
    pub agent: String,
    pub state: QueueState,
    /// The pull requests merged ahead of it in the state being tested, in
    /// queue order. Empty when it was tested on the default branch alone.
    pub ahead: Vec<u32>,
    /// The default branch's commit the tested state was built on.
    pub base_commit: Option<String>,
    /// The tested state: the default branch with everything ahead and this.
    pub combined_commit: Option<String>,
    /// Why it failed: a merge conflict or what could not be run.
    pub error: Option<String>,
    /// The checks run against the tested state.
    pub results: Vec<CheckResult>,
    /// Username of whoever merged it into the queue: a person, or `g1t`.
    pub enqueued_by: String,
    /// RFC 3339.
    pub created_at: String,
    /// RFC 3339. When it landed or left.
    pub finished_at: Option<String>,
}

/// A repository's merge queue: what is in it, in order, and what recently
/// left it.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueueView {
    /// Whether the repository merges through the queue.
    pub enabled: bool,
    pub active: Vec<QueueEntry>,
    /// Newest first.
    pub recent: Vec<QueueEntry>,
}

/// `queue`: a repository's merge queue. Returns `Outcome<QueueView>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct QueueArgs {
    pub repo: RepoPath,
    pub viewer: Viewer,
}

/// `queue_build`: the next batch to test for a repository, if nothing is
/// being tested now. Returns `Vec<QueueJob>`, one per entry, each testing
/// the default branch with that entry and everything ahead of it.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueueBuildArgs {
    pub repo_id: String,
}

/// One pull request in a state being tested: where its change is.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueueStackItem {
    pub number: u32,
    pub title: String,
    /// The repository holding the change: its fork, or the repository.
    pub source: RepoPath,
    /// The branch of `source` holding it.
    pub branch: String,
    pub commit: String,
}

/// What a sandbox needs to build and check one combined state.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QueueJob {
    pub entry_id: String,
    /// Lets the sandbox, and nothing else, report this state's result.
    pub token: String,
    pub repo: RepoPath,
    pub default_branch: String,
    /// The default branch's commit to build on.
    pub base_commit: String,
    /// Where to push the tested state, in the repository itself.
    pub branch: String,
    /// The pull requests to merge in, in order; the last is the entry.
    pub stack: Vec<QueueStackItem>,
    /// Every acceptance check of every pull request in the stack.
    pub checks: Vec<String>,
    /// The checks of issues already completed: the default branch's
    /// contract. One that fails on the base alone is not held against the
    /// entry.
    #[serde(default)]
    pub contract_checks: Vec<String>,
    /// Who the sandbox acts as: a member who can push the tested state.
    pub actor: User,
}

/// `report_queue`: a sandbox's result for one combined state. Returns
/// `Outcome<QueueState>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReportQueueArgs {
    pub entry_id: String,
    pub token: String,
    #[serde(default)]
    pub combined_commit: Option<String>,
    #[serde(default)]
    pub results: Vec<CheckResult>,
    /// Set when the state could not be built or checked.
    #[serde(default)]
    pub error: Option<String>,
    /// For a merge conflict: the pull request whose change it collided with.
    #[serde(default)]
    pub conflict_with: Option<u32>,
    /// For a merge conflict: the files that conflicted.
    #[serde(default)]
    pub conflicts: Vec<String>,
}


/// `locate_pull`: where a pull request lives, by its id, for a tool that
/// knows only the fork it is working in (`g1t.sh/pulls/<id>`). Returns
/// `Outcome<LocatedPull>`; not found for anyone who cannot see it.
#[derive(Debug, Serialize, Deserialize)]
pub struct LocatePullArgs {
    pub id: String,
    pub viewer: Viewer,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocatedPull {
    pub repo: RepoPath,
    pub number: u32,
    pub title: String,
    pub status: PullStatus,
}

// --- A person's work -------------------------------------------------------

/// Issues or pull requests, on a person's profile.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuthoredKind {
    Issue,
    Pull,
}

/// The state filter on a person's work. `Closed` takes in merged pull
/// requests too; `Merged` is only those.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuthoredState {
    Open,
    Closed,
    Merged,
}

/// How a person's work is ordered.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuthoredSort {
    /// Newest first.
    #[default]
    Created,
    /// Most recently changed first.
    Updated,
    /// Oldest first.
    Oldest,
}

/// The most items one `by_author` page holds.
pub const AUTHORED_PAGE: u32 = 25;

/// `by_author`: the issues and pull requests a person opened, only on
/// repositories `viewer` may read, so a private title never reaches anyone
/// who could not open it. Returns `Outcome<Authored>`; not found for an
/// account that does not exist.
#[derive(Debug, Serialize, Deserialize)]
pub struct ByAuthorArgs {
    pub username: String,
    pub viewer: Viewer,
    #[serde(default)]
    pub kind: Option<AuthoredKind>,
    #[serde(default)]
    pub state: Option<AuthoredState>,
    /// Only work on this repository: `namespace/name`.
    #[serde(default)]
    pub repo: Option<String>,
    #[serde(default)]
    pub sort: AuthoredSort,
    /// The `next` of the page before, to read on from there.
    #[serde(default)]
    pub before: Option<String>,
    /// At most [`AUTHORED_PAGE`]; that when absent.
    #[serde(default)]
    pub limit: Option<u32>,
}

/// One issue or pull request a person opened.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthoredItem {
    pub kind: AuthoredKind,
    pub repo: RepoPath,
    pub number: u32,
    pub title: String,
    /// Open or closed; a merged pull request is closed.
    pub state: State,
    /// A pull request's own status.
    pub status: Option<PullStatus>,
    /// Why an issue was closed.
    pub reason: Option<IssueReason>,
    pub draft: bool,
    pub merged: bool,
    /// RFC 3339.
    pub created_at: String,
    /// RFC 3339.
    pub updated_at: String,
    /// When a pull request was merged. RFC 3339.
    pub merged_at: Option<String>,
}

/// What a person has done, as far as the viewer may see.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthoredCounts {
    pub pulls_merged: u32,
    pub pulls_open: u32,
    pub pulls: u32,
    pub issues: u32,
    pub issues_open: u32,
}

/// A repository a person has opened work on, with how much.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthoredRepo {
    pub repo: RepoPath,
    pub count: u32,
}

/// A page of a person's work.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Authored {
    pub items: Vec<AuthoredItem>,
    /// Pass as `before` for the next page; null on the last.
    pub next: Option<String>,
    /// Over every repository the viewer may read, whatever the filters.
    pub counts: AuthoredCounts,
    /// Those repositories, most work first.
    pub repos: Vec<AuthoredRepo>,
}