mod common;
use common::{OutputAssertions, TestRepo};
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
fn install_failing_pre_commit_hook(repo: &TestRepo) {
let hooks_dir = repo.path().join(".git").join("hooks");
fs::create_dir_all(&hooks_dir).unwrap();
let hook_path = hooks_dir.join("pre-commit");
fs::write(&hook_path, "#!/bin/sh\nexit 1\n").unwrap();
fs::set_permissions(&hook_path, fs::Permissions::from_mode(0o755)).unwrap();
}
#[cfg(unix)]
fn remove_pre_commit_hook(repo: &TestRepo) {
let hook_path = repo.path().join(".git").join("hooks").join("pre-commit");
let _ = std::fs::remove_file(hook_path);
}
#[test]
#[cfg(unix)]
fn create_with_failing_hook_leaves_no_branch() {
let repo = TestRepo::new();
repo.run_stax(&["init"]).assert_success();
repo.create_file("test.txt", "hello");
install_failing_pre_commit_hook(&repo);
let main_before = repo.head_sha();
let output = repo.run_stax(&["create", "-a", "-m", "my feature"]);
output.assert_failure();
let branches = repo.list_branches();
assert!(
!branches.iter().any(|b| b.contains("my-feature")),
"No orphan branch should exist when commit fails. Branches: {:?}",
branches
);
assert_eq!(repo.current_branch(), "main");
assert_eq!(
repo.head_sha(),
main_before,
"main's HEAD must not advance when the commit fails",
);
output.assert_stderr_contains("No branch was created");
}
#[test]
#[cfg(unix)]
fn create_retry_after_hook_failure_uses_same_name() {
let repo = TestRepo::new();
repo.run_stax(&["init"]).assert_success();
repo.create_file("test.txt", "hello");
install_failing_pre_commit_hook(&repo);
repo.run_stax(&["create", "-a", "-m", "my feature"])
.assert_failure();
repo.run_stax(&["create", "-a", "-m", "my feature"])
.assert_failure();
let branches = repo.list_branches();
assert!(
!branches.iter().any(|b| b.contains("my-feature-2")),
"No suffixed branch should exist. Branches: {:?}",
branches
);
}
#[test]
fn create_with_successful_commit_still_works() {
let repo = TestRepo::new();
repo.run_stax(&["init"]).assert_success();
repo.create_file("test.txt", "hello");
repo.run_stax(&["create", "-a", "-m", "my feature"])
.assert_success();
assert!(
repo.current_branch().contains("my-feature"),
"Should be on the new branch. Current: {}",
repo.current_branch()
);
}
#[test]
fn create_commit_lives_only_on_new_branch() {
let repo = TestRepo::new();
repo.run_stax(&["init"]).assert_success();
let main_before = repo.get_commit_sha("main");
repo.create_file("test.txt", "hello");
repo.run_stax(&["create", "-a", "-m", "my feature"])
.assert_success();
let main_after = repo.get_commit_sha("main");
assert_eq!(
main_before, main_after,
"main's HEAD must not move when `st create -m` splits the commit off",
);
let new_branch = repo.current_branch();
let new_branch_sha = repo.get_commit_sha(&new_branch);
assert_ne!(
new_branch_sha, main_before,
"new branch must point at a new commit, not at main's HEAD",
);
}
#[test]
fn create_commit_first_with_insert_reparents_children_onto_new_commit() {
let repo = TestRepo::new();
repo.run_stax(&["status"]).assert_success();
let branches = repo.create_stack(&["insert-a", "insert-b"]);
repo.run_stax(&["checkout", &branches[0]]).assert_success();
repo.create_file("insert_mid.txt", "new work");
let insert_a_before = repo.get_commit_sha(&branches[0]);
let output = repo.run_stax(&["create", "-a", "-m", "mid feature", "--insert"]);
output.assert_success();
assert_eq!(
repo.get_commit_sha(&branches[0]),
insert_a_before,
"parent branch must not advance in commit-first + --insert",
);
let stdout = TestRepo::stdout(&output);
assert!(
stdout.contains("Reparented"),
"Expected reparent message, got: {}",
stdout
);
repo.run_stax(&["checkout", &branches[1]]).assert_success();
let b_parent = repo.get_current_parent();
assert!(
b_parent
.as_deref()
.is_some_and(|p| p.contains("mid-feature")),
"insert-b should be reparented onto the mid-feature branch, got: {:?}",
b_parent,
);
}
#[test]
#[cfg(unix)]
fn create_failing_hook_preserves_working_tree_changes() {
let repo = TestRepo::new();
repo.run_stax(&["init"]).assert_success();
repo.create_file("notes.txt", "draft idea");
install_failing_pre_commit_hook(&repo);
repo.run_stax(&["create", "-a", "-m", "my feature"])
.assert_failure();
let contents = std::fs::read_to_string(repo.path().join("notes.txt"))
.expect("notes.txt should still exist");
assert_eq!(contents, "draft idea");
remove_pre_commit_hook(&repo);
repo.run_stax(&["create", "-a", "-m", "my feature"])
.assert_success();
let branch = repo.current_branch();
assert!(
branch.contains("my-feature") && !branch.contains("my-feature-2"),
"Retry should use the original branch name, got: {}",
branch,
);
}
#[test]
#[cfg(unix)]
fn create_without_message_unaffected_by_hook() {
let repo = TestRepo::new();
repo.run_stax(&["init"]).assert_success();
install_failing_pre_commit_hook(&repo);
repo.run_stax(&["create", "my-branch"]).assert_success();
assert!(
repo.current_branch().contains("my-branch"),
"Branch should be created. Current: {}",
repo.current_branch()
);
}
#[test]
#[cfg(unix)]
fn create_rollback_then_succeed_after_hook_removed() {
let repo = TestRepo::new();
repo.run_stax(&["init"]).assert_success();
repo.create_file("test.txt", "hello");
install_failing_pre_commit_hook(&repo);
repo.run_stax(&["create", "-a", "-m", "my feature"])
.assert_failure();
remove_pre_commit_hook(&repo);
repo.run_stax(&["create", "-a", "-m", "my feature"])
.assert_success();
let branch = repo.current_branch();
assert!(
branch.contains("my-feature"),
"Should be on the feature branch: {}",
branch
);
assert!(
!branch.contains("my-feature-2"),
"Should not have a -2 suffix: {}",
branch
);
}
#[test]
#[cfg(unix)]
fn create_from_other_branch_with_failing_hook_rolls_back() {
let repo = TestRepo::new();
repo.run_stax(&["init"]).assert_success();
let branches = repo.create_stack(&["sibling"]);
repo.run_stax(&["checkout", "main"]).assert_success();
install_failing_pre_commit_hook(&repo);
repo.create_file("wip.txt", "wip");
let output = repo.run_stax(&[
"create",
"--from",
&branches[0],
"-a",
"-m",
"off sibling feature",
]);
output.assert_failure();
assert!(
!repo
.list_branches()
.iter()
.any(|b| b.contains("off-sibling-feature")),
"new branch must be rolled back after hook failure. Branches: {:?}",
repo.list_branches(),
);
output.assert_stderr_contains("rolled back");
}