cascade_cli/git/
branch_manager.rs1use crate::errors::Result;
2use crate::git::GitRepository;
3use serde::{Deserialize, Serialize};
4
5#[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
14pub struct BranchManager {
16 git_repo: GitRepository,
17}
18
19impl BranchManager {
20 pub fn new(git_repo: GitRepository) -> Self {
22 Self { git_repo }
23 }
24
25 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, });
41 }
42
43 Ok(branch_info)
44 }
45
46 fn get_branch_commit_hash(&self, branch_name: &str) -> Result<String> {
48 if branch_name == self.git_repo.get_current_branch()? {
51 self.git_repo.get_head_commit_hash()
52 } else {
53 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(¤t_branch)?;
59 Ok(commit_hash)
60 }
61 }
62
63 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) .collect::<Vec<_>>()
77 .join("-");
78
79 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 if candidate.chars().next().is_none_or(|c| !c.is_alphabetic()) {
90 candidate = format!("feature-{candidate}");
91 }
92
93 candidate
94 }
95
96 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 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 let repo = Repository::init(repo_path).unwrap();
126
127 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 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 assert!(branch_info.iter().any(|b| b.is_current));
198
199 assert!(branch_info.len() >= 2);
201 }
202}