Skip to main content

lds_git/
output.rs

1//! Typed return shapes for [`GitModule`] methods.
2//!
3//! Every public method on [`GitModule`] returns one of these structs (wrapped
4//! in [`anyhow::Result`]) instead of a `format!`-shaped `String`. The lds MCP
5//! layer then serialises the struct with `serde_json::to_string_pretty` so
6//! callers receive a stable JSON shape and can access fields directly.
7//!
8//! Keep this module field-stable: any rename / type change is a wire breakage
9//! and must be paired with a SemVer bump on the lds-git crate and the lds MCP
10//! tool description.
11
12use std::path::PathBuf;
13
14use serde::{Deserialize, Serialize};
15
16// ---------------------------------------------------------------------------
17// Read
18// ---------------------------------------------------------------------------
19
20/// One entry from `git status` (a single path with its current state).
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct EntryStatus {
23    pub path: PathBuf,
24    pub kind: StatusKind,
25}
26
27#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "snake_case")]
29pub enum StatusKind {
30    New,
31    Modified,
32    Deleted,
33    Renamed,
34    Typechange,
35    Conflicted,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct StatusOutput {
40    /// Current branch name (HEAD short name), `None` on detached HEAD.
41    pub branch: Option<String>,
42    /// HEAD commit sha (full 40-char hex), `None` for an unborn HEAD.
43    pub head_sha: Option<String>,
44    /// Entries with staged changes (index vs HEAD).
45    pub staged: Vec<EntryStatus>,
46    /// Entries with unstaged changes (worktree vs index).
47    pub unstaged: Vec<EntryStatus>,
48    /// Paths git reports as untracked (worktree-only files).
49    pub untracked: Vec<PathBuf>,
50    /// `true` when staged + unstaged + untracked are all empty.
51    pub clean: bool,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub struct CommitEntry {
56    /// Full 40-char commit sha.
57    pub sha: String,
58    /// First 7 chars of `sha` (git's conventional short form).
59    pub short_sha: String,
60    /// First line of the commit message.
61    pub summary: String,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65pub struct LogOutput {
66    pub commits: Vec<CommitEntry>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70pub struct DiffOutput {
71    /// `true` when the diff is `git diff --cached` (HEAD vs index);
72    /// `false` when it's `git diff` (index vs worktree).
73    pub staged: bool,
74    /// Unified diff patch, byte-for-byte equivalent to `git diff [--cached]`.
75    pub patch: String,
76    /// Number of distinct files touched by the diff.
77    pub file_count: usize,
78}
79
80// ---------------------------------------------------------------------------
81// Worktree
82// ---------------------------------------------------------------------------
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85pub struct WorktreeEntry {
86    pub path: PathBuf,
87    /// HEAD commit sha of this worktree (full 40-char hex), if any.
88    pub head: Option<String>,
89    /// Checked-out branch (short ref name), `None` on detached HEAD.
90    pub branch: Option<String>,
91    /// `true` when this worktree was created by the current session.
92    pub owned: bool,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96pub struct WorktreeListOutput {
97    pub worktrees: Vec<WorktreeEntry>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101pub struct WorktreeStateOutput {
102    /// Branch name (short ref).
103    pub branch: String,
104    /// Upstream tracking branch (e.g. `origin/main`), `None` when unset.
105    pub tracking: Option<String>,
106    pub ahead: u32,
107    pub behind: u32,
108    /// Number of uncommitted changes (staged + unstaged + untracked).
109    pub uncommitted: usize,
110    pub clean: bool,
111    /// `true` when `behind == 0` (no incoming work to integrate).
112    pub sync: bool,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116pub struct WorktreeAddOutput {
117    pub path: PathBuf,
118    pub branch: String,
119    pub session: String,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123pub struct WorktreeRemoveOutput {
124    pub path: PathBuf,
125}
126
127// ---------------------------------------------------------------------------
128// Remote
129// ---------------------------------------------------------------------------
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132pub struct FetchOutput {
133    pub remote: String,
134    pub refspec: Option<String>,
135    /// `true` when `--prune` was requested.
136    pub prune: bool,
137    /// Raw transport output (stdout merged with stderr) for diagnostics.
138    pub raw: String,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
142pub struct RemoteEntry {
143    pub name: String,
144    pub fetch_url: Option<String>,
145    pub push_url: Option<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149pub struct RemoteListOutput {
150    pub remotes: Vec<RemoteEntry>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct BranchStatusOutput {
155    pub branch: String,
156    pub base: String,
157    pub ahead: u32,
158    pub behind: u32,
159    pub up_to_date: bool,
160    /// Merge-base sha (full 40-char hex), `None` when no common ancestor.
161    pub common_ancestor: Option<String>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165pub struct UnpushedCommitsOutput {
166    pub branch: String,
167    pub remote: String,
168    /// Sha of the remote tracking ref's tip (`<remote>/<branch>`).
169    pub remote_head: String,
170    pub count: usize,
171    pub commits: Vec<CommitEntry>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
175pub struct IsPushedOutput {
176    pub commit: String,
177    pub remote: String,
178    pub pushed: bool,
179    /// Remote refs that contain this commit (e.g. `refs/remotes/origin/main`).
180    pub refs: Vec<String>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184pub struct TagPushedOutput {
185    pub tag: String,
186    pub remote: String,
187    pub pushed: bool,
188    /// Raw lines from `git ls-remote --tags <remote> refs/tags/<tag>`.
189    pub remote_refs: Vec<String>,
190}
191
192// ---------------------------------------------------------------------------
193// Write
194// ---------------------------------------------------------------------------
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
197pub struct CommitOutput {
198    pub sha: String,
199    pub short_sha: String,
200    pub message: String,
201    pub files_changed: usize,
202}
203
204/// How [`GitModule::commit`] handles staged paths outside the `only` list.
205///
206/// Only consulted when `only` is `Some(non_empty)`. When `only` is `None` /
207/// empty, `commit` still stages every change via `git add -A` and this enum
208/// is ignored.
209#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
210#[serde(rename_all = "snake_case")]
211pub enum OtherStagedMode {
212    /// Fail (state unchanged) when the index carries paths outside `only`.
213    /// The safe default — protects against silently sweeping unrelated work.
214    #[default]
215    Stop,
216    /// Unstage the other paths, commit `only`, then re-stage them. The
217    /// commit ends up containing exactly the `only` paths; the pre-existing
218    /// staged work stays in the index afterwards.
219    Restage,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223pub struct MergeOutput {
224    pub branch: String,
225    pub into_branch: String,
226    /// Merge commit sha (full 40-char hex).
227    pub sha: String,
228    pub short_sha: String,
229    /// Raw `git merge` output for diagnostics (fast-forward note, conflict tip).
230    pub raw: String,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
234pub struct BranchDeleteOutput {
235    pub branch: String,
236}
237
238// ---------------------------------------------------------------------------
239// Reset
240// ---------------------------------------------------------------------------
241
242#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
243#[serde(rename_all = "snake_case")]
244pub enum ResetMode {
245    Soft,
246    Mixed,
247    Hard,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
251pub struct ResetOutput {
252    pub mode: ResetMode,
253    /// Revspec / sha that was passed in as the reset target.
254    pub target: String,
255    /// HEAD sha before the reset (full 40-char hex).
256    pub previous_head: String,
257    /// HEAD sha after the reset (full 40-char hex).
258    pub current_head: String,
259}
260
261// ---------------------------------------------------------------------------
262// Session
263// ---------------------------------------------------------------------------
264
265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
266pub struct SessionReleaseOutput {
267    /// Worktree paths whose ownership this session adopted.
268    pub adopted_worktrees: Vec<PathBuf>,
269    /// Branches whose ownership this session adopted.
270    pub adopted_branches: Vec<String>,
271}