19.3 KiB · text History d859987
//! Wire types for `/api/mobile/v1`.
//!
//! Field names mirror the JSON exactly (see `GitGudWeb.Api.Mobile.JSON`
//! on the server), so a field that stops matching is obvious on both
//! sides. Everything the server can leave out is `Option` or defaulted:
//! the CLI has to keep working against a forge a few commits behind it.

// The wire shape is the point: fields the CLI doesn't render yet are
// still what the server sends, and dropping them would mean rediscovering
// them the next time a command needs one.
#![allow(dead_code)]

use serde::{Deserialize, Serialize};

fn queued() -> String {
    "queued".into()
}

fn pending() -> String {
    "pending".into()
}

fn first_attempt() -> i64 {
    1
}

fn open() -> String {
    "open".into()
}

// ── identity ─────────────────────────────────────────────────────────

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ApiUser {
    pub handle: Option<String>,
    pub display_name: Option<String>,
    pub email: Option<String>,
}

impl ApiUser {
    /// Best available label: display name, else handle, else "someone".
    pub fn label(&self) -> &str {
        self.display_name
            .as_deref()
            .filter(|s| !s.trim().is_empty())
            .or(self.handle.as_deref())
            .unwrap_or("someone")
    }
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct Instance {
    pub url: Option<String>,
    pub name: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct MeResponse {
    pub user: ApiUser,
    #[serde(default)]
    pub scopes: Vec<String>,
    #[serde(default)]
    pub mobile: bool,
    pub instance: Option<Instance>,
}

// ── repositories ─────────────────────────────────────────────────────

#[derive(Debug, Clone, Deserialize)]
pub struct Namespace {
    pub handle: String,
    pub kind: String,
    pub name: Option<String>,
    pub description: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Repository {
    pub id: i64,
    pub name: String,
    pub full_name: Option<String>,
    pub owner: Option<String>,
    pub owner_kind: Option<String>,
    pub description: Option<String>,
    pub visibility: Option<String>,
    pub default_branch: Option<String>,
    #[serde(default)]
    pub archived: bool,
    pub pushed_at: Option<String>,
    pub permission: Option<String>,
    pub ci_status: Option<String>,
    pub html_url: Option<String>,
}

impl Repository {
    pub fn slug(&self) -> String {
        self.full_name.clone().unwrap_or_else(|| match &self.owner {
            Some(owner) => format!("{owner}/{}", self.name),
            None => self.name.clone(),
        })
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct ReposResponse {
    #[serde(default)]
    pub namespaces: Vec<Namespace>,
    #[serde(default)]
    pub repositories: Vec<Repository>,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct RepoCounts {
    #[serde(default)]
    pub open_issues: i64,
    #[serde(default)]
    pub open_pulls: i64,
    #[serde(default)]
    pub wiki_pages: i64,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct RepoPermissions {
    #[serde(default)]
    pub ci: bool,
    #[serde(default)]
    pub issues: bool,
    #[serde(default)]
    pub pulls: bool,
    #[serde(default)]
    pub merge: bool,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RepoDetailResponse {
    pub repository: Repository,
    #[serde(default)]
    pub counts: RepoCounts,
    pub latest_run: Option<Run>,
    #[serde(default)]
    pub permissions: RepoPermissions,
}

// ── CI ───────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Deserialize)]
pub struct Run {
    pub number: i64,
    pub workflow_name: Option<String>,
    pub workflow_path: Option<String>,
    pub trigger: Option<String>,
    #[serde(default = "queued")]
    pub status: String,
    pub conclusion: Option<String>,
    pub head_sha: Option<String>,
    pub head_ref: Option<String>,
    pub started_at: Option<String>,
    pub completed_at: Option<String>,
    pub inserted_at: Option<String>,
    pub triggered_by: Option<ApiUser>,
    /// The pull request a `pull_request` / `pull_request_target` run is for.
    pub pull_request_number: Option<i64>,
    /// A fork pull request's run: no secrets, a read-only token.
    #[serde(default)]
    pub fork: bool,
    /// A fork run nobody with write access has approved yet. Its jobs are
    /// `awaiting_approval` meanwhile.
    #[serde(default)]
    pub awaiting_approval: bool,
}

impl Run {
    /// What the run's status reads as. A run waiting for approval is
    /// `queued` on the server, but "queued" would promise a runner is
    /// coming — nothing happens until someone approves it.
    pub fn display_status(&self) -> &str {
        if self.awaiting_approval && matches!(self.status.as_str(), "queued" | "running") {
            "awaiting_approval"
        } else {
            crate::output::effective_status(&self.status, self.conclusion.as_deref())
        }
    }

    /// `refs/heads/main` reads as `main`.
    pub fn short_ref(&self) -> Option<&str> {
        self.head_ref.as_deref().map(|r| {
            r.strip_prefix("refs/heads/")
                .or_else(|| r.strip_prefix("refs/tags/"))
                .unwrap_or(r)
        })
    }

    pub fn short_sha(&self) -> Option<&str> {
        self.head_sha.as_deref().map(|s| &s[..s.len().min(7)])
    }

    pub fn workflow_label(&self) -> &str {
        self.workflow_name
            .as_deref()
            .filter(|s| !s.is_empty())
            .or(self.workflow_path.as_deref())
            .unwrap_or("workflow")
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct Step {
    pub id: i64,
    #[serde(default)]
    pub number: i64,
    pub name: Option<String>,
    #[serde(default = "pending")]
    pub status: String,
    pub conclusion: Option<String>,
    pub started_at: Option<String>,
    pub completed_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Job {
    pub id: i64,
    pub job_id: Option<String>,
    pub name: Option<String>,
    #[serde(default = "queued")]
    pub status: String,
    pub conclusion: Option<String>,
    #[serde(default)]
    pub needs: Vec<String>,
    /// Why it's waiting, skipped or cancelled, or why its matrix couldn't
    /// expand — worded for display.
    pub status_reason: Option<String>,
    /// `github.run_attempt`: 1, plus one per re-run of the job.
    #[serde(default = "first_attempt")]
    pub attempt: i64,
    /// Cancelled because a newer run or job in its concurrency group
    /// replaced it: news, but not bad news.
    #[serde(default)]
    pub superseded: bool,
    /// A dynamic matrix that hasn't expanded yet. When it does, this row
    /// is deleted and its legs appear with new ids — so nothing should
    /// hold on to this one's id.
    #[serde(default)]
    pub placeholder: bool,
    pub started_at: Option<String>,
    pub completed_at: Option<String>,
    #[serde(default)]
    pub steps: Vec<Step>,
}

impl Job {
    /// `superseded` for a job a newer one replaced, else its effective
    /// status.
    pub fn display_status(&self) -> &str {
        if self.superseded {
            "superseded"
        } else {
            crate::output::effective_status(&self.status, self.conclusion.as_deref())
        }
    }

    /// What to say under the job: the server's reason, or for a
    /// placeholder with nothing to explain yet, what it is.
    pub fn reason(&self) -> Option<String> {
        if let Some(reason) = self
            .status_reason
            .as_deref()
            .filter(|r| !r.trim().is_empty())
        {
            return Some(reason.to_string());
        }
        (self.placeholder && !self.needs.is_empty() && self.display_status() == "blocked")
            .then(|| format!("Matrix expands when {} finish", self.needs.join(", ")))
    }

    pub fn label(&self) -> String {
        self.name
            .clone()
            .or_else(|| self.job_id.clone())
            .unwrap_or_else(|| format!("job {}", self.id))
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct Artifact {
    pub id: i64,
    pub name: Option<String>,
    pub size: Option<i64>,
    pub inserted_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RunsResponse {
    #[serde(default)]
    pub runs: Vec<Run>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RunDetailResponse {
    pub run: Run,
    #[serde(default)]
    pub jobs: Vec<Job>,
    #[serde(default)]
    pub artifacts: Vec<Artifact>,
    #[serde(default)]
    pub can_manage: bool,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LogLine {
    pub seq: i64,
    #[serde(default)]
    pub body: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct StepLogs {
    pub step: Step,
    #[serde(default)]
    pub lines: Vec<LogLine>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LogsResponse {
    pub job: Job,
    #[serde(default)]
    pub steps: Vec<StepLogs>,
    #[serde(default)]
    pub truncated: bool,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RunResponse {
    pub run: Run,
    #[serde(default)]
    pub jobs: Vec<Job>,
}

/// One input of a `workflow_dispatch`, as the workflow declares it.
/// `type` is GitHub's: `string`, `boolean`, `choice`, `number` or
/// `environment` (a plain string here — the forge has no environments).
#[derive(Debug, Clone, Deserialize)]
pub struct DispatchInput {
    pub name: String,
    #[serde(rename = "type", default = "string_type")]
    pub kind: String,
    pub description: Option<String>,
    #[serde(default)]
    pub required: bool,
    pub default: Option<String>,
    #[serde(default)]
    pub options: Vec<String>,
}

fn string_type() -> String {
    "string".into()
}

#[derive(Debug, Clone, Deserialize)]
pub struct DispatchWorkflow {
    pub path: String,
    pub name: Option<String>,
    #[serde(default)]
    pub inputs: Vec<DispatchInput>,
}

impl DispatchWorkflow {
    pub fn label(&self) -> &str {
        self.name
            .as_deref()
            .filter(|s| !s.is_empty())
            .unwrap_or_else(|| self.path.rsplit('/').next().unwrap_or(&self.path))
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct WorkflowsResponse {
    #[serde(default)]
    pub workflows: Vec<DispatchWorkflow>,
    pub default_branch: Option<String>,
    #[serde(default)]
    pub branches: Vec<String>,
    #[serde(default)]
    pub can_dispatch: bool,
}

// ── audit ────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Deserialize)]
pub struct AuditResponse {
    #[serde(default)]
    pub entries: Vec<AuditEntry>,
    /// The id to pass as `before` for the next page; none at the end.
    pub next_before: Option<i64>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct AuditEntry {
    pub id: i64,
    pub action: String,
    /// The phrase the web pages show, without the actor.
    pub summary: String,
    pub actor: AuditActor,
    pub organization: Option<String>,
    pub repository: Option<String>,
    pub target: AuditTarget,
    pub inserted_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct AuditActor {
    /// A handle, a runner's name, "console", a remote actor's URL.
    pub label: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct AuditTarget {
    pub label: Option<String>,
}

// ── issues, pulls, comments ──────────────────────────────────────────

#[derive(Debug, Clone, Deserialize)]
pub struct Label {
    pub name: String,
    pub color: Option<String>,
    /// Only in a repository's label list, not on an issue or pull request.
    pub description: Option<String>,
    /// Comes from the repository's organization (repository label list
    /// only).
    #[serde(default)]
    pub inherited: bool,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LabelsResponse {
    #[serde(default)]
    pub labels: Vec<Label>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LabelResponse {
    pub label: Label,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Issue {
    pub number: i64,
    pub title: Option<String>,
    #[serde(default = "open")]
    pub state: String,
    pub author: Option<ApiUser>,
    #[serde(default)]
    pub labels: Vec<Label>,
    pub body: Option<String>,
    pub body_html: Option<String>,
    pub inserted_at: Option<String>,
    pub updated_at: Option<String>,
    pub closed_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Comment {
    pub id: i64,
    pub author: Option<ApiUser>,
    pub body: Option<String>,
    pub body_html: Option<String>,
    #[serde(default)]
    pub deleted: bool,
    pub edited_at: Option<String>,
    pub inserted_at: Option<String>,
    pub file_path: Option<String>,
    pub line: Option<i64>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct IssuesResponse {
    #[serde(default)]
    pub issues: Vec<Issue>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct IssueDetailResponse {
    pub issue: Issue,
    #[serde(default)]
    pub comments: Vec<Comment>,
    #[serde(default)]
    pub can_comment: bool,
    #[serde(default)]
    pub can_close: bool,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Pull {
    pub number: i64,
    pub title: Option<String>,
    #[serde(default = "open")]
    pub state: String,
    pub author: Option<ApiUser>,
    #[serde(default)]
    pub labels: Vec<Label>,
    pub source_ref: Option<String>,
    pub target_ref: Option<String>,
    pub mergeable: Option<bool>,
    pub body: Option<String>,
    pub body_html: Option<String>,
    pub head_sha: Option<String>,
    pub base_sha: Option<String>,
    pub merge_commit_sha: Option<String>,
    pub merged_by: Option<ApiUser>,
    pub inserted_at: Option<String>,
    pub updated_at: Option<String>,
    pub merged_at: Option<String>,
    pub closed_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct Review {
    pub id: i64,
    pub state: Option<String>,
    pub body: Option<String>,
    pub reviewer: Option<ApiUser>,
    pub inserted_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct ReviewRequest {
    pub reviewer: Option<ApiUser>,
    pub requested_by: Option<ApiUser>,
    pub inserted_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PullsResponse {
    #[serde(default)]
    pub pulls: Vec<Pull>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PullDetailResponse {
    pub pull: Pull,
    #[serde(default)]
    pub comments: Vec<Comment>,
    #[serde(default)]
    pub line_comments: Vec<Comment>,
    #[serde(default)]
    pub reviews: Vec<Review>,
    #[serde(default)]
    pub review_requests: Vec<ReviewRequest>,
    #[serde(default)]
    pub merge_blockers: Vec<String>,
    pub my_review: Option<Review>,
    #[serde(default)]
    pub can_comment: bool,
    #[serde(default)]
    pub can_review: bool,
    #[serde(default)]
    pub can_merge: bool,
    #[serde(default)]
    pub can_override: bool,
    /// A fork's runs on the current head, waiting for someone to approve
    /// them.
    #[serde(default)]
    pub awaiting_approval_runs: Vec<Run>,
    /// Whether the viewer may approve (or reject) them: write access.
    #[serde(default)]
    pub can_approve_runs: bool,
    /// Whether the viewer may edit, close, reopen and label it: its
    /// author, or a repository admin.
    #[serde(default)]
    pub can_edit: bool,
}

// ── wiki ─────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Deserialize)]
pub struct WikiPageMeta {
    pub slug: String,
    pub title: Option<String>,
    pub size: Option<i64>,
    pub sha: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct WikiIndexResponse {
    #[serde(default)]
    pub pages: Vec<WikiPageMeta>,
    #[serde(default)]
    pub initialized: bool,
}

#[derive(Debug, Clone, Deserialize)]
pub struct WikiPageResponse {
    pub slug: String,
    pub title: Option<String>,
    pub body: Option<String>,
    pub body_html: Option<String>,
}

// ── packages ─────────────────────────────────────────────────────────

#[derive(Debug, Clone, Deserialize)]
pub struct PackagesResponse {
    #[serde(default)]
    pub formats: Vec<PackageFormat>,
    /// Every format the forge can serve, so an empty listing can still
    /// point at `gg package usage <format>`.
    #[serde(default)]
    pub available_formats: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PackageFormat {
    pub format: String,
    pub label: String,
    pub summary: Option<String>,
    #[serde(default)]
    pub packages: Vec<PackageSummary>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PackageSummary {
    pub name: String,
    pub format: String,
    pub kind: Option<String>,
    pub latest_version: Option<String>,
    /// The line you'd paste to install it, or none for a format without.
    pub install_line: Option<String>,
    pub updated_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PackageDetail {
    pub name: String,
    pub format: String,
    pub kind: Option<String>,
    pub latest_version: Option<String>,
    #[serde(default)]
    pub versions: Vec<PackageVersion>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PackageVersion {
    pub version: String,
    #[serde(default)]
    pub size: i64,
    pub digest: Option<String>,
    #[serde(default)]
    pub yanked: bool,
    pub yanked_reason: Option<String>,
    pub pushed_at: Option<String>,
    pub install_line: Option<String>,
    #[serde(default)]
    pub files: Vec<PackageFileInfo>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct PackageFileInfo {
    pub filename: String,
    #[serde(default)]
    pub size: i64,
    pub content_type: Option<String>,
    pub sha256: Option<String>,
}

/// Setup instructions for one format. Rendered verbatim — every URL in
/// here is one only the server can know (its own address, the namespace a
/// repository belongs to, the key path), so the client never builds one.
#[derive(Debug, Clone, Deserialize)]
pub struct PackageUsage {
    pub format: String,
    pub label: String,
    pub summary: Option<String>,
    pub repository_url: Option<String>,
    pub namespace_url: Option<String>,
    pub key_url: Option<String>,
    #[serde(default)]
    pub sections: Vec<UsageSection>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct UsageSection {
    pub key: String,
    pub title: String,
    #[serde(default)]
    pub steps: Vec<UsageStep>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct UsageStep {
    pub label: String,
    pub language: Option<String>,
    pub code: String,
}

/// The server's error shape — `{"message": "..."}` on every 4xx, plus
/// `errors` (input name → reason) when a dispatch's inputs don't fit.
#[derive(Debug, Clone, Deserialize)]
pub struct ApiError {
    pub message: Option<String>,
    #[serde(default)]
    pub errors: std::collections::BTreeMap<String, String>,
}