git_x/commands/
branch.rs

1use crate::core::traits::*;
2use crate::core::{git::*, interactive::Interactive, safety::Safety, validation::Validate};
3use crate::{GitXError, Result};
4
5/// Branch-related commands grouped together
6pub struct BranchCommands;
7
8impl BranchCommands {
9    /// Create a new branch command
10    pub fn new_branch(name: &str, from: Option<&str>) -> Result<String> {
11        NewBranchCommand::new(name.to_string(), from.map(|s| s.to_string())).execute()
12    }
13
14    /// Clean merged branches command
15    pub fn clean_branches(dry_run: bool) -> Result<String> {
16        CleanBranchesCommand::new(dry_run).execute()
17    }
18
19    /// Switch to recent branch command
20    pub fn switch_recent() -> Result<String> {
21        SwitchRecentCommand::new().execute()
22    }
23
24    /// Rename current branch command
25    pub fn rename_branch(new_name: &str) -> Result<String> {
26        RenameBranchCommand::new(new_name.to_string()).execute()
27    }
28
29    /// Prune remote branches command
30    pub fn prune_branches(dry_run: bool) -> Result<String> {
31        PruneBranchesCommand::new(dry_run).execute()
32    }
33
34    /// Stash current work into a branch
35    pub fn stash_branch(branch_name: &str) -> Result<String> {
36        StashBranchCommand::new(branch_name.to_string()).execute()
37    }
38}
39
40/// Command to create a new branch
41pub struct NewBranchCommand {
42    name: String,
43    from: Option<String>,
44}
45
46impl NewBranchCommand {
47    pub fn new(name: String, from: Option<String>) -> Self {
48        Self { name, from }
49    }
50}
51
52impl Command for NewBranchCommand {
53    fn execute(&self) -> Result<String> {
54        // Validate inputs
55        Validate::branch_name(&self.name)?;
56
57        if let Some(ref base) = self.from {
58            if !GitOperations::commit_exists(base)? {
59                return Err(GitXError::GitCommand(format!(
60                    "Base branch or ref '{base}' does not exist"
61                )));
62            }
63        }
64
65        // Check if branch already exists
66        if BranchOperations::exists(&self.name)? {
67            return Err(GitXError::GitCommand(format!(
68                "Branch '{}' already exists",
69                self.name
70            )));
71        }
72
73        // Create and switch to the branch
74        BranchOperations::create(&self.name, self.from.as_deref())?;
75
76        Ok(format!("✅ Created and switched to branch '{}'", self.name))
77    }
78
79    fn name(&self) -> &'static str {
80        "new-branch"
81    }
82
83    fn description(&self) -> &'static str {
84        "Create and switch to a new branch"
85    }
86}
87
88impl GitCommand for NewBranchCommand {}
89
90/// Command to clean merged branches
91pub struct CleanBranchesCommand {
92    dry_run: bool,
93}
94
95impl CleanBranchesCommand {
96    pub fn new(dry_run: bool) -> Self {
97        Self { dry_run }
98    }
99
100    fn get_protected_branches() -> Vec<&'static str> {
101        vec!["main", "master", "develop"]
102    }
103
104    fn is_protected_branch(branch: &str) -> bool {
105        Self::get_protected_branches().contains(&branch)
106    }
107}
108
109impl Command for CleanBranchesCommand {
110    fn execute(&self) -> Result<String> {
111        let merged_branches = GitOperations::merged_branches()?;
112        let current_branch = GitOperations::current_branch()?;
113
114        let branches_to_delete: Vec<String> = merged_branches
115            .into_iter()
116            .filter(|branch| branch != &current_branch)
117            .filter(|branch| !Self::is_protected_branch(branch))
118            .collect();
119
120        if branches_to_delete.is_empty() {
121            return Ok("No merged branches to delete.".to_string());
122        }
123
124        if self.dry_run {
125            let mut result = format!(
126                "🧪 (dry run) {} branches would be deleted:\n",
127                branches_to_delete.len()
128            );
129            for branch in &branches_to_delete {
130                result.push_str(&format!("(dry run) Would delete: {branch}\n"));
131            }
132            return Ok(result);
133        }
134
135        // Confirm deletion
136        let details = format!(
137            "This will delete {} merged branches: {}",
138            branches_to_delete.len(),
139            branches_to_delete.join(", ")
140        );
141
142        if !Safety::confirm_destructive_operation("Clean merged branches", &details)? {
143            return Ok("Operation cancelled by user.".to_string());
144        }
145
146        let mut deleted = Vec::new();
147        for branch in branches_to_delete {
148            if BranchOperations::delete(&branch, false).is_ok() {
149                deleted.push(branch);
150            }
151        }
152
153        Ok(format!(
154            "🧹 Deleted {} merged branches:\n{}",
155            deleted.len(),
156            deleted.join("\n")
157        ))
158    }
159
160    fn name(&self) -> &'static str {
161        "clean-branches"
162    }
163
164    fn description(&self) -> &'static str {
165        "Delete merged branches"
166    }
167}
168
169impl GitCommand for CleanBranchesCommand {}
170impl DryRunnable for CleanBranchesCommand {
171    fn execute_dry_run(&self) -> Result<String> {
172        CleanBranchesCommand::new(true).execute()
173    }
174
175    fn is_dry_run(&self) -> bool {
176        self.dry_run
177    }
178}
179
180impl Destructive for CleanBranchesCommand {
181    fn destruction_description(&self) -> String {
182        "This will permanently delete merged branches".to_string()
183    }
184}
185
186/// Command to switch to a recent branch
187pub struct SwitchRecentCommand;
188
189impl Default for SwitchRecentCommand {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl SwitchRecentCommand {
196    pub fn new() -> Self {
197        Self
198    }
199}
200
201impl Command for SwitchRecentCommand {
202    fn execute(&self) -> Result<String> {
203        let branches = GitOperations::recent_branches(Some(10))?;
204
205        if branches.is_empty() {
206            return Err(GitXError::GitCommand(
207                "No recent branches found".to_string(),
208            ));
209        }
210
211        let selected_branch = if Interactive::is_interactive() {
212            Interactive::branch_picker(&branches, Some("Select a recent branch to switch to"))?
213        } else {
214            // In non-interactive mode, just switch to the most recent branch
215            branches[0].clone()
216        };
217
218        BranchOperations::switch(&selected_branch)?;
219        Ok(format!("Switched to branch '{selected_branch}'"))
220    }
221
222    fn name(&self) -> &'static str {
223        "switch-recent"
224    }
225
226    fn description(&self) -> &'static str {
227        "Switch to a recently used branch"
228    }
229}
230
231impl GitCommand for SwitchRecentCommand {}
232impl crate::core::traits::Interactive for SwitchRecentCommand {
233    fn execute_non_interactive(&self) -> Result<String> {
234        let branches = GitOperations::recent_branches(Some(1))?;
235        if branches.is_empty() {
236            return Err(GitXError::GitCommand(
237                "No recent branches found".to_string(),
238            ));
239        }
240        BranchOperations::switch(&branches[0])?;
241        Ok(format!("Switched to branch '{}'", branches[0]))
242    }
243}
244
245/// Command to rename current branch
246pub struct RenameBranchCommand {
247    new_name: String,
248}
249
250impl RenameBranchCommand {
251    pub fn new(new_name: String) -> Self {
252        Self { new_name }
253    }
254}
255
256impl Command for RenameBranchCommand {
257    fn execute(&self) -> Result<String> {
258        Validate::branch_name(&self.new_name)?;
259
260        let current_branch = GitOperations::current_branch()?;
261
262        if BranchOperations::exists(&self.new_name)? {
263            return Err(GitXError::GitCommand(format!(
264                "Branch '{}' already exists",
265                self.new_name
266            )));
267        }
268
269        BranchOperations::rename(&self.new_name)?;
270        Ok(format!(
271            "✅ Renamed branch '{}' to '{}'",
272            current_branch, self.new_name
273        ))
274    }
275
276    fn name(&self) -> &'static str {
277        "rename-branch"
278    }
279
280    fn description(&self) -> &'static str {
281        "Rename the current branch"
282    }
283}
284
285impl GitCommand for RenameBranchCommand {}
286
287/// Command to prune remote branches
288pub struct PruneBranchesCommand {
289    dry_run: bool,
290}
291
292impl PruneBranchesCommand {
293    pub fn new(dry_run: bool) -> Self {
294        Self { dry_run }
295    }
296}
297
298impl Command for PruneBranchesCommand {
299    fn execute(&self) -> Result<String> {
300        if self.dry_run {
301            GitOperations::run(&["remote", "prune", "origin", "--dry-run"])?;
302            Ok("🧪 (dry run) Showed what would be pruned".to_string())
303        } else {
304            GitOperations::run_status(&["remote", "prune", "origin"])?;
305            Ok("🧹 Pruned remote tracking branches".to_string())
306        }
307    }
308
309    fn name(&self) -> &'static str {
310        "prune-branches"
311    }
312
313    fn description(&self) -> &'static str {
314        "Prune remote tracking branches"
315    }
316}
317
318impl GitCommand for PruneBranchesCommand {}
319impl DryRunnable for PruneBranchesCommand {
320    fn execute_dry_run(&self) -> Result<String> {
321        PruneBranchesCommand::new(true).execute()
322    }
323
324    fn is_dry_run(&self) -> bool {
325        self.dry_run
326    }
327}
328
329/// Command to stash work into a new branch
330pub struct StashBranchCommand {
331    branch_name: String,
332}
333
334impl StashBranchCommand {
335    pub fn new(branch_name: String) -> Self {
336        Self { branch_name }
337    }
338}
339
340impl Command for StashBranchCommand {
341    fn execute(&self) -> Result<String> {
342        Validate::branch_name(&self.branch_name)?;
343
344        if BranchOperations::exists(&self.branch_name)? {
345            return Err(GitXError::GitCommand(format!(
346                "Branch '{}' already exists",
347                self.branch_name
348            )));
349        }
350
351        // Create branch from current state
352        BranchOperations::create(&self.branch_name, None)?;
353
354        // Reset to clean state
355        GitOperations::run_status(&["reset", "--hard", "HEAD"])?;
356
357        Ok(format!(
358            "✅ Created branch '{}' with current changes and reset working directory",
359            self.branch_name
360        ))
361    }
362
363    fn name(&self) -> &'static str {
364        "stash-branch"
365    }
366
367    fn description(&self) -> &'static str {
368        "Create a branch with current changes and reset working directory"
369    }
370}
371
372impl GitCommand for StashBranchCommand {}
373impl Destructive for StashBranchCommand {
374    fn destruction_description(&self) -> String {
375        "This will reset your working directory to a clean state".to_string()
376    }
377}