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}