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_AUTOFIX_THEN_PASS, PRE_PUSH_FAILING, PRE_PUSH_INDEX_TOUCH_ONLY, PRE_PUSH_PASSING,
PRE_PUSH_RECORD_RANGE, 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);
assert!(
repo.rev_parse("refs/heads/jj-hooks-fixup/main").is_none(),
"temp fixup ref should be cleaned up after import"
);
let fixup = repo
.fixup_commit_for("main")
.expect("fixup commit should be findable by description");
assert!(
repo.jj_knows_commit(&fixup),
"jj should still see the fixup commit even with no ref pointing at it"
);
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 index_touch_without_content_change_does_not_emit_fixup() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_INDEX_TOUCH_ONLY);
repo.write("existing.txt", "stable content\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(),
"push should abort when the hook reports failure:\n{}",
show(&out)
);
assert_eq!(repo.remote_commit("main"), remote_before);
assert!(
repo.rev_parse("refs/heads/jj-hooks-fixup/main").is_none(),
"no fixup ref should be created when the worktree's tree is unchanged"
);
assert!(
repo.fixup_commit_for("main").is_none(),
"no fixup commit should be addressable when the worktree's tree is unchanged"
);
}
#[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));
assert!(
repo.rev_parse("refs/heads/jj-hooks-fixup/main").is_none(),
"temp fixup ref should be cleaned up after import"
);
let fixup = repo
.fixup_commit_for("main")
.expect("fixup commit should be findable by description");
assert!(repo.jj_knows_commit(&fixup));
}
#[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 run_subcommand_autofix_does_not_crash_on_bad_ref_name() {
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_hooks(&["--runner", "pre-commit", "run", "--stage", "pre-push", "@-"]);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("refusing to update ref with bad name"),
"expected sanitizer to scrub `:` from synthesized ref:\n{}",
show(&out)
);
assert!(
!stderr.contains("update_ref failed"),
"ref update should succeed after sanitization:\n{}",
show(&out)
);
assert!(
repo.rev_parse("refs/heads/jj-hooks-fixup/revset_@-")
.is_none(),
"temp fixup ref should be cleaned up after import"
);
let fixup = repo
.fixup_commit_for("revset:@-")
.expect("fixup commit should be findable by description");
assert!(repo.jj_knows_commit(&fixup));
}
#[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.rev_parse("refs/heads/jj-hooks-fixup/main").is_none(),
"temp fixup ref should be cleaned up after import"
);
let fixup = repo
.fixup_commit_for("main")
.expect("fixup commit should be findable by description");
assert!(repo.jj_knows_commit(&fixup));
}
#[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.rev_parse("refs/heads/jj-hooks-fixup/main").is_none(),
"temp fixup ref should be cleaned up after import"
);
let fixup = repo
.fixup_commit_for("main")
.expect("fixup commit should be findable by description");
assert!(repo.jj_knows_commit(&fixup));
}
#[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.rev_parse("refs/heads/jj-hooks-fixup/main").is_none(),
"temp fixup ref should be cleaned up after import"
);
let fixup = repo
.fixup_commit_for("main")
.expect("fixup commit should be findable by description");
assert!(repo.jj_knows_commit(&fixup));
}
#[test]
fn runner_autodetect_inside_target_worktree_not_primary() {
let repo = TestRepo::new();
repo.write_hk_config(HK_PRE_PUSH_FAILING);
let out = repo.jj(&["commit", "-m", "migrate to hk"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "create", "migrate-to-hk", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["new", "main"]);
assert!(out.status.success(), "{}", show(&out));
repo.write_lefthook_config(LEFTHOOK_PRE_PUSH_PASSING);
assert!(repo.primary().join("lefthook.yml").exists());
assert!(!repo.primary().join("hk.pkl").exists());
let remote_before = repo.remote_commit("migrate-to-hk");
let out = repo.jj_hooks(&["push", "-b", "migrate-to-hk", "--allow-new"]);
assert!(
!out.status.success(),
"push should abort because hk hook fails:\n{}",
show(&out)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("No config files with names [\"lefthook\""),
"lefthook should not be the picked runner (primary's config bled \
into the target worktree's autodetect):\n{stderr}"
);
assert_eq!(repo.remote_commit("migrate-to-hk"), remote_before);
}
#[test]
fn runner_autodetect_inside_target_worktree_picks_lefthook() {
let repo = TestRepo::new();
repo.write_lefthook_config(LEFTHOOK_PRE_PUSH_FAILING);
let out = repo.jj(&["commit", "-m", "migrate to lefthook"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["bookmark", "create", "migrate-to-lefthook", "-r", "@-"]);
assert!(out.status.success(), "{}", show(&out));
let out = repo.jj(&["new", "main"]);
assert!(out.status.success(), "{}", show(&out));
repo.write_hk_config(HK_PRE_PUSH_PASSING);
assert!(repo.primary().join("hk.pkl").exists());
assert!(!repo.primary().join("lefthook.yml").exists());
let remote_before = repo.remote_commit("migrate-to-lefthook");
let out = repo.jj_hooks(&["push", "-b", "migrate-to-lefthook", "--allow-new"]);
assert!(
!out.status.success(),
"push should abort because the target commit's lefthook hook fails:\n{}",
show(&out)
);
assert_eq!(repo.remote_commit("migrate-to-lefthook"), remote_before);
}
#[test]
fn runner_autodetect_skips_when_target_commit_has_no_config() {
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");
repo.write_lefthook_config(LEFTHOOK_PRE_PUSH_FAILING);
let out = repo.jj_hooks(&["push", "-b", "main"]);
assert!(
out.status.success(),
"target commit has no hook config; push should proceed:\n{}",
show(&out)
);
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn retry_after_fixup_heals_transient_failure_by_default() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_AUTOFIX_THEN_PASS);
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(), "{}", show(&out));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("re-run on fixup commit was clean"),
"expected the retry-healed message, got:\n{stderr}"
);
assert!(
!stderr.contains(": hook failed"),
"the retry succeeded, so the bare \"hook failed\" line should not appear:\n{stderr}"
);
assert_eq!(repo.remote_commit("main"), remote_before);
let fixup = repo
.fixup_commit_for("main")
.expect("fixup commit should still be findable after retry");
assert!(repo.jj_knows_commit(&fixup));
}
#[test]
fn no_retry_after_fixup_flag_restores_pre_0_3_behavior() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_AUTOFIX_THEN_PASS);
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",
"pre-commit",
"push",
"--no-retry-after-fixup",
"-b",
"main",
]);
assert!(!out.status.success(), "{}", show(&out));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains(": hook failed"),
"without retry, the failure line should appear:\n{stderr}"
);
assert!(
stderr.contains("hooks modified files (fixup commit"),
"without retry, the bare fixup line should appear:\n{stderr}"
);
assert!(
!stderr.contains("re-run on fixup commit was clean"),
"the retry-healed message must NOT appear when --no-retry-after-fixup is set:\n{stderr}"
);
}
#[test]
fn retry_after_fixup_still_fails_when_retry_also_fails() {
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 out = repo.jj_hooks(&["--runner", "pre-commit", "push", "-b", "main"]);
assert!(!out.status.success(), "{}", show(&out));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains(": hook failed"),
"expected the bare failure line, got:\n{stderr}"
);
assert!(
!stderr.contains("re-run on fixup commit was clean"),
"no fixup was produced, so no retry should have happened:\n{stderr}"
);
}
#[test]
fn run_for_revset_uses_full_range_for_multi_commit_revset() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_RECORD_RANGE);
let trunk_tip = repo
.jj(&[
"log",
"-r",
"main@origin",
"--no-graph",
"-T",
"commit_id",
"--ignore-working-copy",
])
.stdout;
let trunk_tip = String::from_utf8_lossy(&trunk_tip).trim().to_owned();
repo.write("commit_a.txt", "from A\n");
let out = repo.jj(&["commit", "-m", "commit A"]);
assert!(out.status.success(), "{}", show(&out));
repo.write("commit_b.txt", "from B\n");
let out = repo.jj(&["commit", "-m", "commit B"]);
assert!(out.status.success(), "{}", show(&out));
repo.write("commit_c.txt", "from C\n");
let out = repo.jj(&["commit", "-m", "commit C"]);
assert!(out.status.success(), "{}", show(&out));
let stack_tip = repo
.jj(&[
"log",
"-r",
"@-",
"--no-graph",
"-T",
"commit_id",
"--ignore-working-copy",
])
.stdout;
let stack_tip = String::from_utf8_lossy(&stack_tip).trim().to_owned();
let out_dir = tempfile::tempdir().unwrap();
let out_path = out_dir.path().join("range");
let out_path_str = out_path.to_string_lossy().into_owned();
let out = repo.jj_hooks_with_env(
&[
"--runner",
"pre-commit",
"run",
"--stage",
"pre-push",
"main@origin..@-",
],
&[("JJ_HOOKS_TEST_RANGE_OUT", &out_path_str)],
);
assert!(out.status.success(), "{}", show(&out));
let contents = std::fs::read_to_string(&out_path).unwrap_or_else(|e| {
panic!(
"hook didn't write to {}: {e}\n{}",
out_path.display(),
show(&out)
)
});
let from_line = contents
.lines()
.find_map(|l| l.strip_prefix("FROM="))
.unwrap_or_else(|| panic!("missing FROM= line in {contents:?}"));
let to_line = contents
.lines()
.find_map(|l| l.strip_prefix("TO="))
.unwrap_or_else(|| panic!("missing TO= line in {contents:?}"));
assert!(
trunk_tip.starts_with(from_line) || from_line.starts_with(&trunk_tip),
"expected FROM_REF to be trunk tip `{trunk_tip}`, got `{from_line}`",
);
assert!(
stack_tip.starts_with(to_line) || to_line.starts_with(&stack_tip),
"expected TO_REF to be stack tip `{stack_tip}`, got `{to_line}`. \
If this is the middle commit's SHA, the multi-commit-range fix has regressed.",
);
}