1use crate::cli::StashBranchAction;
2use std::process::Command;
3
4pub fn run(action: StashBranchAction) {
5 match action {
6 StashBranchAction::Create {
7 branch_name,
8 stash_ref,
9 } => create_branch_from_stash(branch_name, stash_ref),
10 StashBranchAction::Clean {
11 older_than,
12 dry_run,
13 } => clean_old_stashes(older_than, dry_run),
14 StashBranchAction::ApplyByBranch {
15 branch_name,
16 list_only,
17 } => apply_stashes_by_branch(branch_name, list_only),
18 }
19}
20
21fn create_branch_from_stash(branch_name: String, stash_ref: Option<String>) {
22 if let Err(msg) = validate_branch_name(&branch_name) {
24 eprintln!("{}", format_error_message(msg));
25 return;
26 }
27
28 if branch_exists(&branch_name) {
30 eprintln!("{}", format_branch_exists_message(&branch_name));
31 return;
32 }
33
34 let stash = stash_ref.unwrap_or_else(|| "stash@{0}".to_string());
36
37 if let Err(msg) = validate_stash_exists(&stash) {
39 eprintln!("{}", format_error_message(msg));
40 return;
41 }
42
43 println!("{}", format_creating_branch_message(&branch_name, &stash));
44
45 if let Err(msg) = create_branch_from_stash_ref(&branch_name, &stash) {
47 eprintln!("{}", format_error_message(msg));
48 return;
49 }
50
51 println!("{}", format_branch_created_message(&branch_name));
52}
53
54fn clean_old_stashes(older_than: Option<String>, dry_run: bool) {
55 let stashes = match get_stash_list_with_dates() {
57 Ok(stashes) => stashes,
58 Err(msg) => {
59 eprintln!("{}", format_error_message(msg));
60 return;
61 }
62 };
63
64 if stashes.is_empty() {
65 println!("{}", format_no_stashes_message());
66 return;
67 }
68
69 let stashes_to_clean = if let Some(age) = older_than {
71 match filter_stashes_by_age(&stashes, &age) {
72 Ok(filtered) => filtered,
73 Err(msg) => {
74 eprintln!("{}", format_error_message(msg));
75 return;
76 }
77 }
78 } else {
79 stashes
80 };
81
82 if stashes_to_clean.is_empty() {
83 println!("{}", format_no_old_stashes_message());
84 return;
85 }
86
87 println!(
88 "{}",
89 format_stashes_to_clean_message(stashes_to_clean.len(), dry_run)
90 );
91
92 for stash in &stashes_to_clean {
93 println!(" {}", format_stash_entry(&stash.name, &stash.message));
94 }
95
96 if !dry_run {
97 for stash in &stashes_to_clean {
98 if let Err(msg) = delete_stash(&stash.name) {
99 eprintln!(
100 "{}",
101 format_error_message(&format!("Failed to delete {}: {}", stash.name, msg))
102 );
103 }
104 }
105 println!(
106 "{}",
107 format_cleanup_complete_message(stashes_to_clean.len())
108 );
109 }
110}
111
112fn apply_stashes_by_branch(branch_name: String, list_only: bool) {
113 let stashes = match get_stash_list_with_branches() {
115 Ok(stashes) => stashes,
116 Err(msg) => {
117 eprintln!("{}", format_error_message(msg));
118 return;
119 }
120 };
121
122 let branch_stashes: Vec<_> = stashes
124 .into_iter()
125 .filter(|s| s.branch == branch_name)
126 .collect();
127
128 if branch_stashes.is_empty() {
129 println!("{}", format_no_stashes_for_branch_message(&branch_name));
130 return;
131 }
132
133 if list_only {
134 println!(
135 "{}",
136 format_stashes_for_branch_header(&branch_name, branch_stashes.len())
137 );
138 for stash in &branch_stashes {
139 println!(" {}", format_stash_entry(&stash.name, &stash.message));
140 }
141 } else {
142 println!(
143 "{}",
144 format_applying_stashes_message(&branch_name, branch_stashes.len())
145 );
146
147 for stash in &branch_stashes {
148 match apply_stash(&stash.name) {
149 Ok(()) => println!(" โ
Applied {}", stash.name),
150 Err(msg) => eprintln!(" โ Failed to apply {}: {}", stash.name, msg),
151 }
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
157pub struct StashInfo {
158 pub name: String,
159 pub message: String,
160 pub branch: String,
161 #[allow(dead_code)]
162 pub timestamp: String,
163}
164
165pub fn validate_branch_name(name: &str) -> Result<(), &'static str> {
167 if name.is_empty() {
168 return Err("Branch name cannot be empty");
169 }
170
171 if name.starts_with('-') {
172 return Err("Branch name cannot start with a dash");
173 }
174
175 if name.contains("..") {
176 return Err("Branch name cannot contain '..'");
177 }
178
179 if name.contains(' ') {
180 return Err("Branch name cannot contain spaces");
181 }
182
183 Ok(())
184}
185
186pub fn branch_exists(branch_name: &str) -> bool {
188 Command::new("git")
189 .args([
190 "show-ref",
191 "--verify",
192 "--quiet",
193 &format!("refs/heads/{branch_name}"),
194 ])
195 .status()
196 .map(|status| status.success())
197 .unwrap_or(false)
198}
199
200pub fn validate_stash_exists(stash_ref: &str) -> Result<(), &'static str> {
202 let output = Command::new("git")
203 .args(["rev-parse", "--verify", stash_ref])
204 .output()
205 .map_err(|_| "Failed to validate stash reference")?;
206
207 if !output.status.success() {
208 return Err("Stash reference does not exist");
209 }
210
211 Ok(())
212}
213
214fn create_branch_from_stash_ref(branch_name: &str, stash_ref: &str) -> Result<(), &'static str> {
216 let status = Command::new("git")
217 .args(["stash", "branch", branch_name, stash_ref])
218 .status()
219 .map_err(|_| "Failed to create branch from stash")?;
220
221 if !status.success() {
222 return Err("Failed to create branch from stash");
223 }
224
225 Ok(())
226}
227
228fn get_stash_list_with_dates() -> Result<Vec<StashInfo>, &'static str> {
230 let output = Command::new("git")
231 .args(["stash", "list", "--pretty=format:%gd|%s|%gD"])
232 .output()
233 .map_err(|_| "Failed to get stash list")?;
234
235 if !output.status.success() {
236 return Err("Failed to retrieve stash list");
237 }
238
239 let stdout = String::from_utf8_lossy(&output.stdout);
240 let mut stashes = Vec::new();
241
242 for line in stdout.lines() {
243 if let Some(stash) = parse_stash_line_with_date(line) {
244 stashes.push(stash);
245 }
246 }
247
248 Ok(stashes)
249}
250
251fn get_stash_list_with_branches() -> Result<Vec<StashInfo>, &'static str> {
253 let output = Command::new("git")
254 .args(["stash", "list", "--pretty=format:%gd|%s"])
255 .output()
256 .map_err(|_| "Failed to get stash list")?;
257
258 if !output.status.success() {
259 return Err("Failed to retrieve stash list");
260 }
261
262 let stdout = String::from_utf8_lossy(&output.stdout);
263 let mut stashes = Vec::new();
264
265 for line in stdout.lines() {
266 if let Some(stash) = parse_stash_line_with_branch(line) {
267 stashes.push(stash);
268 }
269 }
270
271 Ok(stashes)
272}
273
274pub fn parse_stash_line_with_date(line: &str) -> Option<StashInfo> {
276 let parts: Vec<&str> = line.splitn(3, '|').collect();
277 if parts.len() != 3 {
278 return None;
279 }
280
281 Some(StashInfo {
282 name: parts[0].to_string(),
283 message: parts[1].to_string(),
284 branch: extract_branch_from_message(parts[1]),
285 timestamp: parts[2].to_string(),
286 })
287}
288
289pub fn parse_stash_line_with_branch(line: &str) -> Option<StashInfo> {
291 let parts: Vec<&str> = line.splitn(2, '|').collect();
292 if parts.len() != 2 {
293 return None;
294 }
295
296 Some(StashInfo {
297 name: parts[0].to_string(),
298 message: parts[1].to_string(),
299 branch: extract_branch_from_message(parts[1]),
300 timestamp: String::new(),
301 })
302}
303
304pub fn extract_branch_from_message(message: &str) -> String {
306 if let Some(start) = message.find("On ") {
308 let rest = &message[start + 3..];
309 if let Some(end) = rest.find(':') {
310 return rest[..end].to_string();
311 }
312 }
313
314 if let Some(start) = message.find("WIP on ") {
315 let rest = &message[start + 7..];
316 if let Some(end) = rest.find(':') {
317 return rest[..end].to_string();
318 }
319 }
320
321 "unknown".to_string()
322}
323
324pub fn filter_stashes_by_age(
326 stashes: &[StashInfo],
327 age: &str,
328) -> Result<Vec<StashInfo>, &'static str> {
329 if age.ends_with('d') || age.ends_with('w') || age.ends_with('m') {
332 Ok(stashes.to_vec())
334 } else {
335 Err("Invalid age format. Use format like '7d', '2w', '1m'")
336 }
337}
338
339fn delete_stash(stash_name: &str) -> Result<(), &'static str> {
341 let status = Command::new("git")
342 .args(["stash", "drop", stash_name])
343 .status()
344 .map_err(|_| "Failed to delete stash")?;
345
346 if !status.success() {
347 return Err("Failed to delete stash");
348 }
349
350 Ok(())
351}
352
353fn apply_stash(stash_name: &str) -> Result<(), &'static str> {
355 let status = Command::new("git")
356 .args(["stash", "apply", stash_name])
357 .status()
358 .map_err(|_| "Failed to apply stash")?;
359
360 if !status.success() {
361 return Err("Failed to apply stash");
362 }
363
364 Ok(())
365}
366
367pub fn get_git_stash_branch_args() -> [&'static str; 2] {
369 ["stash", "branch"]
370}
371
372pub fn get_git_stash_drop_args() -> [&'static str; 2] {
374 ["stash", "drop"]
375}
376
377pub fn format_error_message(msg: &str) -> String {
379 format!("โ {msg}")
380}
381
382pub fn format_branch_exists_message(branch_name: &str) -> String {
383 format!("โ Branch '{branch_name}' already exists")
384}
385
386pub fn format_creating_branch_message(branch_name: &str, stash_ref: &str) -> String {
387 format!("๐ฟ Creating branch '{branch_name}' from {stash_ref}...")
388}
389
390pub fn format_branch_created_message(branch_name: &str) -> String {
391 format!("โ
Branch '{branch_name}' created and checked out")
392}
393
394pub fn format_no_stashes_message() -> &'static str {
395 "โน๏ธ No stashes found"
396}
397
398pub fn format_no_old_stashes_message() -> &'static str {
399 "โ
No old stashes to clean"
400}
401
402pub fn format_stashes_to_clean_message(count: usize, dry_run: bool) -> String {
403 if dry_run {
404 format!("๐งช (dry run) Would clean {count} stash(es):")
405 } else {
406 format!("๐งน Cleaning {count} stash(es):")
407 }
408}
409
410pub fn format_cleanup_complete_message(count: usize) -> String {
411 format!("โ
Cleaned {count} stash(es)")
412}
413
414pub fn format_no_stashes_for_branch_message(branch_name: &str) -> String {
415 format!("โน๏ธ No stashes found for branch '{branch_name}'")
416}
417
418pub fn format_stashes_for_branch_header(branch_name: &str, count: usize) -> String {
419 format!("๐ Found {count} stash(es) for branch '{branch_name}':")
420}
421
422pub fn format_applying_stashes_message(branch_name: &str, count: usize) -> String {
423 format!("๐ Applying {count} stash(es) from branch '{branch_name}':")
424}
425
426pub fn format_stash_entry(name: &str, message: &str) -> String {
427 format!("{name}: {message}")
428}