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`, `.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}