//! Copyright (c) 2022-2026 GitDataAi All rights reserved. mod common; use gitks::pb::branch_service_server::BranchService; use gitks::pb::*; #[allow(unused_imports)] use gitks::pb::{BranchUpstream, SetBranchUpstreamRequest, UpdateBranchTargetRequest}; fn hdr() -> RepositoryHeader { RepositoryHeader { relative_path: "test-repo".into(), ..Default::default() } } #[tokio::test] async fn test_list_branches() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let result = svc .list_branches(tonic::Request::new(ListBranchesRequest { repository: Some(hdr()), pattern: String::new(), merged_into_head: false, not_merged_into_head: false, pagination: None, sort_direction: 0, })) .await .unwrap() .into_inner(); let names: Vec = result.branches.iter().map(|b| b.name.clone()).collect(); assert!(names.contains(&"feature".to_string())); assert!(names.contains(&"main".to_string())); assert!(result.branches.len() >= 2); } #[tokio::test] async fn test_list_branches_merged_filter() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let merged = svc .list_branches(tonic::Request::new(ListBranchesRequest { repository: Some(hdr()), pattern: String::new(), merged_into_head: true, not_merged_into_head: false, pagination: None, sort_direction: 0, })) .await .unwrap() .into_inner(); let not_merged = svc .list_branches(tonic::Request::new(ListBranchesRequest { repository: Some(hdr()), pattern: String::new(), merged_into_head: false, not_merged_into_head: true, pagination: None, sort_direction: 0, })) .await .unwrap() .into_inner(); let merged_names: Vec<&str> = merged.branches.iter().map(|b| b.name.as_str()).collect(); let not_merged_names: Vec<&str> = not_merged .branches .iter() .map(|b| b.name.as_str()) .collect(); assert!( merged_names.contains(&"main"), "main should be merged into HEAD, got: {:?}", merged_names ); assert!( not_merged_names.contains(&"feature"), "feature should NOT be merged into HEAD, got: {:?}", not_merged_names ); } #[tokio::test] async fn test_get_branch() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let branch = svc .get_branch(tonic::Request::new(GetBranchRequest { repository: Some(hdr()), name: "feature".into(), })) .await .unwrap() .into_inner(); assert_eq!(branch.full_ref, "refs/heads/feature"); let oid = branch.target_oid.unwrap(); assert!(!oid.value.is_empty()); assert_eq!(oid.value.len(), 20); assert_eq!(oid.hex.len(), 40); } #[tokio::test] async fn test_branch_pagination() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let result = svc .list_branches(tonic::Request::new(ListBranchesRequest { repository: Some(hdr()), pattern: String::new(), merged_into_head: false, not_merged_into_head: false, pagination: Some(Pagination { page_size: 1, page_token: String::new(), }), sort_direction: 0, })) .await .unwrap() .into_inner(); let page_info = result.page_info.unwrap(); assert_eq!(result.branches.len(), 1); assert!(page_info.has_next_page); let result2 = svc .list_branches(tonic::Request::new(ListBranchesRequest { repository: Some(hdr()), pattern: String::new(), merged_into_head: false, not_merged_into_head: false, pagination: Some(Pagination { page_size: 1, page_token: page_info.next_page_token, }), sort_direction: 0, })) .await .unwrap() .into_inner(); assert!(!result2.branches.is_empty()); assert_ne!(result.branches[0].name, result2.branches[0].name); } #[tokio::test] async fn test_create_and_delete_branch() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let branch = svc .create_branch(tonic::Request::new(CreateBranchRequest { repository: Some(hdr()), name: "new-branch".into(), start_point: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), force: false, })) .await .unwrap() .into_inner(); assert_eq!(branch.name, "new-branch"); svc.delete_branch(tonic::Request::new(DeleteBranchRequest { repository: Some(hdr()), name: "new-branch".into(), force: true, })) .await .unwrap(); let result = svc .get_branch(tonic::Request::new(GetBranchRequest { repository: Some(hdr()), name: "new-branch".into(), })) .await; assert!(result.is_err(), "deleted branch should not exist"); } #[tokio::test] async fn test_rename_branch() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); svc.create_branch(tonic::Request::new(CreateBranchRequest { repository: Some(hdr()), name: "to-rename".into(), start_point: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main".into(), })), }), force: false, })) .await .unwrap(); let renamed = svc .rename_branch(tonic::Request::new(RenameBranchRequest { repository: Some(hdr()), old_name: "to-rename".into(), new_name: "renamed".into(), })) .await .unwrap() .into_inner(); assert_eq!(renamed.name, "renamed"); let old = svc .get_branch(tonic::Request::new(GetBranchRequest { repository: Some(hdr()), name: "to-rename".into(), })) .await; assert!(old.is_err(), "old branch name should not exist"); } #[tokio::test] async fn test_compare_branch() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let result = svc .compare_branch(tonic::Request::new(CompareBranchRequest { repository: Some(hdr()), source_branch: "feature".into(), target_branch: "main".into(), })) .await .unwrap() .into_inner(); assert!( result.ahead_by > 0 || result.behind_by > 0, "branches should differ" ); assert!(result.merge_base.is_some(), "should find merge base"); } #[tokio::test] async fn test_update_branch_target() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); // Get current main OID let main_branch = svc .get_branch(tonic::Request::new(GetBranchRequest { repository: Some(hdr()), name: "main".into(), })) .await .unwrap() .into_inner(); let main_oid = main_branch.target_oid.as_ref().unwrap().clone(); // Create a new branch pointing to main's HEAD svc.create_branch(tonic::Request::new(CreateBranchRequest { repository: Some(hdr()), name: "movable".into(), start_point: Some(ObjectSelector { selector: Some(object_selector::Selector::Revision(ObjectName { revision: "main~2".into(), })), }), force: false, })) .await .unwrap(); // Update target to main's OID let updated = svc .update_branch_target(tonic::Request::new(UpdateBranchTargetRequest { repository: Some(hdr()), name: "movable".into(), expected_old_oid: None, new_oid: Some(main_oid), force: true, })) .await .unwrap() .into_inner(); assert_eq!(updated.name, "movable"); assert_eq!( updated.target_oid.as_ref().unwrap().hex, main_branch.target_oid.as_ref().unwrap().hex ); } #[tokio::test] async fn test_set_branch_upstream() { let (dir, _gb) = common::setup_bare_repo(); let svc = common::setup_service(dir.path()); let result = svc .set_branch_upstream(tonic::Request::new(SetBranchUpstreamRequest { repository: Some(hdr()), name: "main".into(), upstream: Some(BranchUpstream { remote_name: "origin".into(), remote_url: String::new(), remote_branch_name: "main".into(), local_branch_name: "main".into(), }), })) .await; // This may fail if no remote is configured, which is expected // The important thing is that the code path is exercised assert!(result.is_ok() || result.is_err()); }