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    /// Commit author in `"Name <email>"` form.
63    pub author: String,
64    /// Commit author time, unix epoch seconds.
65    pub timestamp: i64,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub struct LogOutput {
70    pub commits: Vec<CommitEntry>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74pub struct DiffOutput {
75    /// `true` when the diff is `git diff --cached` (HEAD vs index);
76    /// `false` when it's `git diff` (index vs worktree).
77    pub staged: bool,
78    /// Unified diff patch, byte-for-byte equivalent to `git diff [--cached]`.
79    pub patch: String,
80    /// Number of distinct files touched by the diff.
81    pub file_count: usize,
82}
83
84// ---------------------------------------------------------------------------
85// Worktree
86// ---------------------------------------------------------------------------
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89pub struct WorktreeEntry {
90    pub path: PathBuf,
91    /// HEAD commit sha of this worktree (full 40-char hex), if any.
92    pub head: Option<String>,
93    /// Checked-out branch (short ref name), `None` on detached HEAD.
94    pub branch: Option<String>,
95    /// `true` when this worktree was created by the current session.
96    pub owned: bool,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100pub struct WorktreeListOutput {
101    pub worktrees: Vec<WorktreeEntry>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
105pub struct WorktreeStateOutput {
106    /// Branch name (short ref).
107    pub branch: String,
108    /// Upstream tracking branch (e.g. `origin/main`), `None` when unset.
109    pub tracking: Option<String>,
110    pub ahead: u32,
111    pub behind: u32,
112    /// Number of uncommitted changes (staged + unstaged + untracked).
113    pub uncommitted: usize,
114    pub clean: bool,
115    /// `true` when `behind == 0` (no incoming work to integrate).
116    pub sync: bool,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
120pub struct WorktreeAddOutput {
121    pub path: PathBuf,
122    pub branch: String,
123    pub session: String,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127pub struct WorktreeRemoveOutput {
128    pub path: PathBuf,
129}
130
131// ---------------------------------------------------------------------------
132// Remote
133// ---------------------------------------------------------------------------
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
136pub struct FetchOutput {
137    pub remote: String,
138    pub refspec: Option<String>,
139    /// `true` when `--prune` was requested.
140    pub prune: bool,
141    /// Raw transport output (stdout merged with stderr) for diagnostics.
142    pub raw: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
146pub struct RemoteEntry {
147    pub name: String,
148    pub fetch_url: Option<String>,
149    pub push_url: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
153pub struct RemoteListOutput {
154    pub remotes: Vec<RemoteEntry>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
158pub struct BranchStatusOutput {
159    pub branch: String,
160    pub base: String,
161    pub ahead: u32,
162    pub behind: u32,
163    pub up_to_date: bool,
164    /// Merge-base sha (full 40-char hex), `None` when no common ancestor.
165    pub common_ancestor: Option<String>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
169pub struct UnpushedCommitsOutput {
170    pub branch: String,
171    pub remote: String,
172    /// Sha of the remote tracking ref's tip (`<remote>/<branch>`).
173    pub remote_head: String,
174    pub count: usize,
175    pub commits: Vec<CommitEntry>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
179pub struct IsPushedOutput {
180    pub commit: String,
181    pub remote: String,
182    pub pushed: bool,
183    /// Remote refs that contain this commit (e.g. `refs/remotes/origin/main`).
184    pub refs: Vec<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
188pub struct TagPushedOutput {
189    pub tag: String,
190    pub remote: String,
191    pub pushed: bool,
192    /// Raw lines from `git ls-remote --tags <remote> refs/tags/<tag>`.
193    pub remote_refs: Vec<String>,
194}
195
196// ---------------------------------------------------------------------------
197// Write
198// ---------------------------------------------------------------------------
199
200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
201pub struct CommitOutput {
202    pub sha: String,
203    pub short_sha: String,
204    pub message: String,
205    pub files_changed: usize,
206    /// Dotfile / dot-dir paths that surfaced during this commit call. Every
207    /// change with a `.`-prefixed path component (`.env`,
208    /// `.github/workflows/ci.yml`, `foo/.hidden`) lands here so pre-publish
209    /// eyeballing can catch unintended edits. Tracked entries were still
210    /// committed; untracked-not-in-gitignore entries were skipped (see
211    /// `dotfile_skipped`). `force_dot=true` suppresses this entirely.
212    #[serde(default)]
213    pub dotfile_warnings: Vec<DotfileWarning>,
214    /// Paths dropped from staging by the dotfile safeguard. Populated only
215    /// for the untracked + not-in-gitignore branch (silent-ignored dotfiles
216    /// aren't tracked here — git's default already handles them).
217    #[serde(default)]
218    pub dotfile_skipped: Vec<String>,
219}
220
221/// One dotfile / dot-dir path observed during commit. `tracked=true` means
222/// the change was still committed (with a warn); `tracked=false` means it
223/// was skipped from staging.
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225pub struct DotfileWarning {
226    pub path: String,
227    pub tracked: bool,
228    pub in_gitignore: bool,
229}
230
231/// How [`GitModule::commit`] handles staged paths outside the `only` list.
232///
233/// Only consulted when `only` is `Some(non_empty)`. When `only` is `None` /
234/// empty, `commit` still stages every change via `git add -A` and this enum
235/// is ignored.
236#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
237#[serde(rename_all = "snake_case")]
238pub enum OtherStagedMode {
239    /// Fail (state unchanged) when the index carries paths outside `only`.
240    /// The safe default — protects against silently sweeping unrelated work.
241    #[default]
242    Stop,
243    /// Unstage the other paths, commit `only`, then re-stage them. The
244    /// commit ends up containing exactly the `only` paths; the pre-existing
245    /// staged work stays in the index afterwards.
246    Restage,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
250pub struct MergeOutput {
251    pub branch: String,
252    pub into_branch: String,
253    /// Merge commit sha (full 40-char hex).
254    pub sha: String,
255    pub short_sha: String,
256    /// Raw `git merge` output for diagnostics (fast-forward note, conflict tip).
257    pub raw: String,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
261pub struct BranchDeleteOutput {
262    pub branch: String,
263}
264
265// ---------------------------------------------------------------------------
266// Reset
267// ---------------------------------------------------------------------------
268
269#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
270#[serde(rename_all = "snake_case")]
271pub enum ResetMode {
272    Soft,
273    Mixed,
274    Hard,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
278pub struct ResetOutput {
279    pub mode: ResetMode,
280    /// Revspec / sha that was passed in as the reset target.
281    pub target: String,
282    /// HEAD sha before the reset (full 40-char hex).
283    pub previous_head: String,
284    /// HEAD sha after the reset (full 40-char hex).
285    pub current_head: String,
286}
287
288// ---------------------------------------------------------------------------
289// Stash
290// ---------------------------------------------------------------------------
291
292/// One entry from `git stash list`.
293///
294/// `index` is the position in the stash reflog (`stash@{index}`) at the time
295/// of the call — it shifts whenever an entry is dropped, which is why every
296/// mutating stash method accepts an `expected_sha` to pin the identity.
297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
298pub struct StashEntry {
299    /// Position in the stash reflog (`stash@{index}`).
300    pub index: usize,
301    /// Stash commit sha (full 40-char hex). Stable across index shifts.
302    pub sha: String,
303    /// Reflog message (e.g. `"WIP on main: 1a2b3c4 subject"`).
304    pub message: String,
305    /// `true` when the entry carries untracked files (`git stash push -u`),
306    /// detected via the stash commit's third parent.
307    pub has_untracked: bool,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
311pub struct StashListOutput {
312    /// Entries in reflog order (index 0 == most recent).
313    pub stashes: Vec<StashEntry>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
317pub struct StashShowOutput {
318    pub index: usize,
319    /// Stash commit sha (full 40-char hex).
320    pub sha: String,
321    /// Reflog message of the entry.
322    pub message: String,
323    /// Unified diff of the stashed tracked changes (base commit vs stash).
324    pub patch: String,
325    /// Number of tracked files the entry touches.
326    pub file_count: usize,
327    /// Repo-relative paths (git pathspec form) of the tracked files above.
328    pub files: Vec<String>,
329    /// Repo-relative paths carried as untracked files (`git stash push -u`).
330    /// Empty when `has_untracked` is `false`; no patch is produced for them.
331    pub untracked_paths: Vec<String>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
335pub struct StashApplyOutput {
336    pub index: usize,
337    /// Stash commit sha (full 40-char hex) that was applied.
338    pub sha: String,
339    /// Tracked paths the entry restored into the working tree.
340    pub applied_paths: Vec<String>,
341    /// Untracked paths the entry restored into the working tree.
342    pub restored_untracked: Vec<String>,
343    /// Always `true` — apply never drops the entry. Dropping is
344    /// [`super::GitModule::stash_finalize`]'s job.
345    pub entry_kept: bool,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
349pub struct StashAbortOutput {
350    pub index: usize,
351    /// Stash commit sha (full 40-char hex) whose apply was rolled back.
352    pub sha: String,
353    /// Tracked paths returned to their HEAD state (restored or, when the
354    /// entry added them, removed from the working tree).
355    pub reverted_paths: Vec<String>,
356    /// Untracked paths removed from the working tree.
357    pub removed_untracked: Vec<String>,
358    /// Always `true` — abort undoes the apply, it does not drop the entry.
359    pub entry_kept: bool,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
363pub struct StashFinalizeOutput {
364    /// Index the entry occupied before the drop.
365    pub index: usize,
366    /// Sha of the dropped stash commit (full 40-char hex). The commit itself
367    /// survives until `git gc` prunes it, so this is the recovery key: feed
368    /// it to [`super::GitModule::stash_restore`] to put the entry back.
369    pub dropped_sha: String,
370    /// Reflog message of the dropped entry.
371    pub message: String,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
375pub struct StashRestoreOutput {
376    /// Full 40-char sha of the stash commit now back on `refs/stash`.
377    pub restored_sha: String,
378    /// Always `0` — `git stash store` pushes onto the top of the list, so the
379    /// restored entry is `stash@{0}` and every pre-existing index shifts by 1.
380    pub index: usize,
381    /// Reflog message the restored entry carries (the caller's `message`, or
382    /// the stash commit's own summary when none was given).
383    pub message: String,
384}
385
386// ---------------------------------------------------------------------------
387// Session
388// ---------------------------------------------------------------------------
389
390#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
391pub struct SessionReleaseOutput {
392    /// Worktree paths whose ownership this session adopted.
393    pub adopted_worktrees: Vec<PathBuf>,
394    /// Branches whose ownership this session adopted.
395    pub adopted_branches: Vec<String>,
396}