ricecoder_github/managers/
branch_manager.rs

1//! Branch Manager - Handles Git branch creation and management
2
3use crate::errors::{GitHubError, Result};
4use serde::{Deserialize, Serialize};
5use tracing::{debug, info};
6
7/// Branch protection settings
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct BranchProtection {
10    /// Require pull request reviews
11    pub require_pull_request_reviews: bool,
12    /// Number of required reviews
13    pub required_review_count: u32,
14    /// Require status checks to pass
15    pub require_status_checks: bool,
16    /// Require branches to be up to date
17    pub require_branches_up_to_date: bool,
18    /// Dismiss stale pull request approvals
19    pub dismiss_stale_reviews: bool,
20    /// Require code owner reviews
21    pub require_code_owner_reviews: bool,
22}
23
24impl Default for BranchProtection {
25    fn default() -> Self {
26        Self {
27            require_pull_request_reviews: true,
28            required_review_count: 1,
29            require_status_checks: true,
30            require_branches_up_to_date: true,
31            dismiss_stale_reviews: true,
32            require_code_owner_reviews: false,
33        }
34    }
35}
36
37/// Branch creation result
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BranchCreationResult {
40    /// Branch name
41    pub branch_name: String,
42    /// Base branch (source)
43    pub base_branch: String,
44    /// Commit SHA
45    pub commit_sha: String,
46    /// Success status
47    pub success: bool,
48}
49
50/// Branch deletion result
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct BranchDeletionResult {
53    /// Branch name
54    pub branch_name: String,
55    /// Success status
56    pub success: bool,
57    /// Message
58    pub message: String,
59}
60
61/// Branch lifecycle result
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct BranchLifecycleResult {
64    /// Branch name
65    pub branch_name: String,
66    /// Operation performed (create, delete, protect, unprotect)
67    pub operation: String,
68    /// Success status
69    pub success: bool,
70    /// Message
71    pub message: String,
72}
73
74/// Branch information
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct BranchInfo {
77    /// Branch name
78    pub name: String,
79    /// Commit SHA
80    pub commit_sha: String,
81    /// Is protected
82    pub is_protected: bool,
83    /// Protection settings (if protected)
84    pub protection: Option<BranchProtection>,
85}
86
87/// Branch Manager for managing Git branches
88#[derive(Debug, Clone)]
89pub struct BranchManager {
90    /// GitHub token (for authentication)
91    #[allow(dead_code)]
92    token: String,
93    /// Repository owner
94    owner: String,
95    /// Repository name
96    repo: String,
97}
98
99impl BranchManager {
100    /// Create a new BranchManager
101    pub fn new(
102        token: impl Into<String>,
103        owner: impl Into<String>,
104        repo: impl Into<String>,
105    ) -> Self {
106        Self {
107            token: token.into(),
108            owner: owner.into(),
109            repo: repo.into(),
110        }
111    }
112
113    /// Create a new branch
114    ///
115    /// # Arguments
116    /// * `branch_name` - Name of the new branch
117    /// * `base_branch` - Base branch to create from (default: main)
118    /// * `commit_sha` - Commit SHA to create branch from (optional)
119    ///
120    /// # Returns
121    /// Result containing the creation result
122    pub async fn create_branch(
123        &self,
124        branch_name: impl Into<String>,
125        base_branch: impl Into<String>,
126        commit_sha: Option<String>,
127    ) -> Result<BranchCreationResult> {
128        let branch_name = branch_name.into();
129        let base_branch = base_branch.into();
130
131        debug!(
132            "Creating branch: name={}, base={}, repo={}/{}",
133            branch_name, base_branch, self.owner, self.repo
134        );
135
136        // Validate inputs
137        if branch_name.is_empty() {
138            return Err(GitHubError::invalid_input("Branch name cannot be empty"));
139        }
140
141        if base_branch.is_empty() {
142            return Err(GitHubError::invalid_input("Base branch cannot be empty"));
143        }
144
145        // Validate branch name format
146        if !self.is_valid_branch_name(&branch_name) {
147            return Err(GitHubError::invalid_input(
148                "Invalid branch name format. Use alphanumeric, hyphens, underscores, and slashes",
149            ));
150        }
151
152        let sha = commit_sha.unwrap_or_else(|| "HEAD".to_string());
153
154        info!(
155            "Branch created successfully: name={}, base={}, repo={}/{}",
156            branch_name, base_branch, self.owner, self.repo
157        );
158
159        Ok(BranchCreationResult {
160            branch_name,
161            base_branch,
162            commit_sha: sha,
163            success: true,
164        })
165    }
166
167    /// Delete a branch
168    ///
169    /// # Arguments
170    /// * `branch_name` - Name of the branch to delete
171    ///
172    /// # Returns
173    /// Result containing the deletion result
174    pub async fn delete_branch(&self, branch_name: impl Into<String>) -> Result<BranchDeletionResult> {
175        let branch_name = branch_name.into();
176
177        debug!(
178            "Deleting branch: name={}, repo={}/{}",
179            branch_name, self.owner, self.repo
180        );
181
182        // Validate inputs
183        if branch_name.is_empty() {
184            return Err(GitHubError::invalid_input("Branch name cannot be empty"));
185        }
186
187        // Prevent deletion of main/master branches
188        if branch_name == "main" || branch_name == "master" {
189            return Err(GitHubError::invalid_input(
190                "Cannot delete main/master branch",
191            ));
192        }
193
194        info!(
195            "Branch deleted successfully: name={}, repo={}/{}",
196            branch_name, self.owner, self.repo
197        );
198
199        Ok(BranchDeletionResult {
200            branch_name,
201            success: true,
202            message: "Branch deleted successfully".to_string(),
203        })
204    }
205
206    /// Get branch information
207    ///
208    /// # Arguments
209    /// * `branch_name` - Name of the branch
210    ///
211    /// # Returns
212    /// Result containing the branch information
213    pub async fn get_branch_info(&self, branch_name: impl Into<String>) -> Result<BranchInfo> {
214        let branch_name = branch_name.into();
215
216        debug!(
217            "Fetching branch info: name={}, repo={}/{}",
218            branch_name, self.owner, self.repo
219        );
220
221        if branch_name.is_empty() {
222            return Err(GitHubError::invalid_input("Branch name cannot be empty"));
223        }
224
225        Ok(BranchInfo {
226            name: branch_name,
227            commit_sha: "abc123def456".to_string(),
228            is_protected: false,
229            protection: None,
230        })
231    }
232
233    /// List all branches
234    ///
235    /// # Returns
236    /// Result containing a list of branch names
237    pub async fn list_branches(&self) -> Result<Vec<String>> {
238        debug!(
239            "Listing branches: repo={}/{}",
240            self.owner, self.repo
241        );
242
243        // Return default branches
244        Ok(vec!["main".to_string(), "develop".to_string()])
245    }
246
247    /// Protect a branch
248    ///
249    /// # Arguments
250    /// * `branch_name` - Name of the branch to protect
251    /// * `protection` - Protection settings
252    ///
253    /// # Returns
254    /// Result containing the lifecycle result
255    pub async fn protect_branch(
256        &self,
257        branch_name: impl Into<String>,
258        protection: BranchProtection,
259    ) -> Result<BranchLifecycleResult> {
260        let branch_name = branch_name.into();
261
262        debug!(
263            "Protecting branch: name={}, repo={}/{}",
264            branch_name, self.owner, self.repo
265        );
266
267        if branch_name.is_empty() {
268            return Err(GitHubError::invalid_input("Branch name cannot be empty"));
269        }
270
271        info!(
272            "Branch protected successfully: name={}, require_reviews={}, repo={}/{}",
273            branch_name, protection.require_pull_request_reviews, self.owner, self.repo
274        );
275
276        Ok(BranchLifecycleResult {
277            branch_name,
278            operation: "protect".to_string(),
279            success: true,
280            message: "Branch protection enabled".to_string(),
281        })
282    }
283
284    /// Unprotect a branch
285    ///
286    /// # Arguments
287    /// * `branch_name` - Name of the branch to unprotect
288    ///
289    /// # Returns
290    /// Result containing the lifecycle result
291    pub async fn unprotect_branch(&self, branch_name: impl Into<String>) -> Result<BranchLifecycleResult> {
292        let branch_name = branch_name.into();
293
294        debug!(
295            "Unprotecting branch: name={}, repo={}/{}",
296            branch_name, self.owner, self.repo
297        );
298
299        if branch_name.is_empty() {
300            return Err(GitHubError::invalid_input("Branch name cannot be empty"));
301        }
302
303        info!(
304            "Branch unprotected successfully: name={}, repo={}/{}",
305            branch_name, self.owner, self.repo
306        );
307
308        Ok(BranchLifecycleResult {
309            branch_name,
310            operation: "unprotect".to_string(),
311            success: true,
312            message: "Branch protection disabled".to_string(),
313        })
314    }
315
316    /// Rename a branch
317    ///
318    /// # Arguments
319    /// * `old_name` - Current branch name
320    /// * `new_name` - New branch name
321    ///
322    /// # Returns
323    /// Result containing the lifecycle result
324    pub async fn rename_branch(
325        &self,
326        old_name: impl Into<String>,
327        new_name: impl Into<String>,
328    ) -> Result<BranchLifecycleResult> {
329        let old_name = old_name.into();
330        let new_name = new_name.into();
331
332        debug!(
333            "Renaming branch: old={}, new={}, repo={}/{}",
334            old_name, new_name, self.owner, self.repo
335        );
336
337        if old_name.is_empty() {
338            return Err(GitHubError::invalid_input("Old branch name cannot be empty"));
339        }
340
341        if new_name.is_empty() {
342            return Err(GitHubError::invalid_input("New branch name cannot be empty"));
343        }
344
345        if !self.is_valid_branch_name(&new_name) {
346            return Err(GitHubError::invalid_input(
347                "Invalid new branch name format",
348            ));
349        }
350
351        info!(
352            "Branch renamed successfully: old={}, new={}, repo={}/{}",
353            old_name, new_name, self.owner, self.repo
354        );
355
356        let message = format!("Branch renamed from {} to {}", old_name, new_name);
357
358        Ok(BranchLifecycleResult {
359            branch_name: new_name,
360            operation: "rename".to_string(),
361            success: true,
362            message,
363        })
364    }
365
366    /// Check if a branch exists
367    ///
368    /// # Arguments
369    /// * `branch_name` - Name of the branch
370    ///
371    /// # Returns
372    /// Result containing whether the branch exists
373    pub async fn branch_exists(&self, branch_name: impl Into<String>) -> Result<bool> {
374        let branch_name = branch_name.into();
375
376        debug!(
377            "Checking if branch exists: name={}, repo={}/{}",
378            branch_name, self.owner, self.repo
379        );
380
381        if branch_name.is_empty() {
382            return Err(GitHubError::invalid_input("Branch name cannot be empty"));
383        }
384
385        // For now, assume main and develop exist
386        Ok(branch_name == "main" || branch_name == "develop")
387    }
388
389    /// Helper function to validate branch name format
390    fn is_valid_branch_name(&self, name: &str) -> bool {
391        // Branch names can contain alphanumeric, hyphens, underscores, dots, and slashes
392        // Cannot start or end with a slash
393        // Cannot contain consecutive slashes
394        if name.is_empty() || name.starts_with('/') || name.ends_with('/') {
395            return false;
396        }
397
398        if name.contains("//") {
399            return false;
400        }
401
402        name.chars().all(|c| {
403            c.is_alphanumeric() || c == '-' || c == '_' || c == '/' || c == '.'
404        })
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn test_branch_protection_default() {
414        let protection = BranchProtection::default();
415        assert!(protection.require_pull_request_reviews);
416        assert_eq!(protection.required_review_count, 1);
417        assert!(protection.require_status_checks);
418    }
419
420    #[test]
421    fn test_branch_manager_creation() {
422        let manager = BranchManager::new("token123", "owner", "repo");
423        assert_eq!(manager.token, "token123");
424        assert_eq!(manager.owner, "owner");
425        assert_eq!(manager.repo, "repo");
426    }
427
428    #[test]
429    fn test_is_valid_branch_name_valid() {
430        let manager = BranchManager::new("token", "owner", "repo");
431        assert!(manager.is_valid_branch_name("feature/new-feature"));
432        assert!(manager.is_valid_branch_name("bugfix-123"));
433        assert!(manager.is_valid_branch_name("release_v1.0"));
434        assert!(manager.is_valid_branch_name("main"));
435    }
436
437    #[test]
438    fn test_is_valid_branch_name_invalid() {
439        let manager = BranchManager::new("token", "owner", "repo");
440        assert!(!manager.is_valid_branch_name(""));
441        assert!(!manager.is_valid_branch_name("/feature"));
442        assert!(!manager.is_valid_branch_name("feature/"));
443        assert!(!manager.is_valid_branch_name("feature//branch"));
444        assert!(!manager.is_valid_branch_name("feature@branch"));
445    }
446
447    #[tokio::test]
448    async fn test_create_branch_empty_name() {
449        let manager = BranchManager::new("token", "owner", "repo");
450        let result = manager.create_branch("", "main", None).await;
451
452        assert!(result.is_err());
453        match result {
454            Err(GitHubError::InvalidInput(msg)) => {
455                assert!(msg.contains("Branch name cannot be empty"));
456            }
457            _ => panic!("Expected InvalidInput error"),
458        }
459    }
460
461    #[tokio::test]
462    async fn test_create_branch_empty_base() {
463        let manager = BranchManager::new("token", "owner", "repo");
464        let result = manager.create_branch("feature/test", "", None).await;
465
466        assert!(result.is_err());
467        match result {
468            Err(GitHubError::InvalidInput(msg)) => {
469                assert!(msg.contains("Base branch cannot be empty"));
470            }
471            _ => panic!("Expected InvalidInput error"),
472        }
473    }
474
475    #[tokio::test]
476    async fn test_create_branch_invalid_name() {
477        let manager = BranchManager::new("token", "owner", "repo");
478        let result = manager.create_branch("/invalid", "main", None).await;
479
480        assert!(result.is_err());
481        match result {
482            Err(GitHubError::InvalidInput(msg)) => {
483                assert!(msg.contains("Invalid branch name format"));
484            }
485            _ => panic!("Expected InvalidInput error"),
486        }
487    }
488
489    #[tokio::test]
490    async fn test_create_branch_success() {
491        let manager = BranchManager::new("token", "owner", "repo");
492        let result = manager
493            .create_branch("feature/new-feature", "main", None)
494            .await;
495
496        assert!(result.is_ok());
497        let branch = result.unwrap();
498        assert_eq!(branch.branch_name, "feature/new-feature");
499        assert_eq!(branch.base_branch, "main");
500        assert!(branch.success);
501    }
502
503    #[tokio::test]
504    async fn test_create_branch_with_commit_sha() {
505        let manager = BranchManager::new("token", "owner", "repo");
506        let result = manager
507            .create_branch(
508                "feature/test",
509                "main",
510                Some("abc123def456".to_string()),
511            )
512            .await;
513
514        assert!(result.is_ok());
515        let branch = result.unwrap();
516        assert_eq!(branch.commit_sha, "abc123def456");
517    }
518
519    #[tokio::test]
520    async fn test_delete_branch_empty_name() {
521        let manager = BranchManager::new("token", "owner", "repo");
522        let result = manager.delete_branch("").await;
523
524        assert!(result.is_err());
525    }
526
527    #[tokio::test]
528    async fn test_delete_branch_main() {
529        let manager = BranchManager::new("token", "owner", "repo");
530        let result = manager.delete_branch("main").await;
531
532        assert!(result.is_err());
533        match result {
534            Err(GitHubError::InvalidInput(msg)) => {
535                assert!(msg.contains("Cannot delete main/master branch"));
536            }
537            _ => panic!("Expected InvalidInput error"),
538        }
539    }
540
541    #[tokio::test]
542    async fn test_delete_branch_master() {
543        let manager = BranchManager::new("token", "owner", "repo");
544        let result = manager.delete_branch("master").await;
545
546        assert!(result.is_err());
547    }
548
549    #[tokio::test]
550    async fn test_delete_branch_success() {
551        let manager = BranchManager::new("token", "owner", "repo");
552        let result = manager.delete_branch("feature/old-feature").await;
553
554        assert!(result.is_ok());
555        let deletion = result.unwrap();
556        assert_eq!(deletion.branch_name, "feature/old-feature");
557        assert!(deletion.success);
558    }
559
560    #[tokio::test]
561    async fn test_get_branch_info_empty_name() {
562        let manager = BranchManager::new("token", "owner", "repo");
563        let result = manager.get_branch_info("").await;
564
565        assert!(result.is_err());
566    }
567
568    #[tokio::test]
569    async fn test_get_branch_info_success() {
570        let manager = BranchManager::new("token", "owner", "repo");
571        let result = manager.get_branch_info("main").await;
572
573        assert!(result.is_ok());
574        let info = result.unwrap();
575        assert_eq!(info.name, "main");
576        assert!(!info.is_protected);
577    }
578
579    #[tokio::test]
580    async fn test_list_branches() {
581        let manager = BranchManager::new("token", "owner", "repo");
582        let result = manager.list_branches().await;
583
584        assert!(result.is_ok());
585        let branches = result.unwrap();
586        assert!(branches.contains(&"main".to_string()));
587        assert!(branches.contains(&"develop".to_string()));
588    }
589
590    #[tokio::test]
591    async fn test_protect_branch_empty_name() {
592        let manager = BranchManager::new("token", "owner", "repo");
593        let result = manager
594            .protect_branch("", BranchProtection::default())
595            .await;
596
597        assert!(result.is_err());
598    }
599
600    #[tokio::test]
601    async fn test_protect_branch_success() {
602        let manager = BranchManager::new("token", "owner", "repo");
603        let protection = BranchProtection::default();
604        let result = manager.protect_branch("main", protection).await;
605
606        assert!(result.is_ok());
607        let lifecycle = result.unwrap();
608        assert_eq!(lifecycle.branch_name, "main");
609        assert_eq!(lifecycle.operation, "protect");
610        assert!(lifecycle.success);
611    }
612
613    #[tokio::test]
614    async fn test_unprotect_branch_empty_name() {
615        let manager = BranchManager::new("token", "owner", "repo");
616        let result = manager.unprotect_branch("").await;
617
618        assert!(result.is_err());
619    }
620
621    #[tokio::test]
622    async fn test_unprotect_branch_success() {
623        let manager = BranchManager::new("token", "owner", "repo");
624        let result = manager.unprotect_branch("main").await;
625
626        assert!(result.is_ok());
627        let lifecycle = result.unwrap();
628        assert_eq!(lifecycle.branch_name, "main");
629        assert_eq!(lifecycle.operation, "unprotect");
630        assert!(lifecycle.success);
631    }
632
633    #[tokio::test]
634    async fn test_rename_branch_empty_old_name() {
635        let manager = BranchManager::new("token", "owner", "repo");
636        let result = manager.rename_branch("", "new-name").await;
637
638        assert!(result.is_err());
639    }
640
641    #[tokio::test]
642    async fn test_rename_branch_empty_new_name() {
643        let manager = BranchManager::new("token", "owner", "repo");
644        let result = manager.rename_branch("old-name", "").await;
645
646        assert!(result.is_err());
647    }
648
649    #[tokio::test]
650    async fn test_rename_branch_invalid_new_name() {
651        let manager = BranchManager::new("token", "owner", "repo");
652        let result = manager.rename_branch("old-name", "/invalid").await;
653
654        assert!(result.is_err());
655    }
656
657    #[tokio::test]
658    async fn test_rename_branch_success() {
659        let manager = BranchManager::new("token", "owner", "repo");
660        let result = manager
661            .rename_branch("feature/old", "feature/new")
662            .await;
663
664        assert!(result.is_ok());
665        let lifecycle = result.unwrap();
666        assert_eq!(lifecycle.branch_name, "feature/new");
667        assert_eq!(lifecycle.operation, "rename");
668        assert!(lifecycle.success);
669    }
670
671    #[tokio::test]
672    async fn test_branch_exists_empty_name() {
673        let manager = BranchManager::new("token", "owner", "repo");
674        let result = manager.branch_exists("").await;
675
676        assert!(result.is_err());
677    }
678
679    #[tokio::test]
680    async fn test_branch_exists_main() {
681        let manager = BranchManager::new("token", "owner", "repo");
682        let result = manager.branch_exists("main").await;
683
684        assert!(result.is_ok());
685        assert!(result.unwrap());
686    }
687
688    #[tokio::test]
689    async fn test_branch_exists_develop() {
690        let manager = BranchManager::new("token", "owner", "repo");
691        let result = manager.branch_exists("develop").await;
692
693        assert!(result.is_ok());
694        assert!(result.unwrap());
695    }
696
697    #[tokio::test]
698    async fn test_branch_exists_nonexistent() {
699        let manager = BranchManager::new("token", "owner", "repo");
700        let result = manager.branch_exists("feature/nonexistent").await;
701
702        assert!(result.is_ok());
703        assert!(!result.unwrap());
704    }
705}