mod harness;
use harness::{TestRepo, show};
const PRE_PUSH_REQUIRES_MARKER: &str = r#"
repos:
- repo: local
hooks:
- id: needs-setup
name: needs-setup
entry: sh -c '[ -e setup_ran ] || { echo "setup_ran marker is missing" >&2; exit 1; }'
language: system
stages: [pre-push]
always_run: true
pass_filenames: false
"#;
const PRE_PUSH_TOUCHES_MARKER: &str = r#"
repos:
- repo: local
hooks:
- id: marker
name: marker
entry: sh -c 'touch hook_ran'
language: system
stages: [pre-push]
always_run: true
pass_filenames: false
"#;
#[test]
fn setup_step_runs_in_worktree_before_hook() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_REQUIRES_MARKER);
repo.write(".gitignore", "setup_ran\n");
let out = repo.jj(&[
"config",
"set",
"--repo",
"jj-hooks.setup",
r#"[{ run = ["sh", "-c", "touch setup_ran"] }]"#,
]);
assert!(out.status.success(), "{}", show(&out));
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(),
"the setup step should have created `setup_ran` before the hook \
ran; without setup the hook fails with `marker is missing`:\n{}",
show(&out)
);
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn setup_step_failure_aborts_before_hook_runs() {
let repo = TestRepo::new();
repo.write_pre_commit_config(PRE_PUSH_TOUCHES_MARKER);
let out = repo.jj(&[
"config",
"set",
"--repo",
"jj-hooks.setup",
r#"[{ name = "broken-setup", run = ["false"] }]"#,
]);
assert!(out.status.success(), "{}", show(&out));
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(),
"push must abort when a setup step fails:\n{}",
show(&out)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("broken-setup"),
"abort message should reference the failed step `broken-setup`:\n{stderr}"
);
assert_eq!(repo.remote_commit("main"), remote_before);
}
#[test]
fn multiple_setup_steps_run_in_declared_order() {
let repo = TestRepo::new();
let hook_config = r#"
repos:
- repo: local
hooks:
- id: check-order
name: check-order
entry: sh -c 'printf "step1\nstep2\n" > expected && diff -u expected order'
language: system
stages: [pre-push]
always_run: true
pass_filenames: false
"#;
repo.write_pre_commit_config(hook_config);
repo.write(".gitignore", "order\nexpected\n");
let out = repo.jj(&[
"config",
"set",
"--repo",
"jj-hooks.setup",
r#"[
{ run = ["sh", "-c", "echo step1 >> order"] },
{ run = ["sh", "-c", "echo step2 >> order"] },
]"#,
]);
assert!(out.status.success(), "{}", show(&out));
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(),
"both steps should run in declared order so `order` reads `step1\\nstep2`:\n{}",
show(&out)
);
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn setup_step_sees_workspace_env() {
let repo = TestRepo::new();
repo.write(".gitignore", "shared_resource\n");
let hook_config = r#"
repos:
- repo: local
hooks:
- id: check-shared
name: check-shared
entry: sh -c 'grep -q "from primary" shared_resource || { echo "shared_resource missing or wrong content" >&2; exit 1; }'
language: system
stages: [pre-push]
always_run: true
pass_filenames: false
"#;
repo.write_pre_commit_config(hook_config);
let out = repo.jj(&[
"config",
"set",
"--repo",
"jj-hooks.setup",
r#"[{ run = ["sh", "-c", "cp \"$JJ_HOOKS_WORKSPACE/shared_resource\" ."] }]"#,
]);
assert!(out.status.success(), "{}", show(&out));
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));
std::fs::write(repo.primary().join("shared_resource"), "from primary\n").unwrap();
let head = repo.commit_id_of("main");
let out = repo.jj_hooks(&["--runner", "pre-commit", "push", "-b", "main"]);
assert!(
out.status.success(),
"the setup step should resolve $JJ_HOOKS_WORKSPACE to the invocation \
workspace and copy `shared_resource` into the ephemeral worktree:\n{}",
show(&out)
);
assert_eq!(repo.remote_commit("main").as_deref(), Some(head.as_str()));
}
#[test]
fn no_setup_config_is_a_silent_no_op() {
let repo = TestRepo::new();
repo.write_pre_commit_config(harness::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()));
}