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}
207
208/// How [`GitModule::commit`] handles staged paths outside the `only` list.
209///
210/// Only consulted when `only` is `Some(non_empty)`. When `only` is `None` /
211/// empty, `commit` still stages every change via `git add -A` and this enum
212/// is ignored.
213#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
214#[serde(rename_all = "snake_case")]
215pub enum OtherStagedMode {
216    /// Fail (state unchanged) when the index carries paths outside `only`.
217    /// The safe default — protects against silently sweeping unrelated work.
218    #[default]
219    Stop,
220    /// Unstage the other paths, commit `only`, then re-stage them. The
221    /// commit ends up containing exactly the `only` paths; the pre-existing
222    /// staged work stays in the index afterwards.
223    Restage,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
227pub struct MergeOutput {
228    pub branch: String,
229    pub into_branch: String,
230    /// Merge commit sha (full 40-char hex).
231    pub sha: String,
232    pub short_sha: String,
233    /// Raw `git merge` output for diagnostics (fast-forward note, conflict tip).
234    pub raw: String,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
238pub struct BranchDeleteOutput {
239    pub branch: String,
240}
241
242// ---------------------------------------------------------------------------
243// Reset
244// ---------------------------------------------------------------------------
245
246#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
247#[serde(rename_all = "snake_case")]
248pub enum ResetMode {
249    Soft,
250    Mixed,
251    Hard,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
255pub struct ResetOutput {
256    pub mode: ResetMode,
257    /// Revspec / sha that was passed in as the reset target.
258    pub target: String,
259    /// HEAD sha before the reset (full 40-char hex).
260    pub previous_head: String,
261    /// HEAD sha after the reset (full 40-char hex).
262    pub current_head: String,
263}
264
265// ---------------------------------------------------------------------------
266// Session
267// ---------------------------------------------------------------------------
268
269#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
270pub struct SessionReleaseOutput {
271    /// Worktree paths whose ownership this session adopted.
272    pub adopted_worktrees: Vec<PathBuf>,
273    /// Branches whose ownership this session adopted.
274    pub adopted_branches: Vec<String>,
275}