git_x/domain/
branch_manager.rs

1use crate::core::{git::*, safety::Safety, validation::Validate};
2use crate::domain::GitRepository;
3use crate::{GitXError, Result};
4
5/// High-level branch management operations
6pub struct BranchManager {
7    _repository: GitRepository,
8}
9
10impl BranchManager {
11    /// Create a new branch manager
12    pub fn new(repository: GitRepository) -> Self {
13        Self {
14            _repository: repository,
15        }
16    }
17
18    /// Create a new branch with validation and safety checks
19    pub fn create_branch(&self, request: CreateBranchRequest) -> Result<BranchCreationResult> {
20        // Validate inputs
21        Validate::branch_name(&request.name)?;
22
23        if let Some(ref base) = request.from {
24            if !GitOperations::commit_exists(base)? {
25                return Err(GitXError::GitCommand(format!(
26                    "Base branch or ref '{base}' does not exist"
27                )));
28            }
29        }
30
31        // Check if branch already exists
32        if BranchOperations::exists(&request.name)? {
33            return Err(GitXError::GitCommand(format!(
34                "Branch '{}' already exists",
35                request.name
36            )));
37        }
38
39        // Create backup if requested
40        let backup_branch = if request.create_backup {
41            Some(Safety::create_backup_branch(Some("pre-create"))?)
42        } else {
43            None
44        };
45
46        // Create and switch to the branch
47        BranchOperations::create(&request.name, request.from.as_deref())?;
48
49        Ok(BranchCreationResult {
50            branch_name: request.name,
51            base_commit: request.from,
52            backup_branch,
53            switched: true,
54        })
55    }
56
57    /// Delete branches with safety checks
58    pub fn delete_branches(&self, request: DeleteBranchesRequest) -> Result<BranchDeletionResult> {
59        let mut deleted = Vec::new();
60        let mut failed = Vec::new();
61        let mut protected = Vec::new();
62
63        // Filter out protected branches
64        let protected_branches = ["main", "master", "develop"];
65
66        for branch in &request.branches {
67            if protected_branches.contains(&branch.as_str()) {
68                protected.push(branch.clone());
69                continue;
70            }
71
72            if request.dry_run {
73                deleted.push(branch.clone()); // In dry run, assume all would succeed
74            } else {
75                match BranchOperations::delete(branch, request.force) {
76                    Ok(_) => deleted.push(branch.clone()),
77                    Err(_) => failed.push(branch.clone()),
78                }
79            }
80        }
81
82        Ok(BranchDeletionResult {
83            deleted,
84            failed,
85            protected,
86            dry_run: request.dry_run,
87        })
88    }
89
90    /// Get recent branches with filtering
91    pub fn get_recent_branches(
92        &self,
93        request: RecentBranchesRequest,
94    ) -> Result<RecentBranchesResult> {
95        let all_recent = GitOperations::recent_branches(request.limit)?;
96
97        // Filter out current branch and protected branches if requested
98        let current_branch = GitOperations::current_branch()?;
99        let protected_branches = ["main", "master", "develop"];
100
101        let filtered_branches = all_recent
102            .into_iter()
103            .filter(|branch| {
104                if request.exclude_current && branch == &current_branch {
105                    return false;
106                }
107                if request.exclude_protected && protected_branches.contains(&branch.as_str()) {
108                    return false;
109                }
110                true
111            })
112            .collect();
113
114        Ok(RecentBranchesResult {
115            branches: filtered_branches,
116            current_branch,
117        })
118    }
119
120    /// Switch to a branch with validation
121    pub fn switch_branch(&self, request: SwitchBranchRequest) -> Result<BranchSwitchResult> {
122        // Validate branch exists
123        if !BranchOperations::exists(&request.branch_name)? {
124            return Err(GitXError::GitCommand(format!(
125                "Branch '{}' does not exist",
126                request.branch_name
127            )));
128        }
129
130        // Check for uncommitted changes if strict mode
131        if request.strict_mode && !GitOperations::is_working_directory_clean()? {
132            return Err(GitXError::GitCommand(
133                "Working directory has uncommitted changes. Use --force or commit/stash changes."
134                    .to_string(),
135            ));
136        }
137
138        let previous_branch = GitOperations::current_branch()?;
139
140        // Create checkpoint if requested
141        let checkpoint = if request.create_checkpoint {
142            Some(Safety::create_checkpoint(Some(&format!(
143                "Before switching to {}",
144                request.branch_name
145            )))?)
146        } else {
147            None
148        };
149
150        // Perform the switch
151        BranchOperations::switch(&request.branch_name)?;
152
153        Ok(BranchSwitchResult {
154            previous_branch,
155            new_branch: request.branch_name,
156            checkpoint,
157        })
158    }
159
160    /// Rename current branch
161    pub fn rename_branch(&self, request: RenameBranchRequest) -> Result<BranchRenameResult> {
162        Validate::branch_name(&request.new_name)?;
163
164        let current_branch = GitOperations::current_branch()?;
165
166        if BranchOperations::exists(&request.new_name)? {
167            return Err(GitXError::GitCommand(format!(
168                "Branch '{}' already exists",
169                request.new_name
170            )));
171        }
172
173        // Create backup if requested
174        let backup_branch = if request.create_backup {
175            Some(Safety::create_backup_branch(Some("pre-rename"))?)
176        } else {
177            None
178        };
179
180        BranchOperations::rename(&request.new_name)?;
181
182        Ok(BranchRenameResult {
183            old_name: current_branch,
184            new_name: request.new_name,
185            backup_branch,
186        })
187    }
188
189    /// Clean merged branches
190    pub fn clean_merged_branches(
191        &self,
192        request: CleanBranchesRequest,
193    ) -> Result<CleanBranchesResult> {
194        let merged_branches = GitOperations::merged_branches()?;
195        let current_branch = GitOperations::current_branch()?;
196        let protected_branches = ["main", "master", "develop"];
197
198        let candidates: Vec<String> = merged_branches
199            .into_iter()
200            .filter(|branch| branch != &current_branch)
201            .filter(|branch| !protected_branches.contains(&branch.as_str()))
202            .collect();
203
204        if candidates.is_empty() {
205            return Ok(CleanBranchesResult {
206                candidates: vec![],
207                deleted: vec![],
208                failed: vec![],
209                dry_run: request.dry_run,
210            });
211        }
212
213        let mut deleted = Vec::new();
214        let mut failed = Vec::new();
215
216        if request.dry_run {
217            // In dry run, all candidates would be "deleted"
218            deleted = candidates.clone();
219        } else {
220            // Confirm operation if interactive
221            if request.confirm_deletion {
222                let details = format!(
223                    "This will delete {} merged branches: {}",
224                    candidates.len(),
225                    candidates.join(", ")
226                );
227
228                if !Safety::confirm_destructive_operation("Clean merged branches", &details)? {
229                    return Ok(CleanBranchesResult {
230                        candidates: candidates.clone(),
231                        deleted: vec![],
232                        failed: vec![],
233                        dry_run: false,
234                    });
235                }
236            }
237
238            // Perform deletions
239            for branch in &candidates {
240                match BranchOperations::delete(branch, false) {
241                    Ok(_) => deleted.push(branch.clone()),
242                    Err(_) => failed.push(branch.clone()),
243                }
244            }
245        }
246
247        Ok(CleanBranchesResult {
248            candidates,
249            deleted,
250            failed,
251            dry_run: request.dry_run,
252        })
253    }
254}
255
256// Request/Response DTOs for better type safety
257
258#[derive(Debug, Clone)]
259pub struct CreateBranchRequest {
260    pub name: String,
261    pub from: Option<String>,
262    pub create_backup: bool,
263}
264
265#[derive(Debug, Clone)]
266pub struct BranchCreationResult {
267    pub branch_name: String,
268    pub base_commit: Option<String>,
269    pub backup_branch: Option<String>,
270    pub switched: bool,
271}
272
273#[derive(Debug, Clone)]
274pub struct DeleteBranchesRequest {
275    pub branches: Vec<String>,
276    pub force: bool,
277    pub dry_run: bool,
278}
279
280#[derive(Debug, Clone)]
281pub struct BranchDeletionResult {
282    pub deleted: Vec<String>,
283    pub failed: Vec<String>,
284    pub protected: Vec<String>,
285    pub dry_run: bool,
286}
287
288#[derive(Debug, Clone)]
289pub struct RecentBranchesRequest {
290    pub limit: Option<usize>,
291    pub exclude_current: bool,
292    pub exclude_protected: bool,
293}
294
295#[derive(Debug, Clone)]
296pub struct RecentBranchesResult {
297    pub branches: Vec<String>,
298    pub current_branch: String,
299}
300
301#[derive(Debug, Clone)]
302pub struct SwitchBranchRequest {
303    pub branch_name: String,
304    pub strict_mode: bool,
305    pub create_checkpoint: bool,
306}
307
308#[derive(Debug, Clone)]
309pub struct BranchSwitchResult {
310    pub previous_branch: String,
311    pub new_branch: String,
312    pub checkpoint: Option<String>,
313}
314
315#[derive(Debug, Clone)]
316pub struct RenameBranchRequest {
317    pub new_name: String,
318    pub create_backup: bool,
319}
320
321#[derive(Debug, Clone)]
322pub struct BranchRenameResult {
323    pub old_name: String,
324    pub new_name: String,
325    pub backup_branch: Option<String>,
326}
327
328#[derive(Debug, Clone)]
329pub struct CleanBranchesRequest {
330    pub dry_run: bool,
331    pub confirm_deletion: bool,
332}
333
334#[derive(Debug, Clone)]
335pub struct CleanBranchesResult {
336    pub candidates: Vec<String>,
337    pub deleted: Vec<String>,
338    pub failed: Vec<String>,
339    pub dry_run: bool,
340}
341
342impl CleanBranchesResult {
343    /// Get a summary of the operation
344    pub fn summary(&self) -> String {
345        if self.dry_run {
346            format!("Would delete {} branches", self.candidates.len())
347        } else {
348            format!(
349                "Deleted {} branches, {} failed",
350                self.deleted.len(),
351                self.failed.len()
352            )
353        }
354    }
355}