#!/bin/sh test_description='git-hook command and config-managed multihooks' . ./test-lib.sh . "$TEST_DIRECTORY"/lib-terminal.sh setup_hooks () { test_config hook.ghi.command "/path/ghi" && test_config hook.ghi.event pre-commit --add && test_config hook.ghi.event test-hook --add && test_config_global hook.def.command "/path/def" && test_config_global hook.def.event pre-commit --add } setup_hookdir () { mkdir -p .git/hooks && write_script .git/hooks/pre-commit <<-EOF && echo \"Legacy Hook\" EOF test_when_finished rm -rf .git/hooks } # write_sentinel_hook [sentinel] # # Writes a hook that marks itself as started, sleeps for a few seconds, then # marks itself done. The sleep must be long enough that sentinel_detector can # observe .started before .done appears when both hooks # run concurrently in parallel mode. write_sentinel_hook () { sentinel="${2:-sentinel}" write_script "$1" <<-EOF touch ${sentinel}.started && sleep 2 && touch ${sentinel}.done EOF } # sentinel_detector # # Returns a shell command string suitable for use as hook..command. # The detector must be registered after the sentinel: # 1. In serial mode, the sentinel has completed (and .done exists) # before the detector starts. # 2. In parallel mode, both run concurrently so .done has not appeared # yet and the detector just sees .started. # # At start, poll until .started exists to absorb startup jitter, then # write to : # 1. 'serial' if .done exists (sentinel finished before we started), # 2. 'parallel' if only .started exists (sentinel still running), # 3. 'timeout' if .started never appeared. # # The command ends with ':' so when git appends "$@" for hooks that receive # positional arguments (e.g. pre-push), the result ': "$@"' is valid shell # rather than a syntax error 'fi "$@"'. sentinel_detector () { cat <<-EOF i=0 while ! test -f ${1}.started && test \$i -lt 10; do sleep 1 i=\$((i+1)) done if test -f ${1}.done; then echo serial >${2} elif test -f ${1}.started; then echo parallel >${2} else echo timeout >${2} fi : EOF } test_expect_success 'git hook usage' ' test_expect_code 129 git hook && test_expect_code 129 git hook run && git hook run -h && test_expect_code 129 git hook run --unknown 2>err && test_expect_code 129 git hook list && git hook list -h && test_grep "unknown option" err ' test_expect_success 'git hook list: unknown hook name is rejected' ' test_must_fail git hook list prereceive 2>err && test_grep "unknown hook event" err ' test_expect_success 'git hook run: unknown hook name is rejected' ' test_must_fail git hook run prereceive 2>err && test_grep "unknown hook event" err ' test_expect_success 'git hook list: known hook name is accepted' ' test_must_fail git hook list pre-receive 2>err && test_grep ! "unknown hook event" err ' test_expect_success 'git hook run: known hook name is accepted' ' git hook run --ignore-missing pre-receive 2>err && test_grep ! "unknown hook event" err ' test_expect_success 'git hook run: --allow-unknown-hook-name overrides rejection' ' git hook run --allow-unknown-hook-name --ignore-missing custom-hook 2>err && test_grep ! "unknown hook event" err ' test_expect_success 'git hook list: --allow-unknown-hook-name overrides rejection' ' test_must_fail git hook list --allow-unknown-hook-name custom-hook 2>err && test_grep ! "unknown hook event" err ' test_expect_success 'git hook list: nonexistent hook' ' cat >stderr.expect <<-\EOF && warning: no hooks found for event '\''test-hook'\'' EOF test_expect_code 1 git hook list --allow-unknown-hook-name test-hook 2>stderr.actual && test_cmp stderr.expect stderr.actual ' test_expect_success 'git hook list: traditional hook from hookdir' ' test_hook test-hook <<-EOF && echo Test hook EOF cat >expect <<-\EOF && hook from hookdir EOF git hook list --allow-unknown-hook-name test-hook >actual && test_cmp expect actual ' test_expect_success 'git hook list: configured hook' ' test_config hook.myhook.command "echo Hello" && test_config hook.myhook.event test-hook --add && echo "myhook" >expect && git hook list --allow-unknown-hook-name test-hook >actual && test_cmp expect actual ' test_expect_success 'git hook list: -z shows NUL-terminated output' ' test_hook test-hook <<-EOF && echo Test hook EOF test_config hook.myhook.command "echo Hello" && test_config hook.myhook.event test-hook --add && printf "myhookQhook from hookdirQ" >expect && git hook list --allow-unknown-hook-name -z test-hook >actual.raw && nul_to_q actual && test_cmp expect actual ' test_expect_success 'git hook run: nonexistent hook' ' cat >stderr.expect <<-\EOF && error: cannot find a hook named test-hook EOF test_expect_code 1 git hook run --allow-unknown-hook-name test-hook 2>stderr.actual && test_cmp stderr.expect stderr.actual ' test_expect_success 'git hook run: nonexistent hook with --ignore-missing' ' git hook run --allow-unknown-hook-name --ignore-missing does-not-exist 2>stderr.actual && test_must_be_empty stderr.actual ' test_expect_success 'git hook run: basic' ' test_hook test-hook <<-EOF && echo Test hook EOF cat >expect <<-\EOF && Test hook EOF git hook run --allow-unknown-hook-name test-hook 2>actual && test_cmp expect actual ' test_expect_success 'git hook run: stdout and stderr both write to our stderr' ' test_hook test-hook <<-EOF && echo >&1 Will end up on stderr echo >&2 Will end up on stderr EOF cat >stderr.expect <<-\EOF && Will end up on stderr Will end up on stderr EOF git hook run --allow-unknown-hook-name test-hook >stdout.actual 2>stderr.actual && test_cmp stderr.expect stderr.actual && test_must_be_empty stdout.actual ' for code in 1 2 128 129 do test_expect_success "git hook run: exit code $code is passed along" ' test_hook test-hook <<-EOF && exit $code EOF test_expect_code $code git hook run --allow-unknown-hook-name test-hook ' done test_expect_success 'git hook run arg u ments without -- is not allowed' ' test_expect_code 129 git hook run --allow-unknown-hook-name test-hook arg u ments ' test_expect_success 'git hook run -- pass arguments' ' test_hook test-hook <<-\EOF && echo $1 echo $2 EOF cat >expect <<-EOF && arg u ments EOF git hook run --allow-unknown-hook-name test-hook -- arg "u ments" 2>actual && test_cmp expect actual ' test_expect_success 'git hook run: out-of-repo runs execute global hooks' ' test_config_global hook.global-hook.event test-hook --add && test_config_global hook.global-hook.command "echo no repo no problems" --add && echo "global-hook" >expect && nongit git hook list --allow-unknown-hook-name test-hook >actual && test_cmp expect actual && echo "no repo no problems" >expect && nongit git hook run --allow-unknown-hook-name test-hook 2>actual && test_cmp expect actual ' test_expect_success 'git -c core.hooksPath= hook run' ' mkdir my-hooks && write_script my-hooks/test-hook <<-\EOF && echo Hook ran $1 EOF cat >expect <<-\EOF && Test hook Hook ran one Hook ran two Hook ran three Hook ran four EOF test_hook test-hook <<-EOF && echo Test hook EOF # Test various ways of specifying the path. See also # t1350-config-hooks-path.sh >actual && git hook run --allow-unknown-hook-name test-hook -- ignored 2>>actual && git -c core.hooksPath=my-hooks hook run --allow-unknown-hook-name test-hook -- one 2>>actual && git -c core.hooksPath=my-hooks/ hook run --allow-unknown-hook-name test-hook -- two 2>>actual && git -c core.hooksPath="$PWD/my-hooks" hook run --allow-unknown-hook-name test-hook -- three 2>>actual && git -c core.hooksPath="$PWD/my-hooks/" hook run --allow-unknown-hook-name test-hook -- four 2>>actual && test_cmp expect actual ' test_hook_tty () { expect_tty=$1 shift if test "$expect_tty" != "no_tty"; then cat >expect <<-\EOF STDOUT TTY STDERR TTY EOF else cat >expect <<-\EOF STDOUT NO TTY STDERR NO TTY EOF fi test_when_finished "rm -rf repo" && git init repo && test_commit -C repo A && test_commit -C repo B && git -C repo reset --soft HEAD^ && test_hook -C repo pre-commit <<-EOF && test -t 1 && echo STDOUT TTY >>actual || echo STDOUT NO TTY >>actual && test -t 2 && echo STDERR TTY >>actual || echo STDERR NO TTY >>actual EOF test_terminal git -C repo "$@" && test_cmp expect repo/actual } test_expect_success TTY 'git hook run -j1: stdout and stderr are connected to a TTY' ' # hooks running sequentially (-j1) are always connected to the tty for # optimum real-time performance. test_hook_tty tty hook run -j1 pre-commit ' test_expect_success TTY 'git hook run -jN: stdout and stderr are not connected to a TTY' ' # Hooks are not connected to the tty when run in parallel, instead they # output to a pipe through which run-command collects and de-interlaces # their outputs, which then gets passed either to the tty or a sideband. test_hook_tty no_tty hook run -j2 pre-commit ' test_expect_success TTY 'git commit: stdout and stderr are connected to a TTY' ' test_hook_tty tty commit -m"B.new" ' test_expect_success 'git hook list orders by config order' ' setup_hooks && cat >expected <<-\EOF && def ghi EOF git hook list pre-commit >actual && test_cmp expected actual ' test_expect_success 'git hook list reorders on duplicate event declarations' ' setup_hooks && # 'def' is usually configured globally; move it to the end by # configuring it locally. test_config hook.def.event "pre-commit" --add && cat >expected <<-\EOF && ghi def EOF git hook list pre-commit >actual && test_cmp expected actual ' test_expect_success 'git hook list: empty event value resets events' ' setup_hooks && # ghi is configured for pre-commit; reset it with an empty value test_config hook.ghi.event "" --add && # only def should remain for pre-commit echo "def" >expected && git hook list pre-commit >actual && test_cmp expected actual ' test_expect_success 'hook can be configured for multiple events' ' setup_hooks && # 'ghi' should be included in both 'pre-commit' and 'test-hook' git hook list pre-commit >actual && test_grep "ghi" actual && git hook list --allow-unknown-hook-name test-hook >actual && test_grep "ghi" actual ' test_expect_success 'git hook list shows hooks from the hookdir' ' setup_hookdir && cat >expected <<-\EOF && hook from hookdir EOF git hook list pre-commit >actual && test_cmp expected actual ' test_expect_success 'inline hook definitions execute oneliners' ' test_config hook.oneliner.event "pre-commit" && test_config hook.oneliner.command "echo \"Hello World\"" && echo "Hello World" >expected && # hooks are run with stdout_to_stderr = 1 git hook run pre-commit 2>actual && test_cmp expected actual ' test_expect_success 'inline hook definitions resolve paths' ' write_script sample-hook.sh <<-\EOF && echo \"Sample Hook\" EOF test_when_finished "rm sample-hook.sh" && test_config hook.sample-hook.event pre-commit && test_config hook.sample-hook.command "\"$(pwd)/sample-hook.sh\"" && echo \"Sample Hook\" >expected && # hooks are run with stdout_to_stderr = 1 git hook run pre-commit 2>actual && test_cmp expected actual ' test_expect_success 'hookdir hook included in git hook run' ' setup_hookdir && echo \"Legacy Hook\" >expected && # hooks are run with stdout_to_stderr = 1 git hook run pre-commit 2>actual && test_cmp expected actual ' test_expect_success 'stdin to multiple hooks' ' test_config hook.stdin-a.event "test-hook" && test_config hook.stdin-a.command "xargs -P1 -I% echo a%" && test_config hook.stdin-b.event "test-hook" && test_config hook.stdin-b.command "xargs -P1 -I% echo b%" && cat >input <<-\EOF && 1 2 3 EOF cat >expected <<-\EOF && a1 a2 a3 b1 b2 b3 EOF git hook run --allow-unknown-hook-name --to-stdin=input test-hook 2>actual && test_cmp expected actual ' test_expect_success 'rejects hooks with no commands configured' ' test_config hook.broken.event "test-hook" && test_must_fail git hook list --allow-unknown-hook-name test-hook 2>actual && test_grep "hook.broken.command" actual && test_must_fail git hook run --allow-unknown-hook-name test-hook 2>actual && test_grep "hook.broken.command" actual ' test_expect_success 'disabled hook is not run' ' test_config hook.skipped.event "test-hook" && test_config hook.skipped.command "echo \"Should not run\"" && test_config hook.skipped.enabled false && git hook run --allow-unknown-hook-name --ignore-missing test-hook 2>actual && test_must_be_empty actual ' test_expect_success 'disabled hook with no command warns' ' test_config hook.nocommand.event "pre-commit" && test_config hook.nocommand.enabled false && git hook list pre-commit 2>actual && test_grep "disabled hook.*nocommand.*no command configured" actual ' test_expect_success 'disabled hook appears as disabled in git hook list' ' test_config hook.active.event "pre-commit" && test_config hook.active.command "echo active" && test_config hook.inactive.event "pre-commit" && test_config hook.inactive.command "echo inactive" && test_config hook.inactive.enabled false && git hook list pre-commit >actual && test_grep "^active$" actual && test_grep "^disabled inactive$" actual ' test_expect_success 'disabled hook shows scope with --show-scope' ' test_config hook.myhook.event "pre-commit" && test_config hook.myhook.command "echo hi" && test_config hook.myhook.enabled false && git hook list --show-scope pre-commit >actual && test_grep "^local disabled myhook$" actual ' test_expect_success 'disabled configured hook is not reported as existing by hook_exists' ' test_when_finished "rm -f git-bugreport-hook-exists-test.txt" && test_config hook.linter.event "pre-commit" && test_config hook.linter.command "echo lint" && test_config hook.linter.enabled false && git bugreport -s hook-exists-test && test_grep ! "pre-commit" git-bugreport-hook-exists-test.txt ' test_expect_success 'globally disabled hook can be re-enabled locally' ' test_config_global hook.global-hook.event "test-hook" && test_config_global hook.global-hook.command "echo \"global-hook ran\"" && test_config_global hook.global-hook.enabled false && test_config hook.global-hook.enabled true && echo "global-hook ran" >expected && git hook run --allow-unknown-hook-name test-hook 2>actual && test_cmp expected actual ' test_expect_success 'configured hooks run before hookdir hook' ' setup_hookdir && test_config hook.first.event "pre-commit" && test_config hook.first.command "echo first" && test_config hook.second.event "pre-commit" && test_config hook.second.command "echo second" && cat >expected <<-\EOF && first second hook from hookdir EOF git hook list pre-commit >actual && test_cmp expected actual && # "Legacy Hook" is the output of the hookdir pre-commit script # written by setup_hookdir() above. cat >expected <<-\EOF && first second "Legacy Hook" EOF git hook run pre-commit 2>actual && test_cmp expected actual ' test_expect_success 'git hook list --show-scope shows config scope' ' setup_hookdir && test_config_global hook.global-hook.command "echo global" && test_config_global hook.global-hook.event pre-commit --add && test_config hook.local-hook.command "echo local" && test_config hook.local-hook.event pre-commit --add && cat >expected <<-\EOF && global global-hook local local-hook hook from hookdir EOF git hook list --show-scope pre-commit >actual && test_cmp expected actual && # without --show-scope the scope must not appear git hook list pre-commit >actual && test_grep ! "^global " actual && test_grep ! "^local " actual ' test_expect_success 'git hook run a hook with a bad shebang' ' test_when_finished "rm -rf bad-hooks" && mkdir bad-hooks && write_script bad-hooks/test-hook "/bad/path/no/spaces" out 2>err && test_must_be_empty out && # TODO: We should emit the same (or at least a more similar) # error on MINGW (essentially Git for Windows) and all other # platforms.. See the OS-specific code in start_command() test_grep -E "^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook" err ' test_expect_success 'stdin to hooks' ' mkdir -p .git/hooks && write_script .git/hooks/test-hook <<-\EOF && echo BEGIN stdin cat echo END stdin EOF cat >expect <<-EOF && BEGIN stdin hello END stdin EOF echo hello >input && git hook run --allow-unknown-hook-name --to-stdin=input test-hook 2>actual && test_cmp expect actual ' check_stdout_separate_from_stderr () { for hook in "$@" do # Ensure hook's stdout is only in stdout, not stderr test_grep "Hook $hook stdout" stdout.actual || return 1 test_grep ! "Hook $hook stdout" stderr.actual || return 1 # Ensure hook's stderr is only in stderr, not stdout test_grep "Hook $hook stderr" stderr.actual || return 1 test_grep ! "Hook $hook stderr" stdout.actual || return 1 done } check_stdout_merged_to_stderr () { for hook in "$@" do # Ensure hook's stdout is only in stderr, not stdout test_grep "Hook $hook stdout" stderr.actual || return 1 test_grep ! "Hook $hook stdout" stdout.actual || return 1 # Ensure hook's stderr is only in stderr, not stdout test_grep "Hook $hook stderr" stderr.actual || return 1 test_grep ! "Hook $hook stderr" stdout.actual || return 1 done } setup_hooks () { for hook in "$@" do test_hook $hook <<-EOF echo >&1 Hook $hook stdout echo >&2 Hook $hook stderr EOF done } test_expect_success 'client hooks: pre-push expects separate stdout and stderr' ' test_when_finished "rm -f stdout.actual stderr.actual" && git init --bare remote && git remote add origin remote && test_commit A && setup_hooks pre-push && git push origin HEAD:main >stdout.actual 2>stderr.actual && check_stdout_separate_from_stderr pre-push ' test_expect_success 'client hooks: commit hooks expect stdout redirected to stderr' ' hooks="pre-commit prepare-commit-msg \ commit-msg post-commit \ reference-transaction" && setup_hooks $hooks && test_when_finished "rm -f stdout.actual stderr.actual" && git checkout -B main && git checkout -b branch-a && test_commit commit-on-branch-a && git commit --allow-empty -m "Test" >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr $hooks ' test_expect_success 'client hooks: checkout hooks expect stdout redirected to stderr' ' setup_hooks post-checkout reference-transaction && test_when_finished "rm -f stdout.actual stderr.actual" && git checkout -b new-branch main >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr post-checkout reference-transaction ' test_expect_success 'client hooks: merge hooks expect stdout redirected to stderr' ' setup_hooks pre-merge-commit post-merge reference-transaction && test_when_finished "rm -f stdout.actual stderr.actual" && test_commit new-branch-commit && git merge --no-ff branch-a >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr pre-merge-commit post-merge reference-transaction ' test_expect_success 'client hooks: post-rewrite hooks expect stdout redirected to stderr' ' setup_hooks post-rewrite reference-transaction && test_when_finished "rm -f stdout.actual stderr.actual" && git commit --amend --allow-empty --no-edit >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr post-rewrite reference-transaction ' test_expect_success 'client hooks: applypatch hooks expect stdout redirected to stderr' ' setup_hooks applypatch-msg pre-applypatch post-applypatch && test_when_finished "rm -f stdout.actual stderr.actual" && git checkout -b branch-b main && test_commit branch-b && git format-patch -1 --stdout >patch && git checkout -b branch-c main && git am patch >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr applypatch-msg pre-applypatch post-applypatch ' test_expect_success 'client hooks: rebase hooks expect stdout redirected to stderr' ' setup_hooks pre-rebase && test_when_finished "rm -f stdout.actual stderr.actual" && git checkout -b branch-d main && test_commit branch-d && git checkout main && test_commit diverge-main && git checkout branch-d && git rebase main >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr pre-rebase ' test_expect_success 'client hooks: post-index-change expects stdout redirected to stderr' ' setup_hooks post-index-change && test_when_finished "rm -f stdout.actual stderr.actual" && oid=$(git hash-object -w --stdin stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr post-index-change ' test_expect_success 'server hooks expect stdout redirected to stderr' ' test_when_finished "rm -f stdout.actual stderr.actual" && git init --bare remote-server && git remote add origin-server remote-server && cd remote-server && setup_hooks pre-receive update post-receive post-update && cd .. && git push origin-server HEAD:new-branch >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr pre-receive update post-receive post-update ' test_expect_success 'server push-to-checkout hook expects stdout redirected to stderr' ' test_when_finished "rm -f stdout.actual stderr.actual" && git init server && git -C server checkout -b main && test_config -C server receive.denyCurrentBranch updateInstead && git remote add origin-server-2 server && cd server && setup_hooks push-to-checkout && cd .. && git push origin-server-2 HEAD:main >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr push-to-checkout ' test_expect_success 'parallel hook output is not interleaved' ' test_when_finished "rm -rf .git/hooks" && write_script .git/hooks/test-hook <<-EOF && echo "Hook 1 Start" sleep 1 echo "Hook 1 End" EOF test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "echo \"Hook 2 Start\"; sleep 2; echo \"Hook 2 End\"" && test_config hook.hook-2.parallel true && test_config hook.hook-3.event test-hook && test_config hook.hook-3.command \ "echo \"Hook 3 Start\"; sleep 3; echo \"Hook 3 End\"" && test_config hook.hook-3.parallel true && git hook run --allow-unknown-hook-name -j3 test-hook >out 2>err.parallel && # Verify Hook 1 output is grouped sed -n "/Hook 1 Start/,/Hook 1 End/p" err.parallel >hook1_out && test_line_count = 2 hook1_out && # Verify Hook 2 output is grouped sed -n "/Hook 2 Start/,/Hook 2 End/p" err.parallel >hook2_out && test_line_count = 2 hook2_out && # Verify Hook 3 output is grouped sed -n "/Hook 3 Start/,/Hook 3 End/p" err.parallel >hook3_out && test_line_count = 2 hook3_out ' test_expect_success 'git hook run -j1 runs hooks in series' ' test_when_finished "rm -rf .git/hooks" && test_config hook.series-1.event "test-hook" && test_config hook.series-1.command "echo 1" --add && test_config hook.series-2.event "test-hook" && test_config hook.series-2.command "echo 2" --add && mkdir -p .git/hooks && write_script .git/hooks/test-hook <<-EOF && echo 3 EOF cat >expected <<-\EOF && 1 2 3 EOF git hook run --allow-unknown-hook-name -j1 test-hook 2>actual && test_cmp expected actual ' test_expect_success 'git hook run -j2 runs hooks in parallel' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_when_finished "rm -rf .git/hooks" && mkdir -p .git/hooks && write_sentinel_hook .git/hooks/test-hook && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && test_config hook.hook-2.parallel true && git hook run --allow-unknown-hook-name -j2 test-hook >out 2>err && echo parallel >expect && test_cmp expect hook.order ' test_expect_success 'git hook run -j2 overrides parallel=false' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_config hook.hook-1.event test-hook && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && # hook-1 intentionally has no parallel=true test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && # hook-2 also has no parallel=true # -j2 overrides parallel=false; hooks run in parallel with a warning. git hook run --allow-unknown-hook-name -j2 test-hook >out 2>err && echo parallel >expect && test_cmp expect hook.order ' test_expect_success 'git hook run -j2 warns for hooks not marked parallel=true' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "true" && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command "true" && # neither hook has parallel=true git hook run --allow-unknown-hook-name -j2 test-hook >out 2>err && test_grep "hook .hook-1. is not marked as parallel=true" err && test_grep "hook .hook-2. is not marked as parallel=true" err ' test_expect_success 'hook.jobs=1 config runs hooks in series' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && # Use two configured hooks so the execution order is deterministic: # hook-1 (sentinel) is listed before hook-2 (detector), so hook-1 # always runs first even in serial mode. test_config hook.hook-1.event test-hook && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && test_config hook.jobs 1 && git hook run --allow-unknown-hook-name test-hook >out 2>err && echo serial >expect && test_cmp expect hook.order ' test_expect_success 'hook.jobs=2 config runs hooks in parallel' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_when_finished "rm -rf .git/hooks" && mkdir -p .git/hooks && write_sentinel_hook .git/hooks/test-hook && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && test_config hook.hook-2.parallel true && test_config hook.jobs 2 && git hook run --allow-unknown-hook-name test-hook >out 2>err && echo parallel >expect && test_cmp expect hook.order ' test_expect_success 'hook..parallel=true enables parallel execution' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_config hook.hook-1.event test-hook && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && test_config hook.hook-1.parallel true && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && test_config hook.hook-2.parallel true && test_config hook.jobs 2 && git hook run --allow-unknown-hook-name test-hook >out 2>err && echo parallel >expect && test_cmp expect hook.order ' test_expect_success 'hook..parallel=false (default) forces serial execution' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_config hook.hook-1.event test-hook && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && test_config hook.jobs 2 && git hook run --allow-unknown-hook-name test-hook >out 2>err && echo serial >expect && test_cmp expect hook.order ' test_expect_success 'one non-parallel hook forces the whole event to run serially' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_config hook.hook-1.event test-hook && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && test_config hook.hook-1.parallel true && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && # hook-2 has no parallel=true: should force serial for all test_config hook.jobs 2 && git hook run --allow-unknown-hook-name test-hook >out 2>err && echo serial >expect && test_cmp expect hook.order ' test_expect_success 'client hooks: pre-push parallel execution merges stdout to stderr' ' test_when_finished "rm -rf remote-par stdout.actual stderr.actual" && git init --bare remote-par && git remote add origin-par remote-par && test_commit par-commit && mkdir -p .git/hooks && setup_hooks pre-push && test_config hook.jobs 2 && git push origin-par HEAD:main >stdout.actual 2>stderr.actual && check_stdout_merged_to_stderr pre-push ' test_expect_success 'client hooks: pre-push runs in parallel when hook.jobs > 1' ' test_when_finished "rm -rf repo-parallel remote-parallel" && git init --bare remote-parallel && git init repo-parallel && git -C repo-parallel remote add origin ../remote-parallel && test_commit -C repo-parallel A && write_sentinel_hook repo-parallel/.git/hooks/pre-push && git -C repo-parallel config hook.hook-2.event pre-push && git -C repo-parallel config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && git -C repo-parallel config hook.hook-2.parallel true && git -C repo-parallel config hook.jobs 2 && git -C repo-parallel push origin HEAD >out 2>err && echo parallel >expect && test_cmp expect repo-parallel/hook.order ' test_expect_success 'hook.jobs=2 is ignored for force-serial hooks (pre-commit)' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_config hook.hook-1.event pre-commit && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && test_config hook.hook-1.parallel true && test_config hook.hook-2.event pre-commit && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && test_config hook.hook-2.parallel true && test_config hook.jobs 2 && git commit --allow-empty -m "test: verify force-serial on pre-commit" && echo serial >expect && test_cmp expect hook.order ' test_expect_success 'hook..jobs overrides hook.jobs for that event' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_config hook.hook-1.event test-hook && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && test_config hook.hook-1.parallel true && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && test_config hook.hook-2.parallel true && # Global hook.jobs=1 (serial), but per-event override allows parallel. test_config hook.jobs 1 && test_config hook.test-hook.jobs 2 && git hook run --allow-unknown-hook-name test-hook >out 2>err && echo parallel >expect && test_cmp expect hook.order ' test_expect_success 'hook..jobs=1 forces serial even when hook.jobs>1' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_config hook.hook-1.event test-hook && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && test_config hook.hook-1.parallel true && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && test_config hook.hook-2.parallel true && # Global hook.jobs=4 allows parallel, but per-event override forces serial. test_config hook.jobs 4 && test_config hook.test-hook.jobs 1 && git hook run --allow-unknown-hook-name test-hook >out 2>err && echo serial >expect && test_cmp expect hook.order ' test_expect_success 'hook..jobs still requires hook..parallel=true' ' test_when_finished "rm -f sentinel.started sentinel.done hook.order" && test_config hook.hook-1.event test-hook && test_config hook.hook-1.command \ "touch sentinel.started; sleep 2; touch sentinel.done" && # hook-1 intentionally has no parallel=true test_config hook.hook-2.event test-hook && test_config hook.hook-2.command \ "$(sentinel_detector sentinel hook.order)" && # hook-2 also has no parallel=true # Per-event jobs=2 but no hook has parallel=true: must still run serially. test_config hook.test-hook.jobs 2 && git hook run --allow-unknown-hook-name test-hook >out 2>err && echo serial >expect && test_cmp expect hook.order ' test_expect_success 'hook..jobs warns when name has .command' ' test_config hook.my-hook.command "true" && test_config hook.my-hook.jobs 2 && git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err && test_grep "hook.my-hook.jobs.*friendly-name" err ' test_expect_success 'hook..jobs warns when name has .event' ' test_config hook.my-hook.event test-hook && test_config hook.my-hook.command "true" && test_config hook.my-hook.jobs 2 && git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err && test_grep "hook.my-hook.jobs.*friendly-name" err ' test_expect_success 'hook..jobs warns when name has .parallel' ' test_config hook.my-hook.event test-hook && test_config hook.my-hook.command "true" && test_config hook.my-hook.parallel true && test_config hook.my-hook.jobs 2 && git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err && test_grep "hook.my-hook.jobs.*friendly-name" err ' test_expect_success 'hook..jobs does not warn for a real event name' ' test_config hook.test-hook.jobs 2 && git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err && test_grep ! "friendly-name" err ' test_expect_success 'hook.jobs=-1 resolves to online_cpus()' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "true" && test_config hook.hook-1.parallel true && test_config hook.jobs -1 && cpus=$(test-tool online-cpus) && GIT_TRACE2_EVENT="$(pwd)/trace.txt" \ git hook run --allow-unknown-hook-name test-hook >out 2>err && test_grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt ' test_expect_success 'hook..jobs=-1 resolves to online_cpus()' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "true" && test_config hook.hook-1.parallel true && test_config hook.test-hook.jobs -1 && cpus=$(test-tool online-cpus) && GIT_TRACE2_EVENT="$(pwd)/trace.txt" \ git hook run --allow-unknown-hook-name test-hook >out 2>err && test_grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt ' test_expect_success 'git hook run -j-1 resolves to online_cpus()' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "true" && test_config hook.hook-1.parallel true && cpus=$(test-tool online-cpus) && GIT_TRACE2_EVENT="$(pwd)/trace.txt" \ git hook run --allow-unknown-hook-name -j-1 test-hook >out 2>err && test_grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt ' test_expect_success 'hook.jobs rejects values less than -1' ' test_config hook.jobs -2 && git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err && test_grep "hook.jobs must be a positive integer or -1" err ' test_expect_success 'hook..jobs rejects values less than -1' ' test_config hook.test-hook.jobs -5 && git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err && test_grep "hook.test-hook.jobs must be a positive integer or -1" err ' test_expect_success 'hook..enabled=false skips all hooks for event' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "echo ran" && test_config hook.test-hook.enabled false && git hook run --allow-unknown-hook-name test-hook >out 2>err && test_must_be_empty out ' test_expect_success 'hook..enabled=true does not suppress hooks' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "echo ran" && test_config hook.test-hook.enabled true && git hook run --allow-unknown-hook-name test-hook >out 2>err && test_grep "ran" err ' test_expect_success 'hook..enabled=false does not affect other events' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "echo ran" && test_config hook.other-event.enabled false && git hook run --allow-unknown-hook-name test-hook >out 2>err && test_grep "ran" err ' test_expect_success 'hook..enabled=false still disables that hook' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "echo hook-1" && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command "echo hook-2" && test_config hook.hook-1.enabled false && git hook run --allow-unknown-hook-name test-hook >out 2>err && test_grep ! "hook-1" err && test_grep "hook-2" err ' test_expect_success 'git hook list shows event-disabled hooks as event-disabled' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "echo ran" && test_config hook.hook-2.event test-hook && test_config hook.hook-2.command "echo ran" && test_config hook.test-hook.enabled false && git hook list --allow-unknown-hook-name test-hook >actual && test_grep "^event-disabled hook-1$" actual && test_grep "^event-disabled hook-2$" actual ' test_expect_success 'git hook list shows scope with event-disabled' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "echo ran" && test_config hook.test-hook.enabled false && git hook list --allow-unknown-hook-name --show-scope test-hook >actual && test_grep "^local event-disabled hook-1$" actual ' test_expect_success 'git hook list still shows hooks when event is disabled' ' test_config hook.hook-1.event test-hook && test_config hook.hook-1.command "echo ran" && test_config hook.test-hook.enabled false && git hook list --allow-unknown-hook-name test-hook >actual && test_grep "event-disabled" actual ' test_expect_success 'friendly-name matching known event name is rejected' ' test_config hook.pre-commit.event pre-commit && test_config hook.pre-commit.command "echo oops" && test_must_fail git hook run pre-commit 2>err && test_grep "collides with a known event name" err ' test_expect_success 'friendly-name matching known event name is rejected even for different event' ' test_config hook.pre-commit.event post-commit && test_config hook.pre-commit.command "echo oops" && test_must_fail git hook run post-commit 2>err && test_grep "collides with a known event name" err ' test_expect_success 'friendly-name matching unknown event warns' ' test_config hook.test-hook.event test-hook && test_config hook.test-hook.command "echo ran" && git hook run --allow-unknown-hook-name test-hook >out 2>err && test_grep "same as its event" err ' test_expect_success 'hooks in parallel that do not read input' ' # Add this to our $PATH to avoid having to write the whole trash # directory into our config options, which would require quoting. mkdir bin && PATH=$PWD/bin:$PATH && write_script bin/hook-fast <<-\EOF && # This hook does not read its input, so the parent process # may see SIGPIPE if it is not ignored. It should happen # relatively quickly. exit 0 EOF write_script bin/hook-slow <<-\EOF && # This hook is slow, so we expect it to still be running # when the other hook has exited (and the parent has a pipe error # writing to it). # # So we want to be slow enough that we expect this to happen, but not # so slow that the test takes forever. 1 second is probably enough # in practice (and if it is occasionally not on a loaded system, we # will err on the side of having the test pass). sleep 1 exit 0 EOF git init --bare parallel.git && git -C parallel.git config hook.fast.command "hook-fast" && git -C parallel.git config hook.fast.event pre-receive && git -C parallel.git config hook.fast.parallel true && git -C parallel.git config hook.slow.command "hook-slow" && git -C parallel.git config hook.slow.event pre-receive && git -C parallel.git config hook.slow.parallel true && git -C parallel.git config hook.jobs 2 && git push ./parallel.git "+refs/heads/*:refs/heads/*" ' test_done