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 self.git_repo.get_branch_commit_hash(branch_name)
49 }
50
51 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) .collect::<Vec<_>>()
65 .join("-");
66
67 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 if candidate.chars().next().is_none_or(|c| !c.is_alphabetic()) {
78 candidate = format!("feature-{candidate}");
79 }
80
81 candidate
82 }
83
84 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 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 let repo = Repository::init(repo_path).unwrap();
114
115 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 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 assert!(branch_info.iter().any(|b| b.is_current));
186
187 assert!(branch_info.len() >= 2);
189 }
190}