Skip to main content

mkit_cli/commands/
stash.rs

1//! `mkit stash save|list|pop|drop|show` — stash working-directory
2//! changes. Backing logic lives in `mkit_core::ops::stash`.
3
4use std::io::Write;
5
6use clap::{Parser, Subcommand};
7use mkit_core::ops::stash;
8use mkit_core::store::ObjectStore;
9
10use crate::clap_shim;
11use crate::exit;
12use crate::format;
13
14#[derive(Debug, Parser)]
15#[command(name = "mkit stash", about = "Stash working-directory changes.")]
16struct StashOpts {
17    #[command(subcommand)]
18    sub: StashCmd,
19}
20
21#[derive(Debug, Parser)]
22struct SaveOpts {
23    /// Stash message.
24    #[arg(short, long, default_value = "")]
25    message: String,
26}
27
28#[derive(Debug, Subcommand)]
29enum StashCmd {
30    /// Save the current worktree changes as a new stash entry.
31    Save(SaveOpts),
32    /// List all stash entries.
33    List,
34    /// Apply and remove a stash entry (default: entry 0).
35    Pop {
36        #[arg(default_value_t = 0)]
37        index: usize,
38    },
39    /// Apply a stash entry WITHOUT removing it (default: entry 0).
40    Apply {
41        #[arg(default_value_t = 0)]
42        index: usize,
43    },
44    /// Remove ALL stash entries.
45    Clear,
46    /// Remove a stash entry without applying it (default: entry 0).
47    Drop {
48        #[arg(default_value_t = 0)]
49        index: usize,
50    },
51    /// Show the diff of a stash entry (default: entry 0).
52    Show {
53        #[arg(default_value_t = 0)]
54        index: usize,
55    },
56}
57
58#[must_use]
59pub fn run(args: &[String]) -> u8 {
60    // `mkit stash` (no args) = save with empty message.
61    // `mkit stash -m <msg>` = save with message.
62    // Either is the "save is the default subcommand" form, which
63    // clap doesn't model directly; rewrite the argv so clap sees an
64    // explicit `save` subcommand when the user omitted it.
65    let needs_default = args.first().is_none_or(|a| {
66        !matches!(
67            a.as_str(),
68            "save" | "list" | "pop" | "apply" | "drop" | "clear" | "show" | "-h" | "--help"
69        )
70    });
71    let rewritten: Vec<String> = if needs_default {
72        std::iter::once("save".to_owned())
73            .chain(args.iter().cloned())
74            .collect()
75    } else {
76        args.to_vec()
77    };
78
79    let opts = match clap_shim::parse::<StashOpts>("mkit stash", &rewritten) {
80        Ok(o) => o,
81        Err(code) => return code,
82    };
83    let cwd = match std::env::current_dir() {
84        Ok(p) => p,
85        Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
86    };
87    let store = match super::open_store_configured(&cwd) {
88        Ok(s) => s,
89        Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
90    };
91
92    // Commands that mutate the worktree/index/manifest must serialise
93    // against other worktree commands: `save`/`pop`/`apply`/`drop`/`clear`.
94    // (`apply` writes the worktree; `clear` rewrites the manifest.)
95    // `list` and `show` are read-only and run unlocked.
96    let lock = match opts.sub {
97        StashCmd::Save(_)
98        | StashCmd::Pop { .. }
99        | StashCmd::Apply { .. }
100        | StashCmd::Drop { .. }
101        | StashCmd::Clear => match super::acquire_worktree_lock(&cwd) {
102            Ok(l) => Some(l),
103            Err(code) => return code,
104        },
105        StashCmd::List | StashCmd::Show { .. } => None,
106    };
107
108    // `lock` is held until this binding drops at the end of `run`, so the
109    // worktree stays serialised across the whole `dispatch` call.
110    let code = dispatch(opts.sub, &store, &cwd);
111    drop(lock);
112    code
113}
114
115/// Run a parsed stash subcommand. Split out of [`run`] so the worktree
116/// lock acquisition / mode dispatch stays small enough for clippy's
117/// `too_many_lines`.
118fn dispatch(sub: StashCmd, store: &ObjectStore, cwd: &std::path::Path) -> u8 {
119    match sub {
120        StashCmd::Save(save) => match stash::save(store, cwd, &save.message) {
121            Ok(()) => {
122                let mut stderr = std::io::stderr().lock();
123                let _ = writeln!(stderr, "stashed: {}", save.message);
124                exit::OK
125            }
126            Err(e) => emit_err(&format!("stash save: {e}"), exit::GENERAL_ERROR),
127        },
128        StashCmd::List => match stash::list(cwd) {
129            Ok(list) => {
130                if list.entries.is_empty() {
131                    let mut stderr = std::io::stderr().lock();
132                    let _ = writeln!(stderr, "(no stash entries)");
133                    return exit::OK;
134                }
135                let mut stdout = std::io::stdout().lock();
136                for (i, e) in list.entries.iter().enumerate() {
137                    let _ = writeln!(
138                        stdout,
139                        "stash@{{{i}}}: {} {}",
140                        format::short_hash(&e.commit_hash, 8),
141                        e.message
142                    );
143                }
144                exit::OK
145            }
146            Err(e) => emit_err(&format!("stash list: {e}"), exit::GENERAL_ERROR),
147        },
148        // `pop` removes the entry after a successful restore; `apply`
149        // leaves it in place. Both run the same #205/#176 destructive-
150        // restore guard so they never clobber uncommitted edits on
151        // unrelated paths.
152        StashCmd::Pop { index } => restore_entry(store, cwd, index, true),
153        StashCmd::Apply { index } => restore_entry(store, cwd, index, false),
154        StashCmd::Clear => match stash::clear(cwd) {
155            Ok(()) => {
156                let mut stderr = std::io::stderr().lock();
157                let _ = writeln!(stderr, "cleared all stash entries");
158                exit::OK
159            }
160            Err(e) => emit_err(&format!("stash clear: {e}"), exit::GENERAL_ERROR),
161        },
162        StashCmd::Drop { index } => match stash::drop(cwd, index) {
163            Ok(()) => {
164                let mut stderr = std::io::stderr().lock();
165                let _ = writeln!(stderr, "dropped stash@{{{index}}}");
166                exit::OK
167            }
168            Err(e) => emit_err(&format!("stash drop: {e}"), exit::GENERAL_ERROR),
169        },
170        StashCmd::Show { index } => match stash::render_stash_show(store, cwd, index) {
171            Ok(output) => {
172                let mut stdout = std::io::stdout().lock();
173                let _ = stdout.write_all(output.as_bytes());
174                exit::OK
175            }
176            Err(e) => emit_err(&format!("stash show: {e}"), exit::GENERAL_ERROR),
177        },
178    }
179}
180
181/// Restore stash entry `index` into the worktree. `drop_entry` chooses
182/// between `pop` (removes the entry after a clean restore) and `apply`
183/// (leaves it on the stack). Both run the #205/#176 destructive-restore
184/// guard up-front so a refusal leaves the stash and worktree untouched.
185fn restore_entry(store: &ObjectStore, cwd: &std::path::Path, index: usize, drop_entry: bool) -> u8 {
186    let verb = if drop_entry { "pop" } else { "apply" };
187    let tree_hash = match stash::entry_tree_hash(store, cwd, index) {
188        Ok(h) => h,
189        Err(e) => return emit_err(&format!("stash {verb}: {e}"), exit::GENERAL_ERROR),
190    };
191    if let Err(e) = super::ensure_restore_safe(cwd, store, tree_hash) {
192        return emit_err(&format!("stash {verb}: {e}"), exit::GENERAL_ERROR);
193    }
194    let result = if drop_entry {
195        stash::pop(store, cwd, index)
196    } else {
197        stash::apply(store, cwd, index)
198    };
199    match result {
200        Ok(()) => {
201            let past = if drop_entry { "popped" } else { "applied" };
202            let mut stderr = std::io::stderr().lock();
203            let _ = writeln!(stderr, "{past} stash@{{{index}}}");
204            exit::OK
205        }
206        Err(e) => emit_err(&format!("stash {verb}: {e}"), exit::GENERAL_ERROR),
207    }
208}
209
210fn emit_err(msg: &str, code: u8) -> u8 {
211    let mut stderr = std::io::stderr().lock();
212    let _ = writeln!(stderr, "error: {msg}");
213    code
214}