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