mod harness;
use harness::{
HK_PRE_PUSH_AUTOFIX, HK_PRE_PUSH_FAILING, HK_PRE_PUSH_PASSING, LEFTHOOK_PRE_PUSH_AUTOFIX,
LEFTHOOK_PRE_PUSH_FAILING, LEFTHOOK_PRE_PUSH_PASSING, PRE_PUSH_AUTOFIX, PRE_PUSH_FAILING,
PRE_PUSH_PASSING, TestRepo, show,
};
#[test]
fn harness_smoke() {
let repo = TestRepo::new();
repo.write("hello.txt", "hello\n");
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["git", "push", "-b", "main", "--dry-run"]);
assert!(out.status.success(), "{}", show(&out));
}
#[test]
fn no_runner_config_passes_through_to_jj_git_push() {
let repo = TestRepo::new();
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let head = repo.commit_id_of("main");
let out = repo.jj_hooks(&["push", "-b", "main"]);
assert!(out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn delete_only_push_skips_hooks() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_FAILING);
let out = repo.jj(&["bookmark", "create", "tmp", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["git", "push", "-b", "tmp", "--allow-new"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "delete", "tmp"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj_hooks(&["--runner", "pre-commit", "push", "-b", "tmp"]);
assert!(out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("tmp"), None);
}
#[test]
fn passing_hooks_pushes() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_PASSING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let head = repo.commit_id_of("main");
let out = repo.jj_hooks(&["--runner", "pre-commit", "push", "-b", "main"]);
assert!(out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn failing_hooks_abort_push() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_FAILING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let remote_before = repo.remote_commit("main");
let out = repo.jj_hooks(&["--runner", "pre-commit", "push", "-b", "main"]);
assert!(
!out.status.success(),
"expected nonzero exit:\n{}",
show(&out)
);
assert_eq!(repo.remote_commit("main"), remote_before);
assert!(repo.refs_matching("refs/jj-hooks/fixup/*").is_empty());
}
#[test]
fn hook_autofix_creates_fixup_ref_and_aborts_push() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_AUTOFIX);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let remote_before = repo.remote_commit("main");
let local_main_before = repo.commit_id_of("main");
let out = repo.jj_hooks(&["--runner", "pre-commit", "push", "-b", "main"]);
assert!(
!out.status.success(),
"expected nonzero exit:\n{}",
show(&out)
);
assert_eq!(repo.remote_commit("main"), remote_before);
let fixup_refs = repo.refs_matching("refs/heads/jj-hooks-fixup/*");
assert!(
fixup_refs
.iter()
.any(|r| r == "refs/heads/jj-hooks-fixup/main"),
"expected fixup ref for main, got {fixup_refs:?}"
);
assert_eq!(repo.commit_id_of("main"), local_main_before);
}
#[test]
fn hook_autofix_with_advance_bookmarks_moves_local_bookmark() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_AUTOFIX);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let local_main_before = repo.commit_id_of("main");
let out = repo.jj_hooks(&[
"--runner",
"pre-commit",
"push",
"--advance-bookmarks",
"-b",
"main",
]);
assert!(
!out.status.success(),
"expected nonzero exit:\n{}",
show(&out)
);
let local_main_after = repo.commit_id_of("main");
assert_ne!(
local_main_after, local_main_before,
"bookmark should have moved to the fixup commit"
);
let parent = repo.commit_id_of(&format!("{local_main_after}-"));
assert_eq!(parent, local_main_before);
assert!(
repo.rev_parse("refs/heads/jj-hooks-fixup/main").is_none(),
"temporary fixup ref should be cleaned up after --advance-bookmarks"
);
}
#[test]
fn hook_autofix_from_secondary_workspace() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_AUTOFIX);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let secondary = repo.add_secondary("secondary");
let out = repo.jj_hooks_in(
&secondary,
&["--runner", "pre-commit", "push", "-b", "main"],
);
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
!stderr.contains("Your pre-commit configuration is unstaged"),
"secondary workspace should not trigger pre-commit unstaged warning:\n{stderr}"
);
assert!(!out.status.success(), "{}", show(&out));
let fixup_refs = repo.refs_matching("refs/heads/jj-hooks-fixup/*");
assert!(
fixup_refs
.iter()
.any(|r| r == "refs/heads/jj-hooks-fixup/main"),
"expected fixup ref for main, got {fixup_refs:?}"
);
}
#[test]
fn new_bookmark_uses_remote_ancestors_resolution() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_PASSING);
repo.write("feature.txt", "x\n");
let out = repo.jj(&["commit", "-m", "feature"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "create", "feature", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let head = repo.commit_id_of("feature");
let out = repo.jj_hooks(&[
"--runner",
"pre-commit",
"push",
"-b",
"feature",
"--allow-new",
]);
assert!(out.status.success(), "{}", show(&out));
assert_eq!(
repo.remote_commit("feature").as_deref(),
Some(head.as_str())
);
}
#[test]
fn multi_bookmark_one_fail_blocks_all() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_FAILING);
repo.write("a.txt", "x\n");
let out = repo.jj(&["commit", "-m", "main move"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
repo.write("b.txt", "y\n");
let out = repo.jj(&["commit", "-m", "feature commit"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "create", "feature", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let main_remote_before = repo.remote_commit("main");
let feature_remote_before = repo.remote_commit("feature");
let out = repo.jj_hooks(&[
"--runner",
"pre-commit",
"push",
"-b",
"main",
"-b",
"feature",
"--allow-new",
]);
assert!(!out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main"), main_remote_before);
assert_eq!(repo.remote_commit("feature"), feature_remote_before);
}
#[test]
fn run_subcommand_executes_hooks_without_pushing() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_PASSING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let remote_before = repo.remote_commit("main");
let out = repo.jj_hooks(&["--runner", "pre-commit", "run", "--stage", "pre-push", "@-"]);
assert!(out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main"), remote_before);
}
#[test]
fn prek_passing_hooks_pushes() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_PASSING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let head = repo.commit_id_of("main");
let out = repo.jj_hooks(&["--runner", "prek", "push", "-b", "main"]);
assert!(out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn prek_failing_hooks_abort_push() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_FAILING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let remote_before = repo.remote_commit("main");
let out = repo.jj_hooks(&["--runner", "prek", "push", "-b", "main"]);
assert!(!out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main"), remote_before);
}
#[test]
fn prek_hook_autofix_creates_fixup_ref() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_AUTOFIX);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj_hooks(&["--runner", "prek", "push", "-b", "main"]);
assert!(!out.status.success(), "{}", show(&out));
assert!(
repo.refs_matching("refs/heads/jj-hooks-fixup/*")
.iter()
.any(|r| r == "refs/heads/jj-hooks-fixup/main")
);
}
#[test]
fn lefthook_passing_hooks_pushes() {
let repo = TestRepo::new();
repo.write_lefthook_config(LEFTHOOK_PRE_PUSH_PASSING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let head = repo.commit_id_of("main");
let out = repo.jj_hooks(&["push", "-b", "main"]);
assert!(out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn lefthook_failing_hooks_abort_push() {
let repo = TestRepo::new();
repo.write_lefthook_config(LEFTHOOK_PRE_PUSH_FAILING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let remote_before = repo.remote_commit("main");
let out = repo.jj_hooks(&["push", "-b", "main"]);
assert!(!out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main"), remote_before);
}
#[test]
fn lefthook_hook_autofix_creates_fixup_ref() {
let repo = TestRepo::new();
repo.write_lefthook_config(LEFTHOOK_PRE_PUSH_AUTOFIX);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj_hooks(&["push", "-b", "main"]);
assert!(!out.status.success(), "{}", show(&out));
assert!(
repo.refs_matching("refs/heads/jj-hooks-fixup/*")
.iter()
.any(|r| r == "refs/heads/jj-hooks-fixup/main")
);
}
#[test]
fn hk_passing_hooks_pushes() {
let repo = TestRepo::new();
repo.write_hk_config(HK_PRE_PUSH_PASSING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let head = repo.commit_id_of("main");
let out = repo.jj_hooks(&["push", "-b", "main"]);
assert!(out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn hk_failing_hooks_abort_push() {
let repo = TestRepo::new();
repo.write_hk_config(HK_PRE_PUSH_FAILING);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let remote_before = repo.remote_commit("main");
let out = repo.jj_hooks(&["push", "-b", "main"]);
assert!(!out.status.success(), "{}", show(&out));
assert_eq!(repo.remote_commit("main"), remote_before);
}
#[test]
fn hk_hook_autofix_creates_fixup_ref() {
let repo = TestRepo::new();
repo.write_hk_config(HK_PRE_PUSH_AUTOFIX);
repo.write("new.txt", "x\n");
let out = repo.jj(&["commit", "-m", "second"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "set", "main", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj_hooks(&["push", "-b", "main"]);
assert!(!out.status.success(), "{}", show(&out));
assert!(
repo.refs_matching("refs/heads/jj-hooks-fixup/*")
.iter()
.any(|r| r == "refs/heads/jj-hooks-fixup/main")
);
}