Skip to main content

gwm/
json_api.rs

1//! Stable, machine-readable JSON surface shared by the `--format=json`
2//! CLI flags (issue #38, phase 1) and the daemon's JSON-RPC methods
3//! (phase 2).
4//!
5//! The DTOs here are deliberately decoupled from the internal
6//! [`crate::worktree::WorktreeInfo`] / [`crate::doctor::DoctorReport`]
7//! types. Those structs carry TUI-runtime baggage (loaded GitHub issue /
8//! PR state, cached branch age as a `Duration`, the `BranchLink` graph)
9//! whose shape churns as the TUI evolves. Pinning the documented schema
10//! (see `docs/schema/`) to a dedicated set of `Serialize` DTOs means a
11//! refactor of `WorktreeInfo` can't silently break a downstream editor
12//! plugin. Conversions are one-directional (`From<&Internal>`); the JSON
13//! surface is output-only.
14//!
15//! Key convention: `snake_case`, matching the hand-built
16//! `print_status_json` in [`crate::cli`].
17
18use crate::doctor::{CheckStatus, DoctorReport};
19use crate::error::Result;
20use crate::worktree::{self, BranchStatus, WorktreeInfo};
21use serde::{Deserialize, Serialize};
22
23/// Working-tree + upstream status, the stable projection of
24/// [`BranchStatus`].
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct JsonStatus {
27  pub is_dirty: bool,
28  pub has_upstream: bool,
29  pub ahead: usize,
30  pub behind: usize,
31  /// Status couldn't be computed (detached HEAD, unborn branch).
32  pub unknown: bool,
33}
34
35impl From<&BranchStatus> for JsonStatus {
36  fn from(s: &BranchStatus) -> Self {
37    Self {
38      is_dirty: s.is_dirty,
39      has_upstream: s.has_upstream,
40      ahead: s.ahead,
41      behind: s.behind,
42      unknown: s.unknown,
43    }
44  }
45}
46
47/// One worktree as exposed to scripting / editor integrations. Mirrors
48/// the columns of `gwm list` plus the machine-only fields a consumer
49/// needs (absolute `path`, raw `age_seconds`, linked issue/PR numbers).
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct JsonWorktree {
52  /// Display name — the basename of the worktree directory.
53  pub name: String,
54  /// Internal git worktree id (`.git/worktrees/<id>`); diverges from
55  /// `name` after a `git worktree move`.
56  pub id: String,
57  /// Absolute path to the worktree working directory.
58  pub path: String,
59  pub branch: Option<String>,
60  /// Full HEAD commit oid (40-char hex), when resolvable. A machine
61  /// consumer gets the exact oid for comparison; truncate client-side if a
62  /// short form is wanted.
63  pub head: Option<String>,
64  pub is_main: bool,
65  pub is_locked: bool,
66  pub is_prunable: bool,
67  pub status: JsonStatus,
68  /// Branch age relative to the trunk baseline, in whole seconds.
69  /// `null` for trunk branches and unresolvable repos.
70  pub age_seconds: Option<u64>,
71  /// Linked issue number (branch-name inferred or explicit), if any.
72  pub issue: Option<u64>,
73  /// Linked PR number (inferred, explicit, or auto-detected), if any.
74  pub pr: Option<u64>,
75}
76
77impl From<&WorktreeInfo> for JsonWorktree {
78  fn from(w: &WorktreeInfo) -> Self {
79    Self {
80      name: w.name.clone(),
81      id: w.id.clone(),
82      path: w.path.to_string_lossy().into_owned(),
83      branch: w.branch.clone(),
84      head: w.head.clone(),
85      is_main: w.is_main,
86      is_locked: w.is_locked,
87      is_prunable: w.is_prunable,
88      status: JsonStatus::from(&w.status),
89      age_seconds: w.age.map(|d| d.as_secs()),
90      issue: w.link.issue,
91      pr: w.link.pr,
92    }
93  }
94}
95
96/// The `{ name, path, branch }` triple returned by `gwm path --format=json`.
97#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
98pub struct JsonPath {
99  pub name: String,
100  pub path: String,
101  pub branch: Option<String>,
102}
103
104impl From<&WorktreeInfo> for JsonPath {
105  fn from(w: &WorktreeInfo) -> Self {
106    Self {
107      name: w.name.clone(),
108      path: w.path.to_string_lossy().into_owned(),
109      branch: w.branch.clone(),
110    }
111  }
112}
113
114/// Stable lowercase string for a [`CheckStatus`], used as the `status`
115/// field of [`JsonCheck`] and the `severity` of [`JsonDoctorReport`].
116pub fn check_status_str(status: &CheckStatus) -> &'static str {
117  match status {
118    CheckStatus::Ok => "ok",
119    CheckStatus::Warning => "warning",
120    CheckStatus::Failed => "failed",
121  }
122}
123
124/// One diagnostic check, the stable projection of [`crate::doctor::Check`].
125#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
126pub struct JsonCheck {
127  pub name: String,
128  /// `"ok"`, `"warning"`, or `"failed"`.
129  pub status: String,
130  pub detail: String,
131  pub fix_hint: Option<String>,
132}
133
134/// A full doctor run, carrying the per-check list plus the aggregate
135/// `severity` and the process `exit_code` (`0`/`1`/`2`) so a consumer
136/// doesn't have to re-derive them.
137#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
138pub struct JsonDoctorReport {
139  pub checks: Vec<JsonCheck>,
140  /// Highest severity present: `"ok"`, `"warning"`, or `"failed"`.
141  pub severity: String,
142  pub exit_code: i32,
143}
144
145impl From<&DoctorReport> for JsonDoctorReport {
146  fn from(r: &DoctorReport) -> Self {
147    Self {
148      checks: r
149        .checks
150        .iter()
151        .map(|c| JsonCheck {
152          name: c.name.clone(),
153          status: check_status_str(&c.status).to_string(),
154          detail: c.detail.clone(),
155          fix_hint: c.fix_hint.clone(),
156        })
157        .collect(),
158      severity: check_status_str(&r.severity()).to_string(),
159      exit_code: r.exit_code(),
160    }
161  }
162}
163
164/// Build the stable JSON worktree list for `repo`. Shared by
165/// `gwm list --format=json` and the daemon's `list` RPC method so both
166/// surfaces stay byte-identical.
167pub fn worktrees(repo: &git2::Repository) -> Result<Vec<JsonWorktree>> {
168  Ok(worktree::list(repo)?.iter().map(JsonWorktree::from).collect())
169}