cascade_cli/git/
branch_manager.rs

1use crate::errors::Result;
2use crate::git::GitRepository;
3use serde::{Deserialize, Serialize};
4
5/// Information about a branch
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct BranchInfo {
8    pub name: String,
9    pub commit_hash: String,
10    pub is_current: bool,
11    pub upstream: Option<String>,
12}
13
14/// Manages branch operations and metadata
15pub struct BranchManager {
16    git_repo: GitRepository,
17}
18
19impl BranchManager {
20    /// Create a new BranchManager
21    pub fn new(git_repo: GitRepository) -> Self {
22        Self { git_repo }
23    }
24
25    /// Get information about all branches
26    pub fn get_branch_info(&self) -> Result<Vec<BranchInfo>> {
27        let branches = self.git_repo.list_branches()?;
28        let current_branch = self.git_repo.get_current_branch().ok();
29
30        let mut branch_info = Vec::new();
31        for branch_name in branches {
32            let commit_hash = self.get_branch_commit_hash(&branch_name)?;
33            let is_current = current_branch.as_ref() == Some(&branch_name);
34
35            branch_info.push(BranchInfo {
36                name: branch_name,
37                commit_hash,
38                is_current,
39                upstream: None, // TODO: Implement upstream tracking
40            });
41        }
42
43        Ok(branch_info)
44    }
45
46    /// Get the commit hash for a specific branch safely without switching branches
47    fn get_branch_commit_hash(&self, branch_name: &str) -> Result<String> {
48        self.git_repo.get_branch_commit_hash(branch_name)
49    }
50
51    /// Generate a safe branch name from a commit message
52    pub fn generate_branch_name(&self, message: &str) -> String {
53        let base_name = message
54            .to_lowercase()
55            .chars()
56            .map(|c| match c {
57                'a'..='z' | '0'..='9' => c,
58                _ => '-',
59            })
60            .collect::<String>()
61            .split('-')
62            .filter(|s| !s.is_empty())
63            .take(5) // Limit to first 5 words
64            .collect::<Vec<_>>()
65            .join("-");
66
67        // Ensure the branch name is unique
68        let mut counter = 1;
69        let mut candidate = base_name.clone();
70
71        while self.git_repo.branch_exists(&candidate) {
72            candidate = format!("{base_name}-{counter}");
73            counter += 1;
74        }
75
76        // Ensure it starts with a letter
77        if candidate.chars().next().is_none_or(|c| !c.is_alphabetic()) {
78            candidate = format!("feature-{candidate}");
79        }
80
81        candidate
82    }
83
84    /// Create a new branch with a generated name
85    pub fn create_branch_from_message(
86        &self,
87        message: &str,
88        target: Option<&str>,
89    ) -> Result<String> {
90        let branch_name = self.generate_branch_name(message);
91        self.git_repo.create_branch(&branch_name, target)?;
92        Ok(branch_name)
93    }
94
95    /// Get the underlying Git repository
96    pub fn git_repo(&self) -> &GitRepository {
97        &self.git_repo
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::git::repository::*;
105    use git2::{Repository, Signature};
106    use tempfile::TempDir;
107
108    fn create_test_branch_manager() -> (TempDir, BranchManager) {
109        let temp_dir = TempDir::new().unwrap();
110        let repo_path = temp_dir.path();
111
112        // Initialize repository
113        let repo = Repository::init(repo_path).unwrap();
114
115        // Create initial commit
116        let signature = Signature::now("Test User", "test@example.com").unwrap();
117        let tree_id = {
118            let mut index = repo.index().unwrap();
119            index.write_tree().unwrap()
120        };
121        let tree = repo.find_tree(tree_id).unwrap();
122
123        repo.commit(
124            Some("HEAD"),
125            &signature,
126            &signature,
127            "Initial commit",
128            &tree,
129            &[],
130        )
131        .unwrap();
132
133        let git_repo = GitRepository::open(repo_path).unwrap();
134        let branch_manager = BranchManager::new(git_repo);
135
136        (temp_dir, branch_manager)
137    }
138
139    #[test]
140    fn test_branch_name_generation() {
141        let (_temp_dir, branch_manager) = create_test_branch_manager();
142
143        assert_eq!(
144            branch_manager.generate_branch_name("Add user authentication"),
145            "add-user-authentication"
146        );
147
148        assert_eq!(
149            branch_manager.generate_branch_name("Fix bug in payment system!!!"),
150            "fix-bug-in-payment-system"
151        );
152
153        assert_eq!(
154            branch_manager.generate_branch_name("123 numeric start"),
155            "feature-123-numeric-start"
156        );
157    }
158
159    #[test]
160    fn test_branch_creation() {
161        let (_temp_dir, branch_manager) = create_test_branch_manager();
162
163        let branch_name = branch_manager
164            .create_branch_from_message("Add login feature", None)
165            .unwrap();
166
167        assert_eq!(branch_name, "add-login-feature");
168        assert!(branch_manager.git_repo().branch_exists(&branch_name));
169    }
170
171    #[test]
172    fn test_branch_info() {
173        let (_temp_dir, branch_manager) = create_test_branch_manager();
174
175        // Create a test branch
176        let _branch_name = branch_manager
177            .create_branch_from_message("Test feature", None)
178            .unwrap();
179
180        let branch_info = branch_manager.get_branch_info().unwrap();
181        assert!(!branch_info.is_empty());
182
183        // Should have at least one branch (the default branch, whether it's "main" or "master")
184        // and at least one should be marked as current
185        assert!(branch_info.iter().any(|b| b.is_current));
186
187        // Should have at least 2 branches (default + the test feature branch we created)
188        assert!(branch_info.len() >= 2);
189    }
190}