1use crate::cli::StashBranchAction;
2use crate::command::Command;
3use crate::core::git::{BranchOperations, GitOperations};
4use crate::core::safety::Safety;
5use crate::{GitXError, Result};
6use std::process::Command as StdCommand;
7
8pub fn run(action: StashBranchAction) -> Result<()> {
9 let cmd = StashBranchCommand;
10 cmd.execute(action)
11}
12
13pub struct StashBranchCommand;
15
16impl Command for StashBranchCommand {
17 type Input = StashBranchAction;
18 type Output = ();
19
20 fn execute(&self, action: StashBranchAction) -> Result<()> {
21 run_stash_branch(action)
22 }
23
24 fn name(&self) -> &'static str {
25 "stash-branch"
26 }
27
28 fn description(&self) -> &'static str {
29 "Create branches from stashes or manage stash cleanup"
30 }
31
32 fn is_destructive(&self) -> bool {
33 true
34 }
35}
36
37fn run_stash_branch(action: StashBranchAction) -> Result<()> {
38 match action {
39 StashBranchAction::Create {
40 branch_name,
41 stash_ref,
42 } => create_branch_from_stash(branch_name, stash_ref),
43 StashBranchAction::Clean {
44 older_than,
45 dry_run,
46 } => clean_old_stashes(older_than, dry_run),
47 StashBranchAction::ApplyByBranch {
48 branch_name,
49 list_only,
50 } => apply_stashes_by_branch(branch_name, list_only),
51 }
52}
53
54fn create_branch_from_stash(branch_name: String, stash_ref: Option<String>) -> Result<()> {
55 validate_branch_name(&branch_name).map_err(|e| GitXError::GitCommand(e.to_string()))?;
57
58 if BranchOperations::exists(&branch_name).unwrap_or(false) {
60 return Err(GitXError::GitCommand(format!(
61 "Branch '{branch_name}' already exists"
62 )));
63 }
64
65 let stash = stash_ref.unwrap_or_else(|| "stash@{0}".to_string());
67
68 validate_stash_exists(&stash).map_err(|e| GitXError::GitCommand(e.to_string()))?;
70
71 println!("{}", &branch_name);
72
73 create_branch_from_stash_ref(&branch_name, &stash)
75 .map_err(|e| GitXError::GitCommand(e.to_string()))?;
76
77 println!("{}", &branch_name);
78 Ok(())
79}
80
81fn clean_old_stashes(older_than: Option<String>, dry_run: bool) -> Result<()> {
82 let stashes = get_stash_list_with_dates().map_err(|e| GitXError::GitCommand(e.to_string()))?;
84
85 if stashes.is_empty() {
86 println!("โน๏ธ No stashes found");
87 return Ok(());
88 }
89
90 let stashes_to_clean = if let Some(age) = older_than {
92 filter_stashes_by_age(&stashes, &age).map_err(|e| GitXError::GitCommand(e.to_string()))?
93 } else {
94 stashes
95 };
96
97 if stashes_to_clean.is_empty() {
98 println!("โ
No old stashes to clean");
99 return Ok(());
100 }
101
102 let count = stashes_to_clean.len();
103 println!(
104 "{}",
105 if dry_run {
106 format!("๐งช (dry run) Would clean {count} stash(es):")
107 } else {
108 format!("๐งน Cleaning {count} stash(es):")
109 }
110 );
111
112 for stash in &stashes_to_clean {
113 let name = &stash.name;
114 let message = &stash.message;
115 println!(" {name}: {message}");
116 }
117
118 if !dry_run {
119 let stash_names: Vec<_> = stashes_to_clean.iter().map(|s| s.name.as_str()).collect();
121 let details = format!(
122 "This will delete {} stashes: {}",
123 stashes_to_clean.len(),
124 stash_names.join(", ")
125 );
126
127 match Safety::confirm_destructive_operation("Clean old stashes", &details) {
128 Ok(confirmed) => {
129 if !confirmed {
130 println!("Operation cancelled by user.");
131 return Ok(());
132 }
133 }
134 Err(e) => {
135 return Err(GitXError::GitCommand(format!(
136 "Error during confirmation: {e}"
137 )));
138 }
139 }
140
141 for stash in &stashes_to_clean {
142 if let Err(msg) = delete_stash(&stash.name) {
143 let msg1 = &format!("Failed to delete {}: {}", stash.name, msg);
144 eprintln!("{msg1}");
145 }
146 }
147 println!("{}", stashes_to_clean.len());
148 }
149 Ok(())
150}
151
152fn apply_stashes_by_branch(branch_name: String, list_only: bool) -> Result<()> {
153 let stashes =
155 get_stash_list_with_branches().map_err(|e| GitXError::GitCommand(e.to_string()))?;
156
157 let branch_stashes: Vec<_> = stashes
159 .into_iter()
160 .filter(|s| s.branch == branch_name)
161 .collect();
162
163 if branch_stashes.is_empty() {
164 println!("{}", &branch_name);
165 return Ok(());
166 }
167
168 if list_only {
169 println!(
170 "{}",
171 format_stashes_for_branch_header(&branch_name, branch_stashes.len())
172 );
173 for stash in &branch_stashes {
174 let name = &stash.name;
175 let message = &stash.message;
176 println!(" {name}: {message}");
177 }
178 } else {
179 println!(
180 "{}",
181 format_applying_stashes_message(&branch_name, branch_stashes.len())
182 );
183
184 for stash in &branch_stashes {
185 match apply_stash(&stash.name) {
186 Ok(()) => println!(" โ
Applied {}", stash.name),
187 Err(msg) => eprintln!(" โ Failed to apply {}: {}", stash.name, msg),
188 }
189 }
190 }
191 Ok(())
192}
193
194#[derive(Debug, Clone)]
195pub struct StashInfo {
196 pub name: String,
197 pub message: String,
198 pub branch: String,
199 #[allow(dead_code)]
200 pub timestamp: String,
201}
202
203pub fn validate_branch_name(name: &str) -> Result<()> {
204 if name.is_empty() {
205 return Err(GitXError::GitCommand(
206 "Branch name cannot be empty".to_string(),
207 ));
208 }
209
210 if name.starts_with('-') {
211 return Err(GitXError::GitCommand(
212 "Branch name cannot start with a dash".to_string(),
213 ));
214 }
215
216 if name.contains("..") {
217 return Err(GitXError::GitCommand(
218 "Branch name cannot contain '..'".to_string(),
219 ));
220 }
221
222 if name.contains(' ') {
223 return Err(GitXError::GitCommand(
224 "Branch name cannot contain spaces".to_string(),
225 ));
226 }
227
228 Ok(())
229}
230
231pub fn validate_stash_exists(stash_ref: &str) -> Result<()> {
232 match GitOperations::run(&["rev-parse", "--verify", stash_ref]) {
233 Ok(_) => Ok(()),
234 Err(_) => Err(GitXError::GitCommand(
235 "Stash reference does not exist".to_string(),
236 )),
237 }
238}
239
240fn create_branch_from_stash_ref(branch_name: &str, stash_ref: &str) -> Result<()> {
241 let status = StdCommand::new("git")
242 .args(["stash", "branch", branch_name, stash_ref])
243 .status()
244 .map_err(GitXError::Io)?;
245
246 if !status.success() {
247 return Err(GitXError::GitCommand(
248 "Failed to create branch from stash".to_string(),
249 ));
250 }
251
252 Ok(())
253}
254
255fn get_stash_list_with_dates() -> Result<Vec<StashInfo>> {
256 let output = StdCommand::new("git")
257 .args(["stash", "list", "--pretty=format:%gd|%s|%gD"])
258 .output()
259 .map_err(GitXError::Io)?;
260
261 if !output.status.success() {
262 return Err(GitXError::GitCommand(
263 "Failed to retrieve stash list".to_string(),
264 ));
265 }
266
267 let stdout = String::from_utf8_lossy(&output.stdout);
268 let mut stashes = Vec::new();
269
270 for line in stdout.lines() {
271 if let Some(stash) = parse_stash_line_with_date(line) {
272 stashes.push(stash);
273 }
274 }
275
276 Ok(stashes)
277}
278
279fn get_stash_list_with_branches() -> Result<Vec<StashInfo>> {
280 let output = StdCommand::new("git")
281 .args(["stash", "list", "--pretty=format:%gd|%s"])
282 .output()
283 .map_err(GitXError::Io)?;
284
285 if !output.status.success() {
286 return Err(GitXError::GitCommand(
287 "Failed to retrieve stash list".to_string(),
288 ));
289 }
290
291 let stdout = String::from_utf8_lossy(&output.stdout);
292 let mut stashes = Vec::new();
293
294 for line in stdout.lines() {
295 if let Some(stash) = parse_stash_line_with_branch(line) {
296 stashes.push(stash);
297 }
298 }
299
300 Ok(stashes)
301}
302
303pub fn parse_stash_line_with_date(line: &str) -> Option<StashInfo> {
304 let parts: Vec<&str> = line.splitn(3, '|').collect();
305 if parts.len() != 3 {
306 return None;
307 }
308
309 Some(StashInfo {
310 name: parts[0].to_string(),
311 message: parts[1].to_string(),
312 branch: extract_branch_from_message(parts[1]),
313 timestamp: parts[2].to_string(),
314 })
315}
316
317pub fn parse_stash_line_with_branch(line: &str) -> Option<StashInfo> {
318 let parts: Vec<&str> = line.splitn(2, '|').collect();
319 if parts.len() != 2 {
320 return None;
321 }
322
323 Some(StashInfo {
324 name: parts[0].to_string(),
325 message: parts[1].to_string(),
326 branch: extract_branch_from_message(parts[1]),
327 timestamp: String::new(),
328 })
329}
330
331pub fn extract_branch_from_message(message: &str) -> String {
332 if let Some(start) = message.find("On ") {
334 let rest = &message[start + 3..];
335 if let Some(end) = rest.find(':') {
336 return rest[..end].to_string();
337 }
338 }
339
340 if let Some(start) = message.find("WIP on ") {
341 let rest = &message[start + 7..];
342 if let Some(end) = rest.find(':') {
343 return rest[..end].to_string();
344 }
345 }
346
347 "unknown".to_string()
348}
349
350pub fn filter_stashes_by_age(stashes: &[StashInfo], age: &str) -> Result<Vec<StashInfo>> {
351 if age.ends_with('d') || age.ends_with('w') || age.ends_with('m') {
354 Ok(stashes.to_vec())
356 } else {
357 Err(GitXError::GitCommand(
358 "Invalid age format. Use format like '7d', '2w', '1m'".to_string(),
359 ))
360 }
361}
362
363fn delete_stash(stash_name: &str) -> Result<()> {
364 let status = StdCommand::new("git")
365 .args(["stash", "drop", stash_name])
366 .status()
367 .map_err(GitXError::Io)?;
368
369 if !status.success() {
370 return Err(GitXError::GitCommand("Failed to delete stash".to_string()));
371 }
372
373 Ok(())
374}
375
376fn apply_stash(stash_name: &str) -> Result<()> {
377 let status = StdCommand::new("git")
378 .args(["stash", "apply", stash_name])
379 .status()
380 .map_err(GitXError::Io)?;
381
382 if !status.success() {
383 return Err(GitXError::GitCommand("Failed to apply stash".to_string()));
384 }
385
386 Ok(())
387}
388
389fn format_stashes_for_branch_header(branch_name: &str, count: usize) -> String {
390 format!("๐ Found {count} stash(es) for branch '{branch_name}':")
391}
392
393pub fn format_applying_stashes_message(branch_name: &str, count: usize) -> String {
394 format!("๐ Applying {count} stash(es) from branch '{branch_name}':")
395}