git_x/
stash_branch.rs

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    // Validate branch name
23    if let Err(msg) = validate_branch_name(&branch_name) {
24        eprintln!("{}", format_error_message(msg));
25        return;
26    }
27
28    // Check if branch already exists
29    if branch_exists(&branch_name) {
30        eprintln!("{}", format_branch_exists_message(&branch_name));
31        return;
32    }
33
34    // Determine stash reference
35    let stash = stash_ref.unwrap_or_else(|| "stash@{0}".to_string());
36
37    // Validate stash exists
38    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    // Create branch from stash
46    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    // Get all stashes with timestamps
56    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    // Filter stashes by age if specified
70    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    // Get all stashes with their branch information
114    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    // Filter stashes by branch
123    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
165// Helper function to validate branch name
166pub 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
186// Helper function to check if branch exists
187pub 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
200// Helper function to validate stash exists
201pub 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
214// Helper function to create branch from stash
215fn 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
228// Helper function to get stash list with dates
229fn 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
251// Helper function to get stash list with branches
252fn 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
274// Helper function to parse stash line with date
275pub 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
289// Helper function to parse stash line with branch
290pub 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
304// Helper function to extract branch name from stash message
305pub fn extract_branch_from_message(message: &str) -> String {
306    // Stash messages typically start with "On branch_name:" or "WIP on branch_name:"
307    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
324// Helper function to filter stashes by age
325pub fn filter_stashes_by_age(
326    stashes: &[StashInfo],
327    age: &str,
328) -> Result<Vec<StashInfo>, &'static str> {
329    // For simplicity, we'll implement basic age filtering
330    // In a real implementation, you'd parse the age string and compare timestamps
331    if age.ends_with('d') || age.ends_with('w') || age.ends_with('m') {
332        // This is a placeholder - real implementation would parse timestamps
333        Ok(stashes.to_vec())
334    } else {
335        Err("Invalid age format. Use format like '7d', '2w', '1m'")
336    }
337}
338
339// Helper function to delete stash
340fn 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
353// Helper function to apply stash
354fn 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
367// Helper function to get git stash branch args
368pub fn get_git_stash_branch_args() -> [&'static str; 2] {
369    ["stash", "branch"]
370}
371
372// Helper function to get git stash drop args
373pub fn get_git_stash_drop_args() -> [&'static str; 2] {
374    ["stash", "drop"]
375}
376
377// Formatting functions
378pub 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}