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`, `.claude/CLAUDE.md`,
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// Session
290// ---------------------------------------------------------------------------
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
293pub struct SessionReleaseOutput {
294 /// Worktree paths whose ownership this session adopted.
295 pub adopted_worktrees: Vec<PathBuf>,
296 /// Branches whose ownership this session adopted.
297 pub adopted_branches: Vec<String>,
298}