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 { 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    // Validate branch name
20    if let Err(msg) = validate_branch_name(&branch_name) {
21        eprintln!("{}", format_error_message(msg));
22        return;
23    }
24
25    // Check if branch already exists
26    if branch_exists(&branch_name) {
27        eprintln!("{}", format_branch_exists_message(&branch_name));
28        return;
29    }
30
31    // Determine stash reference
32    let stash = stash_ref.unwrap_or_else(|| "stash@{0}".to_string());
33
34    // Validate stash exists
35    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    // Create branch from stash
43    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    // Get all stashes with timestamps
53    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    // Filter stashes by age if specified
67    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    // Get all stashes with their branch information
102    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    // Filter stashes by branch
111    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
147// Helper function to validate branch name
148fn 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
168// Helper function to check if branch exists
169fn 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
177// Helper function to validate stash exists
178fn 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
191// Helper function to create branch from stash
192fn 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
205// Helper function to get stash list with dates
206fn 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
228// Helper function to get stash list with branches
229fn 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
251// Helper function to parse stash line with date
252fn 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
266// Helper function to parse stash line with branch
267fn 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
281// Helper function to extract branch name from stash message
282fn extract_branch_from_message(message: &str) -> String {
283    // Stash messages typically start with "On branch_name:" or "WIP on branch_name:"
284    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
301// Helper function to filter stashes by age
302fn filter_stashes_by_age(stashes: &[StashInfo], age: &str) -> Result<Vec<StashInfo>, &'static str> {
303    // For simplicity, we'll implement basic age filtering
304    // In a real implementation, you'd parse the age string and compare timestamps
305    if age.ends_with('d') || age.ends_with('w') || age.ends_with('m') {
306        // This is a placeholder - real implementation would parse timestamps
307        Ok(stashes.to_vec())
308    } else {
309        Err("Invalid age format. Use format like '7d', '2w', '1m'")
310    }
311}
312
313// Helper function to delete stash
314fn 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
327// Helper function to apply stash
328fn 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
341// Helper function to get git stash branch args
342pub fn get_git_stash_branch_args() -> [&'static str; 2] {
343    ["stash", "branch"]
344}
345
346// Helper function to get git stash drop args
347pub fn get_git_stash_drop_args() -> [&'static str; 2] {
348    ["stash", "drop"]
349}
350
351// Formatting functions
352pub 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}