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
47    fn get_branch_commit_hash(&self, branch_name: &str) -> Result<String> {
48        // For now, we'll use a simple approach
49        // In Phase 2, we'll implement more sophisticated commit tracking
50        if branch_name == self.git_repo.get_current_branch()? {
51            self.git_repo.get_head_commit_hash()
52        } else {
53            // Switch to branch temporarily to get its commit hash
54            // This is not ideal but works for Phase 1
55            let current_branch = self.git_repo.get_current_branch()?;
56            self.git_repo.checkout_branch(branch_name)?;
57            let commit_hash = self.git_repo.get_head_commit_hash()?;
58            self.git_repo.checkout_branch(&current_branch)?;
59            Ok(commit_hash)
60        }
61    }
62
63    /// Generate a safe branch name from a commit message
64    pub fn generate_branch_name(&self, message: &str) -> String {
65        let base_name = message
66            .to_lowercase()
67            .chars()
68            .map(|c| match c {
69                'a'..='z' | '0'..='9' => c,
70                _ => '-',
71            })
72            .collect::<String>()
73            .split('-')
74            .filter(|s| !s.is_empty())
75            .take(5) // Limit to first 5 words
76            .collect::<Vec<_>>()
77            .join("-");
78
79        // Ensure the branch name is unique
80        let mut counter = 1;
81        let mut candidate = base_name.clone();
82
83        while self.git_repo.branch_exists(&candidate) {
84            candidate = format!("{base_name}-{counter}");
85            counter += 1;
86        }
87
88        // Ensure it starts with a letter
89        if candidate.chars().next().is_none_or(|c| !c.is_alphabetic()) {
90            candidate = format!("feature-{candidate}");
91        }
92
93        candidate
94    }
95
96    /// Create a new branch with a generated name
97    pub fn create_branch_from_message(
98        &self,
99        message: &str,
100        target: Option<&str>,
101    ) -> Result<String> {
102        let branch_name = self.generate_branch_name(message);
103        self.git_repo.create_branch(&branch_name, target)?;
104        Ok(branch_name)
105    }
106
107    /// Get the underlying Git repository
108    pub fn git_repo(&self) -> &GitRepository {
109        &self.git_repo
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::git::repository::*;
117    use git2::{Repository, Signature};
118    use tempfile::TempDir;
119
120    fn create_test_branch_manager() -> (TempDir, BranchManager) {
121        let temp_dir = TempDir::new().unwrap();
122        let repo_path = temp_dir.path();
123
124        // Initialize repository
125        let repo = Repository::init(repo_path).unwrap();
126
127        // Create initial commit
128        let signature = Signature::now("Test User", "test@example.com").unwrap();
129        let tree_id = {
130            let mut index = repo.index().unwrap();
131            index.write_tree().unwrap()
132        };
133        let tree = repo.find_tree(tree_id).unwrap();
134
135        repo.commit(
136            Some("HEAD"),
137            &signature,
138            &signature,
139            "Initial commit",
140            &tree,
141            &[],
142        )
143        .unwrap();
144
145        let git_repo = GitRepository::open(repo_path).unwrap();
146        let branch_manager = BranchManager::new(git_repo);
147
148        (temp_dir, branch_manager)
149    }
150
151    #[test]
152    fn test_branch_name_generation() {
153        let (_temp_dir, branch_manager) = create_test_branch_manager();
154
155        assert_eq!(
156            branch_manager.generate_branch_name("Add user authentication"),
157            "add-user-authentication"
158        );
159
160        assert_eq!(
161            branch_manager.generate_branch_name("Fix bug in payment system!!!"),
162            "fix-bug-in-payment-system"
163        );
164
165        assert_eq!(
166            branch_manager.generate_branch_name("123 numeric start"),
167            "feature-123-numeric-start"
168        );
169    }
170
171    #[test]
172    fn test_branch_creation() {
173        let (_temp_dir, branch_manager) = create_test_branch_manager();
174
175        let branch_name = branch_manager
176            .create_branch_from_message("Add login feature", None)
177            .unwrap();
178
179        assert_eq!(branch_name, "add-login-feature");
180        assert!(branch_manager.git_repo().branch_exists(&branch_name));
181    }
182
183    #[test]
184    fn test_branch_info() {
185        let (_temp_dir, branch_manager) = create_test_branch_manager();
186
187        // Create a test branch
188        let _branch_name = branch_manager
189            .create_branch_from_message("Test feature", None)
190            .unwrap();
191
192        let branch_info = branch_manager.get_branch_info().unwrap();
193        assert!(!branch_info.is_empty());
194
195        // Should have at least one branch (the default branch, whether it's "main" or "master")
196        // and at least one should be marked as current
197        assert!(branch_info.iter().any(|b| b.is_current));
198
199        // Should have at least 2 branches (default + the test feature branch we created)
200        assert!(branch_info.len() >= 2);
201    }
202}