1use 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 #[arg(short, long, default_value = "")]
25 message: String,
26}
27
28#[derive(Debug, Subcommand)]
29enum StashCmd {
30 Save(SaveOpts),
32 List,
34 Pop {
36 #[arg(default_value_t = 0)]
37 index: usize,
38 },
39 Apply {
41 #[arg(default_value_t = 0)]
42 index: usize,
43 },
44 Clear,
46 Drop {
48 #[arg(default_value_t = 0)]
49 index: usize,
50 },
51 Show {
53 #[arg(default_value_t = 0)]
54 index: usize,
55 },
56}
57
58#[must_use]
59pub fn run(args: &[String]) -> u8 {
60 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 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 let code = dispatch(opts.sub, &store, &cwd);
111 drop(lock);
112 code
113}
114
115fn 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 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
181fn 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}