git_x/
stash_branch.rs

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
13/// Command implementation for git stash-branch
14pub 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
56    validate_branch_name(&branch_name).map_err(|e| GitXError::GitCommand(e.to_string()))?;
57
58    // Check if branch already exists
59    if BranchOperations::exists(&branch_name).unwrap_or(false) {
60        return Err(GitXError::GitCommand(format!(
61            "Branch '{branch_name}' already exists"
62        )));
63    }
64
65    // Determine stash reference
66    let stash = stash_ref.unwrap_or_else(|| "stash@{0}".to_string());
67
68    // Validate stash exists
69    validate_stash_exists(&stash).map_err(|e| GitXError::GitCommand(e.to_string()))?;
70
71    println!("{}", &branch_name);
72
73    // Create branch from stash
74    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    // Get all stashes with timestamps
83    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    // Filter stashes by age if specified
91    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        // Safety confirmation for destructive operation
120        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    // Get all stashes with their branch information
154    let stashes =
155        get_stash_list_with_branches().map_err(|e| GitXError::GitCommand(e.to_string()))?;
156
157    // Filter stashes by branch
158    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    // Stash messages typically start with "On branch_name:" or "WIP on branch_name:"
333    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    // For simplicity, we'll implement basic age filtering
352    // In a real implementation, you'd parse the age string and compare timestamps
353    if age.ends_with('d') || age.ends_with('w') || age.ends_with('m') {
354        // This is a placeholder - real implementation would parse timestamps
355        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}