mirror of
https://github.com/git/git.git
synced 2026-03-05 14:59:04 +01:00
Add a new config `hook.forceStdoutToStderr` which allows enabling extensions.hookStdoutToStderr by default at runtime, both for new and existing repositories. This makes it easier for users to enable hook parallelization for hooks like pre-push by enforcing output consistency. See previous commit for a more in-depth explanation & alternatives considered. Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
989 lines
30 KiB
Bash
Executable File
989 lines
30 KiB
Bash
Executable File
#!/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 .git/hooks
|
|
write_script .git/hooks/pre-commit <<-EOF
|
|
echo \"Legacy Hook\"
|
|
EOF
|
|
test_when_finished rm -rf .git/hooks
|
|
}
|
|
|
|
# write_sentinel_hook <path> [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 <sentinel>.started before <sentinel>.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 <sentinel> <output>
|
|
#
|
|
# Returns a shell command string suitable for use as hook.<name>.command.
|
|
# The detector must be registered after the sentinel:
|
|
# 1. In serial mode, the sentinel has completed (and <sentinel>.done exists)
|
|
# before the detector starts.
|
|
# 2. In parallel mode, both run concurrently so <sentinel>.done has not appeared
|
|
# yet and the detector just sees <sentinel>.started.
|
|
#
|
|
# At start, poll until <sentinel>.started exists to absorb startup jitter, then
|
|
# write to <output>:
|
|
# 1. 'serial' if <sentinel>.done exists (sentinel finished before we started),
|
|
# 2. 'parallel' if only <sentinel>.started exists (sentinel still running),
|
|
# 3. 'timeout' if <sentinel>.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 &&
|
|
test_expect_code 129 git hook run -h &&
|
|
test_expect_code 129 git hook list -h &&
|
|
test_expect_code 129 git hook run --unknown 2>err &&
|
|
test_expect_code 129 git hook list &&
|
|
test_expect_code 129 git hook list -h &&
|
|
grep "unknown option" 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 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 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 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 -z test-hook >actual.raw &&
|
|
nul_to_q <actual.raw >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 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 --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 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 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 test-hook
|
|
'
|
|
done
|
|
|
|
test_expect_success 'git hook run arg u ments without -- is not allowed' '
|
|
test_expect_code 129 git hook run 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 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 test-hook >actual &&
|
|
test_cmp expect actual &&
|
|
|
|
echo "no repo no problems" >expect &&
|
|
|
|
nongit git hook run test-hook 2>actual &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
test_expect_success 'git -c core.hooksPath=<PATH> 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 test-hook -- ignored 2>>actual &&
|
|
git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
|
|
git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
|
|
git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
|
|
git -c core.hooksPath="$PWD/my-hooks/" hook run 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 &&
|
|
grep "ghi" actual &&
|
|
git hook list test-hook >actual &&
|
|
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 --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 test-hook 2>actual &&
|
|
test_grep "hook.broken.command" actual &&
|
|
test_must_fail git hook run 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 --ignore-missing test-hook 2>actual &&
|
|
test_must_be_empty actual
|
|
'
|
|
|
|
test_expect_success 'disabled hook does not appear 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 ! "inactive" actual
|
|
'
|
|
|
|
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 test-hook 2>actual &&
|
|
test_cmp expected 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" </dev/null &&
|
|
|
|
test_expect_code 1 git \
|
|
-c core.hooksPath=bad-hooks \
|
|
hook run test-hook >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()
|
|
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 --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: extension makes pre-push merge stdout to stderr' '
|
|
test_when_finished "rm -rf remote2 stdout.actual stderr.actual" &&
|
|
git init --bare remote2 &&
|
|
git remote add origin2 remote2 &&
|
|
test_commit B &&
|
|
git config set core.repositoryformatversion 1 &&
|
|
test_config extensions.hookStdoutToStderr true &&
|
|
setup_hooks pre-push &&
|
|
git push origin2 HEAD:main >stdout.actual 2>stderr.actual &&
|
|
check_stdout_merged_to_stderr pre-push
|
|
'
|
|
|
|
test_expect_success 'client hooks: pre-push defaults to serial execution' '
|
|
test_when_finished "rm -rf remote-serial repo-serial" &&
|
|
git init --bare remote-serial &&
|
|
git init repo-serial &&
|
|
git -C repo-serial remote add origin ../remote-serial &&
|
|
test_commit -C repo-serial A &&
|
|
|
|
# Setup 2 pre-push hooks; no parallel=true so they must run serially.
|
|
# Use sentinel/detector pattern: hook-1 (sentinel, configured) runs first
|
|
# because configured hooks precede traditional hooks in list order; hook-2
|
|
# (detector) runs second and checks whether hook-1 has finished.
|
|
git -C repo-serial config hook.hook-1.event pre-push &&
|
|
git -C repo-serial config hook.hook-1.command \
|
|
"touch sentinel.started; sleep 2; touch sentinel.done" &&
|
|
git -C repo-serial config hook.hook-2.event pre-push &&
|
|
git -C repo-serial config hook.hook-2.command \
|
|
"$(sentinel_detector sentinel hook.order)" &&
|
|
|
|
git -C repo-serial config hook.jobs 2 &&
|
|
|
|
git -C repo-serial push origin HEAD >out 2>err &&
|
|
echo serial >expect &&
|
|
test_cmp expect repo-serial/hook.order
|
|
'
|
|
|
|
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 </dev/null) &&
|
|
git update-index --add --cacheinfo 100644 $oid new-file \
|
|
>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 -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 -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 -j2 test-hook >out 2>err &&
|
|
echo parallel >expect &&
|
|
test_cmp expect hook.order
|
|
'
|
|
|
|
test_expect_success 'git hook run -j2 is blocked by 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 must not override parallel=false on configured hooks.
|
|
git hook run -j2 test-hook >out 2>err &&
|
|
echo serial >expect &&
|
|
test_cmp expect hook.order
|
|
'
|
|
|
|
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 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 test-hook >out 2>err &&
|
|
echo parallel >expect &&
|
|
test_cmp expect hook.order
|
|
'
|
|
|
|
test_expect_success 'hook.<name>.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 test-hook >out 2>err &&
|
|
echo parallel >expect &&
|
|
test_cmp expect hook.order
|
|
'
|
|
|
|
test_expect_success 'hook.<name>.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 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 test-hook >out 2>err &&
|
|
echo serial >expect &&
|
|
test_cmp expect 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.<event>.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 test-hook >out 2>err &&
|
|
echo parallel >expect &&
|
|
test_cmp expect hook.order
|
|
'
|
|
|
|
test_expect_success 'hook.<event>.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 test-hook >out 2>err &&
|
|
echo serial >expect &&
|
|
test_cmp expect hook.order
|
|
'
|
|
|
|
test_expect_success 'hook.<event>.jobs still requires hook.<name>.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 test-hook >out 2>err &&
|
|
echo serial >expect &&
|
|
test_cmp expect hook.order
|
|
'
|
|
|
|
test_expect_success '`git init` respects hook.forceStdoutToStderr' '
|
|
test_when_finished "rm -rf repo-init" &&
|
|
test_config_global hook.forceStdoutToStderr true &&
|
|
git init repo-init &&
|
|
git -C repo-init config extensions.hookStdoutToStderr >actual &&
|
|
echo true >expect &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
test_expect_success '`git init` does not set extensions.hookStdoutToStderr by default' '
|
|
test_when_finished "rm -rf upstream" &&
|
|
git init upstream &&
|
|
test_must_fail git -C upstream config extensions.hookStdoutToStderr
|
|
'
|
|
|
|
test_expect_success '`git clone` does not set extensions.hookStdoutToStderr by default' '
|
|
test_when_finished "rm -rf upstream repo-clone-no-ext" &&
|
|
git init upstream &&
|
|
git clone upstream repo-clone-no-ext &&
|
|
test_must_fail git -C repo-clone-no-ext config extensions.hookStdoutToStderr
|
|
'
|
|
|
|
test_expect_success '`git clone` respects hook.forceStdoutToStderr' '
|
|
test_when_finished "rm -rf upstream repo-clone" &&
|
|
git init upstream &&
|
|
test_config_global hook.forceStdoutToStderr true &&
|
|
git clone upstream repo-clone &&
|
|
git -C repo-clone config extensions.hookStdoutToStderr >actual &&
|
|
echo true >expect &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
test_expect_success 'hook.forceStdoutToStderr enables extension for existing repos' '
|
|
test_when_finished "rm -rf remote-repo existing-repo" &&
|
|
git init --bare remote-repo &&
|
|
git init -b main existing-repo &&
|
|
# No local extensions.hookStdoutToStderr config set here
|
|
# so global config should apply
|
|
test_config_global hook.forceStdoutToStderr true &&
|
|
cd existing-repo &&
|
|
test_commit A &&
|
|
git remote add origin ../remote-repo &&
|
|
setup_hooks pre-push &&
|
|
git push origin HEAD >stdout.actual 2>stderr.actual &&
|
|
check_stdout_merged_to_stderr pre-push &&
|
|
cd ..
|
|
'
|
|
|
|
test_expect_success 'hook.forceStdoutToStderr enables pre-push parallel runs' '
|
|
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 config hook.forceStdoutToStderr true &&
|
|
|
|
git -C repo-parallel push origin HEAD >out 2>err &&
|
|
echo parallel >expect &&
|
|
test_cmp expect repo-parallel/hook.order
|
|
'
|
|
|
|
test_done
|