use crate::core::repo;
use crate::core::test_helpers::TestRepo;
#[test]
fn branch_shows_in_status() {
let test_repo = TestRepo::new_with_remote();
let a1_oid = test_repo.commit_empty("A1");
test_repo.commit_empty("A2");
test_repo.create_branch_at("feature-a", &a1_oid.to_string());
let info = repo::gather_repo_info(&test_repo.repo, false, 1).unwrap();
let branch_names: Vec<&str> = info.branches.iter().map(|b| b.name.as_str()).collect();
assert!(
branch_names.contains(&"feature-a"),
"feature-a should appear in status branches, got: {:?}",
branch_names
);
let feature_a = info
.branches
.iter()
.find(|b| b.name == "feature-a")
.unwrap();
assert_eq!(feature_a.tip_oid, a1_oid);
}
#[test]
fn branch_ownership_splits_commits() {
let test_repo = TestRepo::new_with_remote();
let a1_oid = test_repo.commit_empty("A1");
test_repo.commit_empty("A2");
let b1_oid = test_repo.commit_empty("B1");
test_repo.commit_empty("B2");
test_repo.create_branch_at("feature-a", &a1_oid.to_string());
test_repo.create_branch_at("feature-b", &b1_oid.to_string());
assert_eq!(test_repo.branch_names().len(), 2);
}
#[test]
fn branch_invalid_name_fails() {
let result = crate::git::branch_validate_name("my..branch");
assert!(result.is_err(), "double dots should be invalid");
let result = crate::git::branch_validate_name("has space");
assert!(result.is_err(), "spaces should be invalid");
let result = crate::git::branch_validate_name("valid-name");
assert!(result.is_ok(), "valid name should pass");
}
#[test]
fn run_with_name_and_target() {
let test_repo = TestRepo::new_with_remote();
let a1_oid = test_repo.commit("A1", "a1.txt");
test_repo.commit("A2", "a2.txt");
let result = test_repo
.in_dir(|| super::new::run(Some("feature-a".to_string()), Some(a1_oid.to_string())));
assert!(result.is_ok(), "branch::run failed: {:?}", result.err());
assert!(test_repo.branch_exists("feature-a"));
}
#[test]
fn run_duplicate_name_rejected_early() {
let test_repo = TestRepo::new_with_remote();
let a1_oid = test_repo.commit_empty("A1");
test_repo.create_branch_at_commit("feature-a", a1_oid);
let result = test_repo
.in_dir(|| super::new::run(Some("feature-a".to_string()), Some(a1_oid.to_string())));
assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains("already exists"),
"expected 'already exists' error"
);
}
#[test]
fn run_default_target_is_merge_base() {
let test_repo = TestRepo::new_with_remote();
test_repo.commit_empty("A1");
let base_oid = test_repo.find_remote_branch_target("origin/main");
let result = test_repo.in_dir(|| super::new::run(Some("feature-a".to_string()), None));
assert!(result.is_ok(), "branch::run failed: {:?}", result.err());
assert_eq!(test_repo.get_branch_target("feature-a"), base_oid);
}
#[test]
fn branch_weave_creates_merge_topology() {
let test_repo = TestRepo::new_with_remote();
test_repo.commit("A1", "a1.txt");
test_repo.commit("A2", "a2.txt");
let a2_oid = test_repo.head_oid();
test_repo.commit("A3", "a3.txt");
let result = test_repo
.in_dir(|| super::new::run(Some("feature-a".to_string()), Some(a2_oid.to_string())));
assert!(result.is_ok(), "branch::run failed: {:?}", result.err());
assert!(test_repo.branch_exists("feature-a"));
let head = test_repo.head_commit();
assert_eq!(head.parent_count(), 1, "HEAD should be A3' with 1 parent");
let merge_commit = head.parent(0).unwrap();
assert_eq!(
merge_commit.parent_count(),
2,
"HEAD's parent should be the merge commit with 2 parents"
);
let merge_parent_oids: Vec<git2::Oid> = (0..merge_commit.parent_count())
.map(|i| merge_commit.parent_id(i).unwrap())
.collect();
let feature_a_oid = test_repo.get_branch_target("feature-a");
assert!(
merge_parent_oids.contains(&feature_a_oid),
"merge commit should have feature-a as a parent"
);
}
#[test]
fn branch_at_head_weaves() {
let test_repo = TestRepo::new_with_remote();
test_repo.commit("A1", "a1.txt");
test_repo.commit("A2", "a2.txt");
let head_before = test_repo.head_oid();
let result = test_repo
.in_dir(|| super::new::run(Some("feature-a".to_string()), Some(head_before.to_string())));
assert!(result.is_ok(), "branch::run failed: {:?}", result.err());
assert!(test_repo.branch_exists("feature-a"));
let head = test_repo.head_commit();
assert_eq!(
head.parent_count(),
2,
"HEAD should be a merge commit when branching at HEAD"
);
let parent_oids: Vec<git2::Oid> = (0..head.parent_count())
.map(|i| head.parent_id(i).unwrap())
.collect();
let feature_a_oid = test_repo.get_branch_target("feature-a");
assert!(
parent_oids.contains(&feature_a_oid),
"merge commit should have feature-a as a parent"
);
}
#[test]
fn branch_at_merge_base_no_weave() {
let test_repo = TestRepo::new_with_remote();
test_repo.commit("A1", "a1.txt");
let head_before = test_repo.head_oid();
let base_oid = test_repo.find_remote_branch_target("origin/main");
let result = test_repo
.in_dir(|| super::new::run(Some("feature-a".to_string()), Some(base_oid.to_string())));
assert!(result.is_ok(), "branch::run failed: {:?}", result.err());
assert_eq!(test_repo.head_oid(), head_before);
let head = test_repo.head_commit();
assert_eq!(
head.parent_count(),
1,
"HEAD should NOT be a merge commit when branching at merge-base"
);
}
#[test]
fn branch_inside_existing_branch_no_weave() {
let test_repo = TestRepo::new_with_remote();
test_repo.commit("A1", "a1.txt");
let a1_oid = test_repo.head_oid();
test_repo.commit("A2", "a2.txt");
let a2_oid = test_repo.head_oid();
test_repo.create_branch_at("feature-a", &a2_oid.to_string());
let base_oid = test_repo.find_remote_branch_target("origin/main");
test_repo.reset_hard(base_oid);
test_repo.commit("B1", "b1.txt");
test_repo.merge_no_ff("feature-a");
let head_before = test_repo.head_oid();
let result = test_repo
.in_dir(|| super::new::run(Some("feature-b".to_string()), Some(a1_oid.to_string())));
assert!(result.is_ok(), "branch::run failed: {:?}", result.err());
assert!(test_repo.branch_exists("feature-b"));
assert_eq!(
test_repo.head_oid(),
head_before,
"HEAD should be unchanged when branching inside an existing side branch"
);
}