//! 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 IssueResponse {
pub issue: 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>,
}
// ── badges ───────────────────────────────────────────────────────────
/// `GET /repos/:owner/:name/badges`: every badge the repository has,
/// each with the Markdown and HTML that embed it.
#[derive(Debug, Clone, Deserialize)]
pub struct BadgesResponse {
pub repository: String,
/// The badges a README block starts with.
#[serde(default)]
pub defaults: Vec<String>,
#[serde(default)]
pub badges: Vec<Badge>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Badge {
/// `ci`, `workflow:audit`, `package:cargo:gitgud-cli`.
pub key: String,
pub group: String,
pub title: String,
#[serde(default)]
pub hint: Option<String>,
#[serde(default)]
pub label: Option<String>,
/// What it says now; `None` for the GG mark.
#[serde(default)]
pub value: Option<String>,
/// success, warning, error, info or muted.
#[serde(default)]
pub status: Option<String>,
pub url: String,
pub markdown: String,
pub html: String,
}
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
20.5 KiB · text
History
8e00315