use crate::core::test_helpers::TestRepo;
use git2::{BranchType, Repository, Signature};
#[test]
fn update_pulls_upstream_changes() {
let test_repo = TestRepo::new_with_remote();
let remote_oid = test_repo.add_remote_commits(&["Remote commit 1"]);
let before_oid = test_repo.head_oid();
assert_ne!(before_oid, remote_oid);
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
let after_oid = test_repo.head_oid();
assert_eq!(
after_oid, remote_oid,
"HEAD should point at the remote commit after pull"
);
}
#[test]
fn update_works_when_already_up_to_date() {
let test_repo = TestRepo::new_with_remote();
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
}
#[test]
fn update_fails_on_detached_head() {
let test_repo = TestRepo::new_with_remote();
let oid = test_repo.head_oid();
test_repo.set_detached_head(oid);
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("detached"),
"Expected detached HEAD error, got: {}",
err
);
}
#[test]
fn update_fails_without_upstream() {
let test_repo = TestRepo::new();
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("upstream") || err.contains("init"),
"Expected upstream error, got: {}",
err
);
}
#[test]
fn update_rebases_local_commits_on_top_of_upstream() {
let test_repo = TestRepo::new_with_remote();
test_repo.commit("Local work", "local.txt");
test_repo.add_remote_commits(&["Remote commit"]);
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
assert_eq!(test_repo.get_message(0), "Local work");
}
#[test]
fn update_fetches_tags_from_remote() {
let test_repo = TestRepo::new_with_remote();
let remote_path = test_repo.remote_path().unwrap();
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let remote_head = remote_repo
.find_branch("main", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
let remote_commit = remote_repo.find_commit(remote_head).unwrap();
let sig = Signature::now("Test", "test@test.com").unwrap();
remote_repo
.tag(
"v1.0.0",
remote_commit.as_object(),
&sig,
"Release 1.0",
false,
)
.unwrap();
assert!(
test_repo.repo.find_reference("refs/tags/v1.0.0").is_err(),
"Tag should not exist locally before update"
);
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
assert!(
test_repo.repo.find_reference("refs/tags/v1.0.0").is_ok(),
"Tag should exist locally after update"
);
}
#[test]
fn update_prunes_deleted_remote_branches() {
let test_repo = TestRepo::new_with_remote();
let remote_path = test_repo.remote_path().unwrap();
{
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let remote_head = remote_repo
.find_branch("main", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
let commit = remote_repo.find_commit(remote_head).unwrap();
remote_repo.branch("feature-temp", &commit, false).unwrap();
}
test_repo.fetch_remote();
test_repo
.repo
.find_remote("origin")
.unwrap()
.fetch(&["feature-temp"], None, None)
.unwrap();
assert!(
test_repo
.repo
.find_branch("origin/feature-temp", BranchType::Remote)
.is_ok(),
"Remote-tracking branch should exist after fetch"
);
{
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let mut branch = remote_repo
.find_branch("feature-temp", BranchType::Local)
.unwrap();
branch.delete().unwrap();
}
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
assert!(
test_repo
.repo
.find_branch("origin/feature-temp", BranchType::Remote)
.is_err(),
"Remote-tracking branch should be pruned after update"
);
}
#[test]
fn update_preserves_merge_topology() {
let test_repo = TestRepo::new_with_remote();
let merge_base_oid = test_repo.head_oid();
test_repo.create_branch_at_commit("feature-a", merge_base_oid);
test_repo.switch_branch("feature-a");
test_repo.commit("Feature A work", "feature-a.txt");
let feature_tip = test_repo.head_oid();
test_repo.switch_branch("integration");
test_repo.commit_merge("Merge branch 'feature-a'", merge_base_oid, feature_tip);
let head = test_repo.head_commit();
assert_eq!(head.parent_count(), 2, "HEAD should be a merge commit");
test_repo.add_remote_commits(&["Upstream change"]);
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
let log = test_repo.in_dir(|| {
let workdir = test_repo.workdir();
let output = std::process::Command::new("git")
.current_dir(&workdir)
.args(["log", "--oneline", "--graph", "--all", "-10"])
.output()
.unwrap();
String::from_utf8(output.stdout).unwrap()
});
let repo = &test_repo.repo;
let new_head = repo.head().unwrap().peel_to_commit().unwrap();
assert_eq!(
new_head.parent_count(),
2,
"HEAD should still be a merge commit after update, preserving woven topology.\nGraph:\n{}",
log
);
assert!(
new_head.message().unwrap().contains("Merge branch"),
"Merge commit message should be preserved"
);
}
#[test]
fn update_removes_gone_upstream_branches_via_config() {
let test_repo = TestRepo::new_with_remote();
let remote_path = test_repo.remote_path().unwrap();
test_repo
.repo
.config()
.unwrap()
.set_bool("loom.pruneGoneBranches", true)
.unwrap();
{
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let remote_head = remote_repo
.find_branch("main", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
let commit = remote_repo.find_commit(remote_head).unwrap();
remote_repo.branch("feature-x", &commit, false).unwrap();
}
test_repo
.repo
.find_remote("origin")
.unwrap()
.fetch(&["feature-x"], None, None)
.unwrap();
test_repo.create_branch_tracking("feature-x", "origin/feature-x");
{
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let mut branch = remote_repo
.find_branch("feature-x", BranchType::Local)
.unwrap();
branch.delete().unwrap();
}
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
assert!(
!test_repo.branch_exists("feature-x"),
"feature-x should be removed after update (loom.pruneGoneBranches = true)"
);
}
#[test]
fn update_removes_branches_with_gone_upstream() {
let test_repo = TestRepo::new_with_remote();
let remote_path = test_repo.remote_path().unwrap();
{
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let remote_head = remote_repo
.find_branch("main", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
let commit = remote_repo.find_commit(remote_head).unwrap();
remote_repo.branch("feature-x", &commit, false).unwrap();
}
test_repo
.repo
.find_remote("origin")
.unwrap()
.fetch(&["feature-x"], None, None)
.unwrap();
test_repo.create_branch_tracking("feature-x", "origin/feature-x");
assert!(
test_repo.branch_exists("feature-x"),
"feature-x should exist before update"
);
{
let remote_repo = Repository::open_bare(&remote_path).unwrap();
let mut branch = remote_repo
.find_branch("feature-x", BranchType::Local)
.unwrap();
branch.delete().unwrap();
}
let result = test_repo.in_dir(|| super::run(true));
assert!(result.is_ok(), "update failed: {:?}", result.err());
assert!(
!test_repo.branch_exists("feature-x"),
"feature-x should be removed after update (upstream is gone)"
);
}
#[test]
fn update_prunes_the_push_remote_in_a_fork_workflow() {
let test_repo = TestRepo::new_with_remote();
let remote_path = test_repo.remote_path().unwrap();
let fork_path = remote_path.parent().unwrap().join("fork.git");
Repository::init_bare(&fork_path).unwrap();
test_repo
.repo
.remote("personal", fork_path.to_str().unwrap())
.unwrap();
test_repo.set_config("loom.push-remote", "personal");
test_repo.create_branch("feature-x");
let workdir = test_repo.workdir();
crate::git::run_git(&workdir, &["push", "-u", "personal", "feature-x"]).unwrap();
{
let fork_repo = Repository::open_bare(&fork_path).unwrap();
let mut branch = fork_repo
.find_branch("feature-x", BranchType::Local)
.unwrap();
branch.delete().unwrap();
}
let result = test_repo.in_dir(|| super::run(true));
assert!(result.is_ok(), "update failed: {:?}", result.err());
assert!(
test_repo
.repo
.find_reference("refs/remotes/personal/feature-x")
.is_err(),
"the stale tracking ref on the push remote should be pruned"
);
assert!(
!test_repo.branch_exists("feature-x"),
"feature-x should be removed after update (deleted on the push remote)"
);
}
#[test]
fn update_keeps_branches_without_tracking_config() {
let test_repo = TestRepo::new_with_remote();
test_repo.create_branch("local-only");
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
assert!(
test_repo.branch_exists("local-only"),
"local-only branch should be preserved (no upstream configured)"
);
}
#[test]
fn update_does_not_put_upstream_commits_in_feature_branch() {
let test_repo = TestRepo::new_with_remote();
let merge_base_oid = test_repo.head_oid();
test_repo.create_branch_at_commit("feature-a", merge_base_oid);
test_repo.switch_branch("feature-a");
test_repo.commit("Feature A work", "feature-a.txt");
let feature_tip = test_repo.head_oid();
test_repo.switch_branch("integration");
test_repo.commit_merge("Merge branch 'feature-a'", merge_base_oid, feature_tip);
test_repo.add_remote_commits(&["Remote 1", "Remote 2", "Remote 3"]);
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
let repo = &test_repo.repo;
let new_head = repo.head().unwrap().peel_to_commit().unwrap();
assert_eq!(
new_head.parent_count(),
2,
"HEAD should still be a merge commit"
);
let merge_second_parent = new_head.parent(1).unwrap();
let new_base = new_head.parent(0).unwrap();
let mut count = 0;
let mut current = merge_second_parent.id();
let base_oid = new_base.id();
loop {
if current == base_oid {
break;
}
count += 1;
let commit = repo.find_commit(current).unwrap();
current = match commit.parent_id(0) {
Ok(oid) => oid,
Err(_) => break,
};
assert!(count <= 100, "Infinite loop walking branch commits");
}
assert_eq!(
count, 1,
"Feature branch side of the merge should have exactly 1 commit (the feature commit), \
but found {}. Upstream commits leaked into the feature branch.",
count
);
let feature_branch = repo
.find_branch("feature-a", BranchType::Local)
.expect("feature-a branch should still exist");
let feature_oid = feature_branch.get().target().unwrap();
let feature_commit = repo.find_commit(feature_oid).unwrap();
assert_eq!(
feature_commit.summary().unwrap(),
"Feature A work",
"feature-a should still point at its own commit"
);
}
#[test]
fn update_with_multiple_woven_branches() {
let test_repo = TestRepo::new_with_remote();
let merge_base_oid = test_repo.head_oid();
test_repo.create_branch_at_commit("feature-a", merge_base_oid);
test_repo.switch_branch("feature-a");
test_repo.commit("Feature A work", "feature-a.txt");
test_repo.switch_branch("integration");
test_repo.merge_no_ff("feature-a");
test_repo.create_branch_at_commit("feature-b", merge_base_oid);
test_repo.switch_branch("feature-b");
test_repo.commit("Feature B work", "feature-b.txt");
test_repo.switch_branch("integration");
test_repo.merge_no_ff("feature-b");
test_repo.add_remote_commits(&["Remote work"]);
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
let repo = &test_repo.repo;
let head = repo.head().unwrap().peel_to_commit().unwrap();
assert_eq!(head.parent_count(), 2, "HEAD should be a merge (feature-b)");
let first_parent = head.parent(0).unwrap();
assert_eq!(
first_parent.parent_count(),
2,
"First parent of HEAD should be a merge (feature-a)"
);
assert!(test_repo.branch_exists("feature-a"));
assert!(test_repo.branch_exists("feature-b"));
let fa = repo
.find_branch("feature-a", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
let fb = repo
.find_branch("feature-b", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
assert_eq!(
repo.find_commit(fa).unwrap().summary().unwrap(),
"Feature A work"
);
assert_eq!(
repo.find_commit(fb).unwrap().summary().unwrap(),
"Feature B work"
);
}
#[test]
fn update_preserves_multi_commit_branch() {
let test_repo = TestRepo::new_with_remote();
let merge_base_oid = test_repo.head_oid();
test_repo.create_branch_at_commit("feature-a", merge_base_oid);
test_repo.switch_branch("feature-a");
test_repo.commit("F1", "f1.txt");
test_repo.commit("F2", "f2.txt");
test_repo.commit("F3", "f3.txt");
test_repo.switch_branch("integration");
test_repo.merge_no_ff("feature-a");
test_repo.add_remote_commits(&["Upstream work"]);
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
let repo = &test_repo.repo;
let head = repo.head().unwrap().peel_to_commit().unwrap();
assert_eq!(head.parent_count(), 2, "HEAD should still be a merge");
let fa = repo
.find_branch("feature-a", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
assert_eq!(repo.find_commit(fa).unwrap().summary().unwrap(), "F3");
let merge_second_parent = head.parent(1).unwrap();
let merge_first_parent = head.parent(0).unwrap();
let mut count = 0;
let mut current = merge_second_parent.id();
loop {
if current == merge_first_parent.id() {
break;
}
count += 1;
let commit = repo.find_commit(current).unwrap();
current = match commit.parent_id(0) {
Ok(oid) => oid,
Err(_) => break,
};
assert!(count <= 100, "Infinite loop");
}
assert_eq!(
count, 3,
"Branch side should have exactly 3 commits (F1, F2, F3), got {}",
count
);
}
#[test]
fn update_with_partially_cherry_picked_branch() {
let test_repo = TestRepo::new_with_remote();
let merge_base_oid = test_repo.head_oid();
test_repo.create_branch_at_commit("feature-a", merge_base_oid);
test_repo.switch_branch("feature-a");
let f1_oid = test_repo.commit("F1", "f1.txt");
let f2_oid = test_repo.commit("F2", "f2.txt");
test_repo.commit("F3", "f3.txt");
test_repo.switch_branch("integration");
test_repo.merge_no_ff("feature-a");
test_repo.cherry_pick_to_remote(f1_oid, "F1");
test_repo.cherry_pick_to_remote(f2_oid, "F2");
let result = test_repo.in_dir(|| super::run(false));
assert!(result.is_ok(), "update failed: {:?}", result.err());
let repo = &test_repo.repo;
let head = repo.head().unwrap().peel_to_commit().unwrap();
assert_eq!(head.parent_count(), 2, "HEAD should still be a merge");
let fa = repo
.find_branch("feature-a", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
assert_eq!(repo.find_commit(fa).unwrap().summary().unwrap(), "F3");
let merge_second_parent = head.parent(1).unwrap();
let merge_first_parent = head.parent(0).unwrap();
let mut count = 0;
let mut current = merge_second_parent.id();
loop {
if current == merge_first_parent.id() {
break;
}
count += 1;
let commit = repo.find_commit(current).unwrap();
current = match commit.parent_id(0) {
Ok(oid) => oid,
Err(_) => break,
};
assert!(count <= 100, "Infinite loop");
}
assert_eq!(
count, 1,
"Branch side should have exactly 1 commit (F3) after F1/F2 cherry-picked upstream, got {}",
count
);
}
#[test]
fn update_handles_branch_cherry_picked_into_upstream() {
let test_repo = TestRepo::new_with_remote();
let merge_base_oid = test_repo.head_oid();
test_repo.create_branch_at_commit("feature-a", merge_base_oid);
test_repo.switch_branch("feature-a");
let feature_oid = test_repo.commit("Feature A work", "feature-a.txt");
test_repo.switch_branch("integration");
test_repo.merge_no_ff("feature-a");
test_repo.cherry_pick_to_remote(feature_oid, "Feature A work");
let result = test_repo.in_dir(|| super::run(false));
assert!(
result.is_ok(),
"update should succeed when branch is cherry-picked upstream: {:?}",
result.err()
);
}
#[test]
fn update_handles_partial_cherry_pick_to_upstream() {
let test_repo = TestRepo::new_with_remote();
let merge_base_oid = test_repo.head_oid();
test_repo.create_branch_at_commit("feature-a", merge_base_oid);
test_repo.switch_branch("feature-a");
let f1_oid = test_repo.commit("F1", "f1.txt");
test_repo.commit("F2", "f2.txt");
test_repo.switch_branch("integration");
test_repo.merge_no_ff("feature-a");
test_repo.cherry_pick_to_remote(f1_oid, "F1");
let result = test_repo.in_dir(|| super::run(false));
assert!(
result.is_ok(),
"update should succeed with partial cherry-pick: {:?}",
result.err()
);
let repo = &test_repo.repo;
let fa = repo
.find_branch("feature-a", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
assert_eq!(
repo.find_commit(fa).unwrap().summary().unwrap(),
"F2",
"feature-a should still have F2 after update"
);
let head = repo.head().unwrap().peel_to_commit().unwrap();
let merge_second_parent = head.parent(1).unwrap();
let merge_first_parent = head.parent(0).unwrap();
let mut count = 0;
let mut current = merge_second_parent.id();
loop {
if current == merge_first_parent.id() {
break;
}
count += 1;
let commit = repo.find_commit(current).unwrap();
current = match commit.parent_id(0) {
Ok(oid) => oid,
Err(_) => break,
};
assert!(count <= 100, "Infinite loop");
}
assert_eq!(
count, 1,
"Branch side should have 1 commit (F2) after F1 cherry-picked upstream, got {}",
count
);
}
#[test]
fn update_handles_fully_cherry_picked_branch() {
let test_repo = TestRepo::new_with_remote();
let merge_base_oid = test_repo.head_oid();
test_repo.create_branch_at_commit("feature-a", merge_base_oid);
test_repo.switch_branch("feature-a");
let f1_oid = test_repo.commit("F1", "f1.txt");
let f2_oid = test_repo.commit("F2", "f2.txt");
test_repo.switch_branch("integration");
test_repo.merge_no_ff("feature-a");
test_repo.cherry_pick_to_remote(f1_oid, "F1");
test_repo.cherry_pick_to_remote(f2_oid, "F2");
let result = test_repo.in_dir(|| super::run(false));
assert!(
result.is_ok(),
"update should succeed when all branch commits are cherry-picked: {:?}",
result.err()
);
let repo = &test_repo.repo;
let head = repo.head().unwrap().peel_to_commit().unwrap();
assert!(
repo.head().unwrap().is_branch(),
"HEAD should still be on a branch after update"
);
if head.parent_count() == 2 {
let merge_second_parent = head.parent(1).unwrap();
let merge_first_parent = head.parent(0).unwrap();
assert_eq!(
merge_second_parent.id(),
merge_first_parent.id(),
"Empty merge should have both parents pointing at the same commit (the base)"
);
}
let fa_branch = repo.find_branch("feature-a", BranchType::Local);
if let Ok(fa) = fa_branch {
let fa_oid = fa.get().target().unwrap();
let fa_commit = repo.find_commit(fa_oid).unwrap();
let summary = fa_commit.summary().unwrap_or("");
assert!(
summary == "F1" || summary == "F2" || summary == head.summary().unwrap_or(""),
"feature-a should not point at an unrelated upstream commit, \
but points at: {}",
summary
);
}
}
#[test]
fn update_handles_inverted_parent_merge_on_integration() {
let test_repo = TestRepo::new_with_remote();
let remote_path = test_repo.remote_path().unwrap();
let sig = Signature::now("Test", "test@test.com").unwrap();
let (c1_oid, f1_oid) = {
let rr = Repository::open_bare(&remote_path).unwrap();
let root_oid = rr
.find_branch("main", BranchType::Local)
.unwrap()
.get()
.target()
.unwrap();
let root = rr.find_commit(root_oid).unwrap();
let mut b = rr.treebuilder(Some(&root.tree().unwrap())).unwrap();
b.insert("main.txt", rr.blob(b"main").unwrap(), 0o100644)
.unwrap();
let c1_tree = rr.find_tree(b.write().unwrap()).unwrap();
let c1_oid = rr
.commit(
Some("refs/heads/main"),
&sig,
&sig,
"C1",
&c1_tree,
&[&root],
)
.unwrap();
let c1 = rr.find_commit(c1_oid).unwrap();
let mut b = rr.treebuilder(Some(&root.tree().unwrap())).unwrap();
b.insert("feature.txt", rr.blob(b"feat").unwrap(), 0o100644)
.unwrap();
let f1_tree = rr.find_tree(b.write().unwrap()).unwrap();
let f1_oid = rr
.commit(None, &sig, &sig, "F1", &f1_tree, &[&root])
.unwrap();
let f1 = rr.find_commit(f1_oid).unwrap();
let mut b = rr.treebuilder(Some(&c1_tree)).unwrap();
b.insert("feature.txt", rr.blob(b"feat").unwrap(), 0o100644)
.unwrap();
let mt = rr.find_tree(b.write().unwrap()).unwrap();
let mu_oid = rr
.commit(None, &sig, &sig, "Merge F1", &mt, &[&c1, &f1])
.unwrap();
rr.reference("refs/heads/main", mu_oid, true, "merge")
.unwrap();
let mu = rr.find_commit(mu_oid).unwrap();
let mut b = rr.treebuilder(Some(&mu.tree().unwrap())).unwrap();
b.insert("feature.txt", rr.blob(b"feat v2").unwrap(), 0o100644)
.unwrap();
let c2_tree = rr.find_tree(b.write().unwrap()).unwrap();
rr.commit(Some("refs/heads/main"), &sig, &sig, "C2", &c2_tree, &[&mu])
.unwrap();
(c1_oid, f1_oid)
};
test_repo
.repo
.find_remote("origin")
.unwrap()
.fetch(&["main"], None, None)
.unwrap();
let c1 = test_repo.repo.find_commit(c1_oid).unwrap();
let f1 = test_repo.repo.find_commit(f1_oid).unwrap();
test_repo
.repo
.reference("refs/heads/integration", c1_oid, true, "to C1")
.unwrap();
let mut b = test_repo
.repo
.treebuilder(Some(&c1.tree().unwrap()))
.unwrap();
b.insert(
"feature.txt",
test_repo.repo.blob(b"feat").unwrap(),
0o100644,
)
.unwrap();
let mt = test_repo.repo.find_tree(b.write().unwrap()).unwrap();
let merge_oid = test_repo
.repo
.commit(
None,
&sig,
&sig,
"Merge main into feature",
&mt,
&[&f1, &c1],
)
.unwrap();
test_repo
.repo
.reference("refs/heads/integration", merge_oid, true, "merge")
.unwrap();
test_repo.repo.set_head("refs/heads/integration").unwrap();
test_repo
.repo
.checkout_head(Some(git2::build::CheckoutBuilder::new().force()))
.unwrap();
let head = test_repo.repo.head().unwrap().peel_to_commit().unwrap();
assert_eq!(head.parent_count(), 2);
assert_eq!(head.parent_id(0).unwrap(), f1_oid);
test_repo.add_remote_commits(&["C3"]);
let result = test_repo.in_dir(|| super::run(false));
assert!(
result.is_ok(),
"update should handle inverted-parent merges: {:?}",
result.err()
);
let git_dir = test_repo.repo.path().to_path_buf();
assert!(
!crate::git::rebase_is_in_progress(&git_dir),
"rebase should not be in progress — expected clean completion, not conflict pause"
);
let new_head = test_repo.repo.head().unwrap().peel_to_commit().unwrap();
assert_eq!(
new_head.parent_count(),
1,
"HEAD should be linear after update (redundant merge dropped)"
);
}