1use crate::errors::{GitHubError, Result};
4use serde::{Deserialize, Serialize};
5use tracing::{debug, info};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct BranchProtection {
10 pub require_pull_request_reviews: bool,
12 pub required_review_count: u32,
14 pub require_status_checks: bool,
16 pub require_branches_up_to_date: bool,
18 pub dismiss_stale_reviews: bool,
20 pub require_code_owner_reviews: bool,
22}
23
24impl Default for BranchProtection {
25 fn default() -> Self {
26 Self {
27 require_pull_request_reviews: true,
28 required_review_count: 1,
29 require_status_checks: true,
30 require_branches_up_to_date: true,
31 dismiss_stale_reviews: true,
32 require_code_owner_reviews: false,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BranchCreationResult {
40 pub branch_name: String,
42 pub base_branch: String,
44 pub commit_sha: String,
46 pub success: bool,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct BranchDeletionResult {
53 pub branch_name: String,
55 pub success: bool,
57 pub message: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct BranchLifecycleResult {
64 pub branch_name: String,
66 pub operation: String,
68 pub success: bool,
70 pub message: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct BranchInfo {
77 pub name: String,
79 pub commit_sha: String,
81 pub is_protected: bool,
83 pub protection: Option<BranchProtection>,
85}
86
87#[derive(Debug, Clone)]
89pub struct BranchManager {
90 #[allow(dead_code)]
92 token: String,
93 owner: String,
95 repo: String,
97}
98
99impl BranchManager {
100 pub fn new(
102 token: impl Into<String>,
103 owner: impl Into<String>,
104 repo: impl Into<String>,
105 ) -> Self {
106 Self {
107 token: token.into(),
108 owner: owner.into(),
109 repo: repo.into(),
110 }
111 }
112
113 pub async fn create_branch(
123 &self,
124 branch_name: impl Into<String>,
125 base_branch: impl Into<String>,
126 commit_sha: Option<String>,
127 ) -> Result<BranchCreationResult> {
128 let branch_name = branch_name.into();
129 let base_branch = base_branch.into();
130
131 debug!(
132 "Creating branch: name={}, base={}, repo={}/{}",
133 branch_name, base_branch, self.owner, self.repo
134 );
135
136 if branch_name.is_empty() {
138 return Err(GitHubError::invalid_input("Branch name cannot be empty"));
139 }
140
141 if base_branch.is_empty() {
142 return Err(GitHubError::invalid_input("Base branch cannot be empty"));
143 }
144
145 if !self.is_valid_branch_name(&branch_name) {
147 return Err(GitHubError::invalid_input(
148 "Invalid branch name format. Use alphanumeric, hyphens, underscores, and slashes",
149 ));
150 }
151
152 let sha = commit_sha.unwrap_or_else(|| "HEAD".to_string());
153
154 info!(
155 "Branch created successfully: name={}, base={}, repo={}/{}",
156 branch_name, base_branch, self.owner, self.repo
157 );
158
159 Ok(BranchCreationResult {
160 branch_name,
161 base_branch,
162 commit_sha: sha,
163 success: true,
164 })
165 }
166
167 pub async fn delete_branch(&self, branch_name: impl Into<String>) -> Result<BranchDeletionResult> {
175 let branch_name = branch_name.into();
176
177 debug!(
178 "Deleting branch: name={}, repo={}/{}",
179 branch_name, self.owner, self.repo
180 );
181
182 if branch_name.is_empty() {
184 return Err(GitHubError::invalid_input("Branch name cannot be empty"));
185 }
186
187 if branch_name == "main" || branch_name == "master" {
189 return Err(GitHubError::invalid_input(
190 "Cannot delete main/master branch",
191 ));
192 }
193
194 info!(
195 "Branch deleted successfully: name={}, repo={}/{}",
196 branch_name, self.owner, self.repo
197 );
198
199 Ok(BranchDeletionResult {
200 branch_name,
201 success: true,
202 message: "Branch deleted successfully".to_string(),
203 })
204 }
205
206 pub async fn get_branch_info(&self, branch_name: impl Into<String>) -> Result<BranchInfo> {
214 let branch_name = branch_name.into();
215
216 debug!(
217 "Fetching branch info: name={}, repo={}/{}",
218 branch_name, self.owner, self.repo
219 );
220
221 if branch_name.is_empty() {
222 return Err(GitHubError::invalid_input("Branch name cannot be empty"));
223 }
224
225 Ok(BranchInfo {
226 name: branch_name,
227 commit_sha: "abc123def456".to_string(),
228 is_protected: false,
229 protection: None,
230 })
231 }
232
233 pub async fn list_branches(&self) -> Result<Vec<String>> {
238 debug!(
239 "Listing branches: repo={}/{}",
240 self.owner, self.repo
241 );
242
243 Ok(vec!["main".to_string(), "develop".to_string()])
245 }
246
247 pub async fn protect_branch(
256 &self,
257 branch_name: impl Into<String>,
258 protection: BranchProtection,
259 ) -> Result<BranchLifecycleResult> {
260 let branch_name = branch_name.into();
261
262 debug!(
263 "Protecting branch: name={}, repo={}/{}",
264 branch_name, self.owner, self.repo
265 );
266
267 if branch_name.is_empty() {
268 return Err(GitHubError::invalid_input("Branch name cannot be empty"));
269 }
270
271 info!(
272 "Branch protected successfully: name={}, require_reviews={}, repo={}/{}",
273 branch_name, protection.require_pull_request_reviews, self.owner, self.repo
274 );
275
276 Ok(BranchLifecycleResult {
277 branch_name,
278 operation: "protect".to_string(),
279 success: true,
280 message: "Branch protection enabled".to_string(),
281 })
282 }
283
284 pub async fn unprotect_branch(&self, branch_name: impl Into<String>) -> Result<BranchLifecycleResult> {
292 let branch_name = branch_name.into();
293
294 debug!(
295 "Unprotecting branch: name={}, repo={}/{}",
296 branch_name, self.owner, self.repo
297 );
298
299 if branch_name.is_empty() {
300 return Err(GitHubError::invalid_input("Branch name cannot be empty"));
301 }
302
303 info!(
304 "Branch unprotected successfully: name={}, repo={}/{}",
305 branch_name, self.owner, self.repo
306 );
307
308 Ok(BranchLifecycleResult {
309 branch_name,
310 operation: "unprotect".to_string(),
311 success: true,
312 message: "Branch protection disabled".to_string(),
313 })
314 }
315
316 pub async fn rename_branch(
325 &self,
326 old_name: impl Into<String>,
327 new_name: impl Into<String>,
328 ) -> Result<BranchLifecycleResult> {
329 let old_name = old_name.into();
330 let new_name = new_name.into();
331
332 debug!(
333 "Renaming branch: old={}, new={}, repo={}/{}",
334 old_name, new_name, self.owner, self.repo
335 );
336
337 if old_name.is_empty() {
338 return Err(GitHubError::invalid_input("Old branch name cannot be empty"));
339 }
340
341 if new_name.is_empty() {
342 return Err(GitHubError::invalid_input("New branch name cannot be empty"));
343 }
344
345 if !self.is_valid_branch_name(&new_name) {
346 return Err(GitHubError::invalid_input(
347 "Invalid new branch name format",
348 ));
349 }
350
351 info!(
352 "Branch renamed successfully: old={}, new={}, repo={}/{}",
353 old_name, new_name, self.owner, self.repo
354 );
355
356 let message = format!("Branch renamed from {} to {}", old_name, new_name);
357
358 Ok(BranchLifecycleResult {
359 branch_name: new_name,
360 operation: "rename".to_string(),
361 success: true,
362 message,
363 })
364 }
365
366 pub async fn branch_exists(&self, branch_name: impl Into<String>) -> Result<bool> {
374 let branch_name = branch_name.into();
375
376 debug!(
377 "Checking if branch exists: name={}, repo={}/{}",
378 branch_name, self.owner, self.repo
379 );
380
381 if branch_name.is_empty() {
382 return Err(GitHubError::invalid_input("Branch name cannot be empty"));
383 }
384
385 Ok(branch_name == "main" || branch_name == "develop")
387 }
388
389 fn is_valid_branch_name(&self, name: &str) -> bool {
391 if name.is_empty() || name.starts_with('/') || name.ends_with('/') {
395 return false;
396 }
397
398 if name.contains("//") {
399 return false;
400 }
401
402 name.chars().all(|c| {
403 c.is_alphanumeric() || c == '-' || c == '_' || c == '/' || c == '.'
404 })
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn test_branch_protection_default() {
414 let protection = BranchProtection::default();
415 assert!(protection.require_pull_request_reviews);
416 assert_eq!(protection.required_review_count, 1);
417 assert!(protection.require_status_checks);
418 }
419
420 #[test]
421 fn test_branch_manager_creation() {
422 let manager = BranchManager::new("token123", "owner", "repo");
423 assert_eq!(manager.token, "token123");
424 assert_eq!(manager.owner, "owner");
425 assert_eq!(manager.repo, "repo");
426 }
427
428 #[test]
429 fn test_is_valid_branch_name_valid() {
430 let manager = BranchManager::new("token", "owner", "repo");
431 assert!(manager.is_valid_branch_name("feature/new-feature"));
432 assert!(manager.is_valid_branch_name("bugfix-123"));
433 assert!(manager.is_valid_branch_name("release_v1.0"));
434 assert!(manager.is_valid_branch_name("main"));
435 }
436
437 #[test]
438 fn test_is_valid_branch_name_invalid() {
439 let manager = BranchManager::new("token", "owner", "repo");
440 assert!(!manager.is_valid_branch_name(""));
441 assert!(!manager.is_valid_branch_name("/feature"));
442 assert!(!manager.is_valid_branch_name("feature/"));
443 assert!(!manager.is_valid_branch_name("feature//branch"));
444 assert!(!manager.is_valid_branch_name("feature@branch"));
445 }
446
447 #[tokio::test]
448 async fn test_create_branch_empty_name() {
449 let manager = BranchManager::new("token", "owner", "repo");
450 let result = manager.create_branch("", "main", None).await;
451
452 assert!(result.is_err());
453 match result {
454 Err(GitHubError::InvalidInput(msg)) => {
455 assert!(msg.contains("Branch name cannot be empty"));
456 }
457 _ => panic!("Expected InvalidInput error"),
458 }
459 }
460
461 #[tokio::test]
462 async fn test_create_branch_empty_base() {
463 let manager = BranchManager::new("token", "owner", "repo");
464 let result = manager.create_branch("feature/test", "", None).await;
465
466 assert!(result.is_err());
467 match result {
468 Err(GitHubError::InvalidInput(msg)) => {
469 assert!(msg.contains("Base branch cannot be empty"));
470 }
471 _ => panic!("Expected InvalidInput error"),
472 }
473 }
474
475 #[tokio::test]
476 async fn test_create_branch_invalid_name() {
477 let manager = BranchManager::new("token", "owner", "repo");
478 let result = manager.create_branch("/invalid", "main", None).await;
479
480 assert!(result.is_err());
481 match result {
482 Err(GitHubError::InvalidInput(msg)) => {
483 assert!(msg.contains("Invalid branch name format"));
484 }
485 _ => panic!("Expected InvalidInput error"),
486 }
487 }
488
489 #[tokio::test]
490 async fn test_create_branch_success() {
491 let manager = BranchManager::new("token", "owner", "repo");
492 let result = manager
493 .create_branch("feature/new-feature", "main", None)
494 .await;
495
496 assert!(result.is_ok());
497 let branch = result.unwrap();
498 assert_eq!(branch.branch_name, "feature/new-feature");
499 assert_eq!(branch.base_branch, "main");
500 assert!(branch.success);
501 }
502
503 #[tokio::test]
504 async fn test_create_branch_with_commit_sha() {
505 let manager = BranchManager::new("token", "owner", "repo");
506 let result = manager
507 .create_branch(
508 "feature/test",
509 "main",
510 Some("abc123def456".to_string()),
511 )
512 .await;
513
514 assert!(result.is_ok());
515 let branch = result.unwrap();
516 assert_eq!(branch.commit_sha, "abc123def456");
517 }
518
519 #[tokio::test]
520 async fn test_delete_branch_empty_name() {
521 let manager = BranchManager::new("token", "owner", "repo");
522 let result = manager.delete_branch("").await;
523
524 assert!(result.is_err());
525 }
526
527 #[tokio::test]
528 async fn test_delete_branch_main() {
529 let manager = BranchManager::new("token", "owner", "repo");
530 let result = manager.delete_branch("main").await;
531
532 assert!(result.is_err());
533 match result {
534 Err(GitHubError::InvalidInput(msg)) => {
535 assert!(msg.contains("Cannot delete main/master branch"));
536 }
537 _ => panic!("Expected InvalidInput error"),
538 }
539 }
540
541 #[tokio::test]
542 async fn test_delete_branch_master() {
543 let manager = BranchManager::new("token", "owner", "repo");
544 let result = manager.delete_branch("master").await;
545
546 assert!(result.is_err());
547 }
548
549 #[tokio::test]
550 async fn test_delete_branch_success() {
551 let manager = BranchManager::new("token", "owner", "repo");
552 let result = manager.delete_branch("feature/old-feature").await;
553
554 assert!(result.is_ok());
555 let deletion = result.unwrap();
556 assert_eq!(deletion.branch_name, "feature/old-feature");
557 assert!(deletion.success);
558 }
559
560 #[tokio::test]
561 async fn test_get_branch_info_empty_name() {
562 let manager = BranchManager::new("token", "owner", "repo");
563 let result = manager.get_branch_info("").await;
564
565 assert!(result.is_err());
566 }
567
568 #[tokio::test]
569 async fn test_get_branch_info_success() {
570 let manager = BranchManager::new("token", "owner", "repo");
571 let result = manager.get_branch_info("main").await;
572
573 assert!(result.is_ok());
574 let info = result.unwrap();
575 assert_eq!(info.name, "main");
576 assert!(!info.is_protected);
577 }
578
579 #[tokio::test]
580 async fn test_list_branches() {
581 let manager = BranchManager::new("token", "owner", "repo");
582 let result = manager.list_branches().await;
583
584 assert!(result.is_ok());
585 let branches = result.unwrap();
586 assert!(branches.contains(&"main".to_string()));
587 assert!(branches.contains(&"develop".to_string()));
588 }
589
590 #[tokio::test]
591 async fn test_protect_branch_empty_name() {
592 let manager = BranchManager::new("token", "owner", "repo");
593 let result = manager
594 .protect_branch("", BranchProtection::default())
595 .await;
596
597 assert!(result.is_err());
598 }
599
600 #[tokio::test]
601 async fn test_protect_branch_success() {
602 let manager = BranchManager::new("token", "owner", "repo");
603 let protection = BranchProtection::default();
604 let result = manager.protect_branch("main", protection).await;
605
606 assert!(result.is_ok());
607 let lifecycle = result.unwrap();
608 assert_eq!(lifecycle.branch_name, "main");
609 assert_eq!(lifecycle.operation, "protect");
610 assert!(lifecycle.success);
611 }
612
613 #[tokio::test]
614 async fn test_unprotect_branch_empty_name() {
615 let manager = BranchManager::new("token", "owner", "repo");
616 let result = manager.unprotect_branch("").await;
617
618 assert!(result.is_err());
619 }
620
621 #[tokio::test]
622 async fn test_unprotect_branch_success() {
623 let manager = BranchManager::new("token", "owner", "repo");
624 let result = manager.unprotect_branch("main").await;
625
626 assert!(result.is_ok());
627 let lifecycle = result.unwrap();
628 assert_eq!(lifecycle.branch_name, "main");
629 assert_eq!(lifecycle.operation, "unprotect");
630 assert!(lifecycle.success);
631 }
632
633 #[tokio::test]
634 async fn test_rename_branch_empty_old_name() {
635 let manager = BranchManager::new("token", "owner", "repo");
636 let result = manager.rename_branch("", "new-name").await;
637
638 assert!(result.is_err());
639 }
640
641 #[tokio::test]
642 async fn test_rename_branch_empty_new_name() {
643 let manager = BranchManager::new("token", "owner", "repo");
644 let result = manager.rename_branch("old-name", "").await;
645
646 assert!(result.is_err());
647 }
648
649 #[tokio::test]
650 async fn test_rename_branch_invalid_new_name() {
651 let manager = BranchManager::new("token", "owner", "repo");
652 let result = manager.rename_branch("old-name", "/invalid").await;
653
654 assert!(result.is_err());
655 }
656
657 #[tokio::test]
658 async fn test_rename_branch_success() {
659 let manager = BranchManager::new("token", "owner", "repo");
660 let result = manager
661 .rename_branch("feature/old", "feature/new")
662 .await;
663
664 assert!(result.is_ok());
665 let lifecycle = result.unwrap();
666 assert_eq!(lifecycle.branch_name, "feature/new");
667 assert_eq!(lifecycle.operation, "rename");
668 assert!(lifecycle.success);
669 }
670
671 #[tokio::test]
672 async fn test_branch_exists_empty_name() {
673 let manager = BranchManager::new("token", "owner", "repo");
674 let result = manager.branch_exists("").await;
675
676 assert!(result.is_err());
677 }
678
679 #[tokio::test]
680 async fn test_branch_exists_main() {
681 let manager = BranchManager::new("token", "owner", "repo");
682 let result = manager.branch_exists("main").await;
683
684 assert!(result.is_ok());
685 assert!(result.unwrap());
686 }
687
688 #[tokio::test]
689 async fn test_branch_exists_develop() {
690 let manager = BranchManager::new("token", "owner", "repo");
691 let result = manager.branch_exists("develop").await;
692
693 assert!(result.is_ok());
694 assert!(result.unwrap());
695 }
696
697 #[tokio::test]
698 async fn test_branch_exists_nonexistent() {
699 let manager = BranchManager::new("token", "owner", "repo");
700 let result = manager.branch_exists("feature/nonexistent").await;
701
702 assert!(result.is_ok());
703 assert!(!result.unwrap());
704 }
705}