1use crate::errors::Result;
2use crate::git::GitRepository;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct UpstreamInfo {
8 pub remote: String,
9 pub branch: String,
10 pub full_name: String, pub ahead: usize, pub behind: usize, }
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct BranchInfo {
18 pub name: String,
19 pub commit_hash: String,
20 pub is_current: bool,
21 pub upstream: Option<UpstreamInfo>,
22}
23
24pub struct BranchManager {
26 git_repo: GitRepository,
27}
28
29impl BranchManager {
30 pub fn new(git_repo: GitRepository) -> Self {
32 Self { git_repo }
33 }
34
35 pub fn get_branch_info(&self) -> Result<Vec<BranchInfo>> {
37 let branches = self.git_repo.list_branches()?;
38 let current_branch = self.git_repo.get_current_branch().ok();
39
40 let mut branch_info = Vec::new();
41 for branch_name in branches {
42 let commit_hash = self.get_branch_commit_hash(&branch_name)?;
43 let is_current = current_branch.as_ref() == Some(&branch_name);
44 let upstream = self.get_upstream_info(&branch_name)?;
45
46 branch_info.push(BranchInfo {
47 name: branch_name,
48 commit_hash,
49 is_current,
50 upstream,
51 });
52 }
53
54 Ok(branch_info)
55 }
56
57 fn get_branch_commit_hash(&self, branch_name: &str) -> Result<String> {
59 self.git_repo.get_branch_commit_hash(branch_name)
60 }
61
62 fn get_upstream_info(&self, branch_name: &str) -> Result<Option<UpstreamInfo>> {
64 if let Some(upstream) = self.git_repo.get_upstream_branch(branch_name)? {
66 let (remote, remote_branch) = self.parse_upstream_name(&upstream)?;
67
68 let (ahead, behind) = self.calculate_ahead_behind_counts(branch_name, &upstream)?;
70
71 Ok(Some(UpstreamInfo {
72 remote,
73 branch: remote_branch,
74 full_name: upstream,
75 ahead,
76 behind,
77 }))
78 } else {
79 Ok(None)
81 }
82 }
83
84 fn parse_upstream_name(&self, upstream: &str) -> Result<(String, String)> {
86 let parts: Vec<&str> = upstream.splitn(2, '/').collect();
87 if parts.len() == 2 {
88 Ok((parts[0].to_string(), parts[1].to_string()))
89 } else {
90 Ok(("origin".to_string(), upstream.to_string()))
92 }
93 }
94
95 fn calculate_ahead_behind_counts(
97 &self,
98 local_branch: &str,
99 upstream_branch: &str,
100 ) -> Result<(usize, usize)> {
101 match self
102 .git_repo
103 .get_ahead_behind_counts(local_branch, upstream_branch)
104 {
105 Ok((ahead, behind)) => Ok((ahead, behind)),
106 Err(_) => {
107 Ok((0, 0))
109 }
110 }
111 }
112
113 pub fn generate_branch_name(&self, message: &str) -> String {
115 let base_name = message
116 .to_lowercase()
117 .chars()
118 .map(|c| match c {
119 'a'..='z' | '0'..='9' => c,
120 _ => '-',
121 })
122 .collect::<String>()
123 .split('-')
124 .filter(|s| !s.is_empty())
125 .take(5) .collect::<Vec<_>>()
127 .join("-");
128
129 let mut counter = 1;
131 let mut candidate = base_name.clone();
132
133 while self.git_repo.branch_exists(&candidate) {
134 candidate = format!("{base_name}-{counter}");
135 counter += 1;
136 }
137
138 if candidate.chars().next().is_none_or(|c| !c.is_alphabetic()) {
140 candidate = format!("feature-{candidate}");
141 }
142
143 candidate
144 }
145
146 pub fn create_branch_from_message(
148 &self,
149 message: &str,
150 target: Option<&str>,
151 ) -> Result<String> {
152 let branch_name = self.generate_branch_name(message);
153 self.git_repo.create_branch(&branch_name, target)?;
154 Ok(branch_name)
155 }
156
157 pub fn set_upstream(&self, branch_name: &str, remote: &str, remote_branch: &str) -> Result<()> {
159 self.git_repo
160 .set_upstream(branch_name, remote, remote_branch)
161 }
162
163 pub fn get_branch_upstream(&self, branch_name: &str) -> Result<Option<UpstreamInfo>> {
165 self.get_upstream_info(branch_name)
166 }
167
168 pub fn has_upstream(&self, branch_name: &str) -> Result<bool> {
170 Ok(self.get_upstream_info(branch_name)?.is_some())
171 }
172
173 pub fn git_repo(&self) -> &GitRepository {
175 &self.git_repo
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::git::repository::*;
183 use git2::{Repository, Signature};
184 use tempfile::TempDir;
185
186 fn create_test_branch_manager() -> (TempDir, BranchManager) {
187 let temp_dir = TempDir::new().unwrap();
188 let repo_path = temp_dir.path();
189
190 let repo = Repository::init(repo_path).unwrap();
192
193 let signature = Signature::now("Test User", "test@example.com").unwrap();
195 let tree_id = {
196 let mut index = repo.index().unwrap();
197 index.write_tree().unwrap()
198 };
199 let tree = repo.find_tree(tree_id).unwrap();
200
201 repo.commit(
202 Some("HEAD"),
203 &signature,
204 &signature,
205 "Initial commit",
206 &tree,
207 &[],
208 )
209 .unwrap();
210
211 let git_repo = GitRepository::open(repo_path).unwrap();
212 let branch_manager = BranchManager::new(git_repo);
213
214 (temp_dir, branch_manager)
215 }
216
217 #[test]
218 fn test_branch_name_generation() {
219 let (_temp_dir, branch_manager) = create_test_branch_manager();
220
221 assert_eq!(
222 branch_manager.generate_branch_name("Add user authentication"),
223 "add-user-authentication"
224 );
225
226 assert_eq!(
227 branch_manager.generate_branch_name("Fix bug in payment system!!!"),
228 "fix-bug-in-payment-system"
229 );
230
231 assert_eq!(
232 branch_manager.generate_branch_name("123 numeric start"),
233 "feature-123-numeric-start"
234 );
235 }
236
237 #[test]
238 fn test_branch_creation() {
239 let (_temp_dir, branch_manager) = create_test_branch_manager();
240
241 let branch_name = branch_manager
242 .create_branch_from_message("Add login feature", None)
243 .unwrap();
244
245 assert_eq!(branch_name, "add-login-feature");
246 assert!(branch_manager.git_repo().branch_exists(&branch_name));
247 }
248
249 #[test]
250 fn test_branch_info() {
251 let (_temp_dir, branch_manager) = create_test_branch_manager();
252
253 let _branch_name = branch_manager
255 .create_branch_from_message("Test feature", None)
256 .unwrap();
257
258 let branch_info = branch_manager.get_branch_info().unwrap();
259 assert!(!branch_info.is_empty());
260
261 assert!(branch_info.iter().any(|b| b.is_current));
264
265 assert!(branch_info.len() >= 2);
267
268 for branch in &branch_info {
270 assert!(branch.upstream.is_none());
273 }
274 }
275
276 #[test]
277 fn test_upstream_parsing() {
278 let (_temp_dir, branch_manager) = create_test_branch_manager();
279
280 let (remote, branch) = branch_manager
282 .parse_upstream_name("origin/feature-auth")
283 .unwrap();
284 assert_eq!(remote, "origin");
285 assert_eq!(branch, "feature-auth");
286
287 let (remote, branch) = branch_manager.parse_upstream_name("upstream/main").unwrap();
288 assert_eq!(remote, "upstream");
289 assert_eq!(branch, "main");
290
291 let (remote, branch) = branch_manager.parse_upstream_name("feature-auth").unwrap();
293 assert_eq!(remote, "origin");
294 assert_eq!(branch, "feature-auth");
295 }
296}