Raw
1 #!/bin/sh
2
3 test_description='git-hook command and config-managed multihooks'
4
5 . ./test-lib.sh
6 . "$TEST_DIRECTORY"/lib-terminal.sh
7
8 setup_hooks () {
9 test_config hook.ghi.command "/path/ghi" &&
10 test_config hook.ghi.event pre-commit --add &&
11 test_config hook.ghi.event test-hook --add &&
12 test_config_global hook.def.command "/path/def" &&
13 test_config_global hook.def.event pre-commit --add
14 }
15
16 setup_hookdir () {
17 mkdir -p .git/hooks &&
18 write_script .git/hooks/pre-commit <<-EOF &&
19 echo \"Legacy Hook\"
20 EOF
21 test_when_finished rm -rf .git/hooks
22 }
23
24 # write_sentinel_hook <path> [sentinel]
25 #
26 # Writes a hook that marks itself as started, sleeps for a few seconds, then
27 # marks itself done. The sleep must be long enough that sentinel_detector can
28 # observe <sentinel>.started before <sentinel>.done appears when both hooks
29 # run concurrently in parallel mode.
30 write_sentinel_hook () {
31 sentinel="${2:-sentinel}"
32 write_script "$1" <<-EOF
33 touch ${sentinel}.started &&
34 sleep 2 &&
35 touch ${sentinel}.done
36 EOF
37 }
38
39 # sentinel_detector <sentinel> <output>
40 #
41 # Returns a shell command string suitable for use as hook.<name>.command.
42 # The detector must be registered after the sentinel:
43 # 1. In serial mode, the sentinel has completed (and <sentinel>.done exists)
44 # before the detector starts.
45 # 2. In parallel mode, both run concurrently so <sentinel>.done has not appeared
46 # yet and the detector just sees <sentinel>.started.
47 #
48 # At start, poll until <sentinel>.started exists to absorb startup jitter, then
49 # write to <output>:
50 # 1. 'serial' if <sentinel>.done exists (sentinel finished before we started),
51 # 2. 'parallel' if only <sentinel>.started exists (sentinel still running),
52 # 3. 'timeout' if <sentinel>.started never appeared.
53 #
54 # The command ends with ':' so when git appends "$@" for hooks that receive
55 # positional arguments (e.g. pre-push), the result ': "$@"' is valid shell
56 # rather than a syntax error 'fi "$@"'.
57 sentinel_detector () {
58 cat <<-EOF
59 i=0
60 while ! test -f ${1}.started && test \$i -lt 10; do
61 sleep 1
62 i=\$((i+1))
63 done
64 if test -f ${1}.done; then
65 echo serial >${2}
66 elif test -f ${1}.started; then
67 echo parallel >${2}
68 else
69 echo timeout >${2}
70 fi
71 :
72 EOF
73 }
74
75 test_expect_success 'git hook usage' '
76 test_expect_code 129 git hook &&
77 test_expect_code 129 git hook run &&
78 test_expect_code 129 git hook run -h &&
79 test_expect_code 129 git hook run --unknown 2>err &&
80 test_expect_code 129 git hook list &&
81 test_expect_code 129 git hook list -h &&
82 grep "unknown option" err
83 '
84
85 test_expect_success 'git hook list: unknown hook name is rejected' '
86 test_must_fail git hook list prereceive 2>err &&
87 test_grep "unknown hook event" err
88 '
89
90 test_expect_success 'git hook run: unknown hook name is rejected' '
91 test_must_fail git hook run prereceive 2>err &&
92 test_grep "unknown hook event" err
93 '
94
95 test_expect_success 'git hook list: known hook name is accepted' '
96 test_must_fail git hook list pre-receive 2>err &&
97 test_grep ! "unknown hook event" err
98 '
99
100 test_expect_success 'git hook run: known hook name is accepted' '
101 git hook run --ignore-missing pre-receive 2>err &&
102 test_grep ! "unknown hook event" err
103 '
104
105 test_expect_success 'git hook run: --allow-unknown-hook-name overrides rejection' '
106 git hook run --allow-unknown-hook-name --ignore-missing custom-hook 2>err &&
107 test_grep ! "unknown hook event" err
108 '
109
110 test_expect_success 'git hook list: --allow-unknown-hook-name overrides rejection' '
111 test_must_fail git hook list --allow-unknown-hook-name custom-hook 2>err &&
112 test_grep ! "unknown hook event" err
113 '
114
115 test_expect_success 'git hook list: nonexistent hook' '
116 cat >stderr.expect <<-\EOF &&
117 warning: no hooks found for event '\''test-hook'\''
118 EOF
119 test_expect_code 1 git hook list --allow-unknown-hook-name test-hook 2>stderr.actual &&
120 test_cmp stderr.expect stderr.actual
121 '
122
123 test_expect_success 'git hook list: traditional hook from hookdir' '
124 test_hook test-hook <<-EOF &&
125 echo Test hook
126 EOF
127
128 cat >expect <<-\EOF &&
129 hook from hookdir
130 EOF
131 git hook list --allow-unknown-hook-name test-hook >actual &&
132 test_cmp expect actual
133 '
134
135 test_expect_success 'git hook list: configured hook' '
136 test_config hook.myhook.command "echo Hello" &&
137 test_config hook.myhook.event test-hook --add &&
138
139 echo "myhook" >expect &&
140 git hook list --allow-unknown-hook-name test-hook >actual &&
141 test_cmp expect actual
142 '
143
144 test_expect_success 'git hook list: -z shows NUL-terminated output' '
145 test_hook test-hook <<-EOF &&
146 echo Test hook
147 EOF
148 test_config hook.myhook.command "echo Hello" &&
149 test_config hook.myhook.event test-hook --add &&
150
151 printf "myhookQhook from hookdirQ" >expect &&
152 git hook list --allow-unknown-hook-name -z test-hook >actual.raw &&
153 nul_to_q <actual.raw >actual &&
154 test_cmp expect actual
155 '
156
157 test_expect_success 'git hook run: nonexistent hook' '
158 cat >stderr.expect <<-\EOF &&
159 error: cannot find a hook named test-hook
160 EOF
161 test_expect_code 1 git hook run --allow-unknown-hook-name test-hook 2>stderr.actual &&
162 test_cmp stderr.expect stderr.actual
163 '
164
165 test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '
166 git hook run --allow-unknown-hook-name --ignore-missing does-not-exist 2>stderr.actual &&
167 test_must_be_empty stderr.actual
168 '
169
170 test_expect_success 'git hook run: basic' '
171 test_hook test-hook <<-EOF &&
172 echo Test hook
173 EOF
174
175 cat >expect <<-\EOF &&
176 Test hook
177 EOF
178 git hook run --allow-unknown-hook-name test-hook 2>actual &&
179 test_cmp expect actual
180 '
181
182 test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
183 test_hook test-hook <<-EOF &&
184 echo >&1 Will end up on stderr
185 echo >&2 Will end up on stderr
186 EOF
187
188 cat >stderr.expect <<-\EOF &&
189 Will end up on stderr
190 Will end up on stderr
191 EOF
192 git hook run --allow-unknown-hook-name test-hook >stdout.actual 2>stderr.actual &&
193 test_cmp stderr.expect stderr.actual &&
194 test_must_be_empty stdout.actual
195 '
196
197 for code in 1 2 128 129
198 do
199 test_expect_success "git hook run: exit code $code is passed along" '
200 test_hook test-hook <<-EOF &&
201 exit $code
202 EOF
203
204 test_expect_code $code git hook run --allow-unknown-hook-name test-hook
205 '
206 done
207
208 test_expect_success 'git hook run arg u ments without -- is not allowed' '
209 test_expect_code 129 git hook run --allow-unknown-hook-name test-hook arg u ments
210 '
211
212 test_expect_success 'git hook run -- pass arguments' '
213 test_hook test-hook <<-\EOF &&
214 echo $1
215 echo $2
216 EOF
217
218 cat >expect <<-EOF &&
219 arg
220 u ments
221 EOF
222
223 git hook run --allow-unknown-hook-name test-hook -- arg "u ments" 2>actual &&
224 test_cmp expect actual
225 '
226
227 test_expect_success 'git hook run: out-of-repo runs execute global hooks' '
228 test_config_global hook.global-hook.event test-hook --add &&
229 test_config_global hook.global-hook.command "echo no repo no problems" --add &&
230
231 echo "global-hook" >expect &&
232 nongit git hook list --allow-unknown-hook-name test-hook >actual &&
233 test_cmp expect actual &&
234
235 echo "no repo no problems" >expect &&
236
237 nongit git hook run --allow-unknown-hook-name test-hook 2>actual &&
238 test_cmp expect actual
239 '
240
241 test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
242 mkdir my-hooks &&
243 write_script my-hooks/test-hook <<-\EOF &&
244 echo Hook ran $1
245 EOF
246
247 cat >expect <<-\EOF &&
248 Test hook
249 Hook ran one
250 Hook ran two
251 Hook ran three
252 Hook ran four
253 EOF
254
255 test_hook test-hook <<-EOF &&
256 echo Test hook
257 EOF
258
259 # Test various ways of specifying the path. See also
260 # t1350-config-hooks-path.sh
261 >actual &&
262 git hook run --allow-unknown-hook-name test-hook -- ignored 2>>actual &&
263 git -c core.hooksPath=my-hooks hook run --allow-unknown-hook-name test-hook -- one 2>>actual &&
264 git -c core.hooksPath=my-hooks/ hook run --allow-unknown-hook-name test-hook -- two 2>>actual &&
265 git -c core.hooksPath="$PWD/my-hooks" hook run --allow-unknown-hook-name test-hook -- three 2>>actual &&
266 git -c core.hooksPath="$PWD/my-hooks/" hook run --allow-unknown-hook-name test-hook -- four 2>>actual &&
267 test_cmp expect actual
268 '
269
270 test_hook_tty () {
271 expect_tty=$1
272 shift
273
274 if test "$expect_tty" != "no_tty"; then
275 cat >expect <<-\EOF
276 STDOUT TTY
277 STDERR TTY
278 EOF
279 else
280 cat >expect <<-\EOF
281 STDOUT NO TTY
282 STDERR NO TTY
283 EOF
284 fi
285
286 test_when_finished "rm -rf repo" &&
287 git init repo &&
288
289 test_commit -C repo A &&
290 test_commit -C repo B &&
291 git -C repo reset --soft HEAD^ &&
292
293 test_hook -C repo pre-commit <<-EOF &&
294 test -t 1 && echo STDOUT TTY >>actual || echo STDOUT NO TTY >>actual &&
295 test -t 2 && echo STDERR TTY >>actual || echo STDERR NO TTY >>actual
296 EOF
297
298 test_terminal git -C repo "$@" &&
299 test_cmp expect repo/actual
300 }
301
302 test_expect_success TTY 'git hook run -j1: stdout and stderr are connected to a TTY' '
303 # hooks running sequentially (-j1) are always connected to the tty for
304 # optimum real-time performance.
305 test_hook_tty tty hook run -j1 pre-commit
306 '
307
308 test_expect_success TTY 'git hook run -jN: stdout and stderr are not connected to a TTY' '
309 # Hooks are not connected to the tty when run in parallel, instead they
310 # output to a pipe through which run-command collects and de-interlaces
311 # their outputs, which then gets passed either to the tty or a sideband.
312 test_hook_tty no_tty hook run -j2 pre-commit
313 '
314
315 test_expect_success TTY 'git commit: stdout and stderr are connected to a TTY' '
316 test_hook_tty tty commit -m"B.new"
317 '
318
319 test_expect_success 'git hook list orders by config order' '
320 setup_hooks &&
321
322 cat >expected <<-\EOF &&
323 def
324 ghi
325 EOF
326
327 git hook list pre-commit >actual &&
328 test_cmp expected actual
329 '
330
331 test_expect_success 'git hook list reorders on duplicate event declarations' '
332 setup_hooks &&
333
334 # 'def' is usually configured globally; move it to the end by
335 # configuring it locally.
336 test_config hook.def.event "pre-commit" --add &&
337
338 cat >expected <<-\EOF &&
339 ghi
340 def
341 EOF
342
343 git hook list pre-commit >actual &&
344 test_cmp expected actual
345 '
346
347 test_expect_success 'git hook list: empty event value resets events' '
348 setup_hooks &&
349
350 # ghi is configured for pre-commit; reset it with an empty value
351 test_config hook.ghi.event "" --add &&
352
353 # only def should remain for pre-commit
354 echo "def" >expected &&
355 git hook list pre-commit >actual &&
356 test_cmp expected actual
357 '
358
359 test_expect_success 'hook can be configured for multiple events' '
360 setup_hooks &&
361
362 # 'ghi' should be included in both 'pre-commit' and 'test-hook'
363 git hook list pre-commit >actual &&
364 grep "ghi" actual &&
365 git hook list --allow-unknown-hook-name test-hook >actual &&
366 grep "ghi" actual
367 '
368
369 test_expect_success 'git hook list shows hooks from the hookdir' '
370 setup_hookdir &&
371
372 cat >expected <<-\EOF &&
373 hook from hookdir
374 EOF
375
376 git hook list pre-commit >actual &&
377 test_cmp expected actual
378 '
379
380 test_expect_success 'inline hook definitions execute oneliners' '
381 test_config hook.oneliner.event "pre-commit" &&
382 test_config hook.oneliner.command "echo \"Hello World\"" &&
383
384 echo "Hello World" >expected &&
385
386 # hooks are run with stdout_to_stderr = 1
387 git hook run pre-commit 2>actual &&
388 test_cmp expected actual
389 '
390
391 test_expect_success 'inline hook definitions resolve paths' '
392 write_script sample-hook.sh <<-\EOF &&
393 echo \"Sample Hook\"
394 EOF
395
396 test_when_finished "rm sample-hook.sh" &&
397
398 test_config hook.sample-hook.event pre-commit &&
399 test_config hook.sample-hook.command "\"$(pwd)/sample-hook.sh\"" &&
400
401 echo \"Sample Hook\" >expected &&
402
403 # hooks are run with stdout_to_stderr = 1
404 git hook run pre-commit 2>actual &&
405 test_cmp expected actual
406 '
407
408 test_expect_success 'hookdir hook included in git hook run' '
409 setup_hookdir &&
410
411 echo \"Legacy Hook\" >expected &&
412
413 # hooks are run with stdout_to_stderr = 1
414 git hook run pre-commit 2>actual &&
415 test_cmp expected actual
416 '
417
418 test_expect_success 'stdin to multiple hooks' '
419 test_config hook.stdin-a.event "test-hook" &&
420 test_config hook.stdin-a.command "xargs -P1 -I% echo a%" &&
421 test_config hook.stdin-b.event "test-hook" &&
422 test_config hook.stdin-b.command "xargs -P1 -I% echo b%" &&
423
424 cat >input <<-\EOF &&
425 1
426 2
427 3
428 EOF
429
430 cat >expected <<-\EOF &&
431 a1
432 a2
433 a3
434 b1
435 b2
436 b3
437 EOF
438
439 git hook run --allow-unknown-hook-name --to-stdin=input test-hook 2>actual &&
440 test_cmp expected actual
441 '
442
443 test_expect_success 'rejects hooks with no commands configured' '
444 test_config hook.broken.event "test-hook" &&
445 test_must_fail git hook list --allow-unknown-hook-name test-hook 2>actual &&
446 test_grep "hook.broken.command" actual &&
447 test_must_fail git hook run --allow-unknown-hook-name test-hook 2>actual &&
448 test_grep "hook.broken.command" actual
449 '
450
451 test_expect_success 'disabled hook is not run' '
452 test_config hook.skipped.event "test-hook" &&
453 test_config hook.skipped.command "echo \"Should not run\"" &&
454 test_config hook.skipped.enabled false &&
455
456 git hook run --allow-unknown-hook-name --ignore-missing test-hook 2>actual &&
457 test_must_be_empty actual
458 '
459
460 test_expect_success 'disabled hook with no command warns' '
461 test_config hook.nocommand.event "pre-commit" &&
462 test_config hook.nocommand.enabled false &&
463
464 git hook list pre-commit 2>actual &&
465 test_grep "disabled hook.*nocommand.*no command configured" actual
466 '
467
468 test_expect_success 'disabled hook appears as disabled in git hook list' '
469 test_config hook.active.event "pre-commit" &&
470 test_config hook.active.command "echo active" &&
471 test_config hook.inactive.event "pre-commit" &&
472 test_config hook.inactive.command "echo inactive" &&
473 test_config hook.inactive.enabled false &&
474
475 git hook list pre-commit >actual &&
476 test_grep "^active$" actual &&
477 test_grep "^disabled inactive$" actual
478 '
479
480 test_expect_success 'disabled hook shows scope with --show-scope' '
481 test_config hook.myhook.event "pre-commit" &&
482 test_config hook.myhook.command "echo hi" &&
483 test_config hook.myhook.enabled false &&
484
485 git hook list --show-scope pre-commit >actual &&
486 test_grep "^local disabled myhook$" actual
487 '
488
489 test_expect_success 'disabled configured hook is not reported as existing by hook_exists' '
490 test_when_finished "rm -f git-bugreport-hook-exists-test.txt" &&
491 test_config hook.linter.event "pre-commit" &&
492 test_config hook.linter.command "echo lint" &&
493 test_config hook.linter.enabled false &&
494
495 git bugreport -s hook-exists-test &&
496 test_grep ! "pre-commit" git-bugreport-hook-exists-test.txt
497 '
498
499 test_expect_success 'globally disabled hook can be re-enabled locally' '
500 test_config_global hook.global-hook.event "test-hook" &&
501 test_config_global hook.global-hook.command "echo \"global-hook ran\"" &&
502 test_config_global hook.global-hook.enabled false &&
503 test_config hook.global-hook.enabled true &&
504
505 echo "global-hook ran" >expected &&
506 git hook run --allow-unknown-hook-name test-hook 2>actual &&
507 test_cmp expected actual
508 '
509
510 test_expect_success 'configured hooks run before hookdir hook' '
511 setup_hookdir &&
512 test_config hook.first.event "pre-commit" &&
513 test_config hook.first.command "echo first" &&
514 test_config hook.second.event "pre-commit" &&
515 test_config hook.second.command "echo second" &&
516
517 cat >expected <<-\EOF &&
518 first
519 second
520 hook from hookdir
521 EOF
522
523 git hook list pre-commit >actual &&
524 test_cmp expected actual &&
525
526 # "Legacy Hook" is the output of the hookdir pre-commit script
527 # written by setup_hookdir() above.
528 cat >expected <<-\EOF &&
529 first
530 second
531 "Legacy Hook"
532 EOF
533
534 git hook run pre-commit 2>actual &&
535 test_cmp expected actual
536 '
537
538 test_expect_success 'git hook list --show-scope shows config scope' '
539 setup_hookdir &&
540 test_config_global hook.global-hook.command "echo global" &&
541 test_config_global hook.global-hook.event pre-commit --add &&
542 test_config hook.local-hook.command "echo local" &&
543 test_config hook.local-hook.event pre-commit --add &&
544
545 cat >expected <<-\EOF &&
546 global global-hook
547 local local-hook
548 hook from hookdir
549 EOF
550 git hook list --show-scope pre-commit >actual &&
551 test_cmp expected actual &&
552
553 # without --show-scope the scope must not appear
554 git hook list pre-commit >actual &&
555 test_grep ! "^global " actual &&
556 test_grep ! "^local " actual
557 '
558
559 test_expect_success 'git hook run a hook with a bad shebang' '
560 test_when_finished "rm -rf bad-hooks" &&
561 mkdir bad-hooks &&
562 write_script bad-hooks/test-hook "/bad/path/no/spaces" </dev/null &&
563
564 test_expect_code 1 git \
565 -c core.hooksPath=bad-hooks \
566 hook run --allow-unknown-hook-name test-hook >out 2>err &&
567 test_must_be_empty out &&
568
569 # TODO: We should emit the same (or at least a more similar)
570 # error on MINGW (essentially Git for Windows) and all other
571 # platforms.. See the OS-specific code in start_command()
572 grep -E "^(error|fatal): cannot (exec|spawn) .*bad-hooks/test-hook" err
573 '
574
575 test_expect_success 'stdin to hooks' '
576 mkdir -p .git/hooks &&
577 write_script .git/hooks/test-hook <<-\EOF &&
578 echo BEGIN stdin
579 cat
580 echo END stdin
581 EOF
582
583 cat >expect <<-EOF &&
584 BEGIN stdin
585 hello
586 END stdin
587 EOF
588
589 echo hello >input &&
590 git hook run --allow-unknown-hook-name --to-stdin=input test-hook 2>actual &&
591 test_cmp expect actual
592 '
593
594 check_stdout_separate_from_stderr () {
595 for hook in "$@"
596 do
597 # Ensure hook's stdout is only in stdout, not stderr
598 test_grep "Hook $hook stdout" stdout.actual || return 1
599 test_grep ! "Hook $hook stdout" stderr.actual || return 1
600
601 # Ensure hook's stderr is only in stderr, not stdout
602 test_grep "Hook $hook stderr" stderr.actual || return 1
603 test_grep ! "Hook $hook stderr" stdout.actual || return 1
604 done
605 }
606
607 check_stdout_merged_to_stderr () {
608 for hook in "$@"
609 do
610 # Ensure hook's stdout is only in stderr, not stdout
611 test_grep "Hook $hook stdout" stderr.actual || return 1
612 test_grep ! "Hook $hook stdout" stdout.actual || return 1
613
614 # Ensure hook's stderr is only in stderr, not stdout
615 test_grep "Hook $hook stderr" stderr.actual || return 1
616 test_grep ! "Hook $hook stderr" stdout.actual || return 1
617 done
618 }
619
620 setup_hooks () {
621 for hook in "$@"
622 do
623 test_hook $hook <<-EOF
624 echo >&1 Hook $hook stdout
625 echo >&2 Hook $hook stderr
626 EOF
627 done
628 }
629
630 test_expect_success 'client hooks: pre-push expects separate stdout and stderr' '
631 test_when_finished "rm -f stdout.actual stderr.actual" &&
632 git init --bare remote &&
633 git remote add origin remote &&
634 test_commit A &&
635 setup_hooks pre-push &&
636 git push origin HEAD:main >stdout.actual 2>stderr.actual &&
637 check_stdout_separate_from_stderr pre-push
638 '
639
640 test_expect_success 'client hooks: commit hooks expect stdout redirected to stderr' '
641 hooks="pre-commit prepare-commit-msg \
642 commit-msg post-commit \
643 reference-transaction" &&
644 setup_hooks $hooks &&
645 test_when_finished "rm -f stdout.actual stderr.actual" &&
646 git checkout -B main &&
647 git checkout -b branch-a &&
648 test_commit commit-on-branch-a &&
649 git commit --allow-empty -m "Test" >stdout.actual 2>stderr.actual &&
650 check_stdout_merged_to_stderr $hooks
651 '
652
653 test_expect_success 'client hooks: checkout hooks expect stdout redirected to stderr' '
654 setup_hooks post-checkout reference-transaction &&
655 test_when_finished "rm -f stdout.actual stderr.actual" &&
656 git checkout -b new-branch main >stdout.actual 2>stderr.actual &&
657 check_stdout_merged_to_stderr post-checkout reference-transaction
658 '
659
660 test_expect_success 'client hooks: merge hooks expect stdout redirected to stderr' '
661 setup_hooks pre-merge-commit post-merge reference-transaction &&
662 test_when_finished "rm -f stdout.actual stderr.actual" &&
663 test_commit new-branch-commit &&
664 git merge --no-ff branch-a >stdout.actual 2>stderr.actual &&
665 check_stdout_merged_to_stderr pre-merge-commit post-merge reference-transaction
666 '
667
668 test_expect_success 'client hooks: post-rewrite hooks expect stdout redirected to stderr' '
669 setup_hooks post-rewrite reference-transaction &&
670 test_when_finished "rm -f stdout.actual stderr.actual" &&
671 git commit --amend --allow-empty --no-edit >stdout.actual 2>stderr.actual &&
672 check_stdout_merged_to_stderr post-rewrite reference-transaction
673 '
674
675 test_expect_success 'client hooks: applypatch hooks expect stdout redirected to stderr' '
676 setup_hooks applypatch-msg pre-applypatch post-applypatch &&
677 test_when_finished "rm -f stdout.actual stderr.actual" &&
678 git checkout -b branch-b main &&
679 test_commit branch-b &&
680 git format-patch -1 --stdout >patch &&
681 git checkout -b branch-c main &&
682 git am patch >stdout.actual 2>stderr.actual &&
683 check_stdout_merged_to_stderr applypatch-msg pre-applypatch post-applypatch
684 '
685
686 test_expect_success 'client hooks: rebase hooks expect stdout redirected to stderr' '
687 setup_hooks pre-rebase &&
688 test_when_finished "rm -f stdout.actual stderr.actual" &&
689 git checkout -b branch-d main &&
690 test_commit branch-d &&
691 git checkout main &&
692 test_commit diverge-main &&
693 git checkout branch-d &&
694 git rebase main >stdout.actual 2>stderr.actual &&
695 check_stdout_merged_to_stderr pre-rebase
696 '
697
698 test_expect_success 'client hooks: post-index-change expects stdout redirected to stderr' '
699 setup_hooks post-index-change &&
700 test_when_finished "rm -f stdout.actual stderr.actual" &&
701 oid=$(git hash-object -w --stdin </dev/null) &&
702 git update-index --add --cacheinfo 100644 $oid new-file \
703 >stdout.actual 2>stderr.actual &&
704 check_stdout_merged_to_stderr post-index-change
705 '
706
707 test_expect_success 'server hooks expect stdout redirected to stderr' '
708 test_when_finished "rm -f stdout.actual stderr.actual" &&
709 git init --bare remote-server &&
710 git remote add origin-server remote-server &&
711 cd remote-server &&
712 setup_hooks pre-receive update post-receive post-update &&
713 cd .. &&
714 git push origin-server HEAD:new-branch >stdout.actual 2>stderr.actual &&
715 check_stdout_merged_to_stderr pre-receive update post-receive post-update
716 '
717
718 test_expect_success 'server push-to-checkout hook expects stdout redirected to stderr' '
719 test_when_finished "rm -f stdout.actual stderr.actual" &&
720 git init server &&
721 git -C server checkout -b main &&
722 test_config -C server receive.denyCurrentBranch updateInstead &&
723 git remote add origin-server-2 server &&
724 cd server &&
725 setup_hooks push-to-checkout &&
726 cd .. &&
727 git push origin-server-2 HEAD:main >stdout.actual 2>stderr.actual &&
728 check_stdout_merged_to_stderr push-to-checkout
729 '
730
731 test_expect_success 'parallel hook output is not interleaved' '
732 test_when_finished "rm -rf .git/hooks" &&
733
734 write_script .git/hooks/test-hook <<-EOF &&
735 echo "Hook 1 Start"
736 sleep 1
737 echo "Hook 1 End"
738 EOF
739
740 test_config hook.hook-2.event test-hook &&
741 test_config hook.hook-2.command \
742 "echo \"Hook 2 Start\"; sleep 2; echo \"Hook 2 End\"" &&
743 test_config hook.hook-2.parallel true &&
744 test_config hook.hook-3.event test-hook &&
745 test_config hook.hook-3.command \
746 "echo \"Hook 3 Start\"; sleep 3; echo \"Hook 3 End\"" &&
747 test_config hook.hook-3.parallel true &&
748
749 git hook run --allow-unknown-hook-name -j3 test-hook >out 2>err.parallel &&
750
751 # Verify Hook 1 output is grouped
752 sed -n "/Hook 1 Start/,/Hook 1 End/p" err.parallel >hook1_out &&
753 test_line_count = 2 hook1_out &&
754
755 # Verify Hook 2 output is grouped
756 sed -n "/Hook 2 Start/,/Hook 2 End/p" err.parallel >hook2_out &&
757 test_line_count = 2 hook2_out &&
758
759 # Verify Hook 3 output is grouped
760 sed -n "/Hook 3 Start/,/Hook 3 End/p" err.parallel >hook3_out &&
761 test_line_count = 2 hook3_out
762 '
763
764 test_expect_success 'git hook run -j1 runs hooks in series' '
765 test_when_finished "rm -rf .git/hooks" &&
766
767 test_config hook.series-1.event "test-hook" &&
768 test_config hook.series-1.command "echo 1" --add &&
769 test_config hook.series-2.event "test-hook" &&
770 test_config hook.series-2.command "echo 2" --add &&
771
772 mkdir -p .git/hooks &&
773 write_script .git/hooks/test-hook <<-EOF &&
774 echo 3
775 EOF
776
777 cat >expected <<-\EOF &&
778 1
779 2
780 3
781 EOF
782
783 git hook run --allow-unknown-hook-name -j1 test-hook 2>actual &&
784 test_cmp expected actual
785 '
786
787 test_expect_success 'git hook run -j2 runs hooks in parallel' '
788 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
789 test_when_finished "rm -rf .git/hooks" &&
790
791 mkdir -p .git/hooks &&
792 write_sentinel_hook .git/hooks/test-hook &&
793
794 test_config hook.hook-2.event test-hook &&
795 test_config hook.hook-2.command \
796 "$(sentinel_detector sentinel hook.order)" &&
797 test_config hook.hook-2.parallel true &&
798
799 git hook run --allow-unknown-hook-name -j2 test-hook >out 2>err &&
800 echo parallel >expect &&
801 test_cmp expect hook.order
802 '
803
804 test_expect_success 'git hook run -j2 overrides parallel=false' '
805 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
806 test_config hook.hook-1.event test-hook &&
807 test_config hook.hook-1.command \
808 "touch sentinel.started; sleep 2; touch sentinel.done" &&
809 # hook-1 intentionally has no parallel=true
810 test_config hook.hook-2.event test-hook &&
811 test_config hook.hook-2.command \
812 "$(sentinel_detector sentinel hook.order)" &&
813 # hook-2 also has no parallel=true
814
815 # -j2 overrides parallel=false; hooks run in parallel with a warning.
816 git hook run --allow-unknown-hook-name -j2 test-hook >out 2>err &&
817 echo parallel >expect &&
818 test_cmp expect hook.order
819 '
820
821 test_expect_success 'git hook run -j2 warns for hooks not marked parallel=true' '
822 test_config hook.hook-1.event test-hook &&
823 test_config hook.hook-1.command "true" &&
824 test_config hook.hook-2.event test-hook &&
825 test_config hook.hook-2.command "true" &&
826 # neither hook has parallel=true
827
828 git hook run --allow-unknown-hook-name -j2 test-hook >out 2>err &&
829 grep "hook .hook-1. is not marked as parallel=true" err &&
830 grep "hook .hook-2. is not marked as parallel=true" err
831 '
832
833 test_expect_success 'hook.jobs=1 config runs hooks in series' '
834 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
835
836 # Use two configured hooks so the execution order is deterministic:
837 # hook-1 (sentinel) is listed before hook-2 (detector), so hook-1
838 # always runs first even in serial mode.
839 test_config hook.hook-1.event test-hook &&
840 test_config hook.hook-1.command \
841 "touch sentinel.started; sleep 2; touch sentinel.done" &&
842 test_config hook.hook-2.event test-hook &&
843 test_config hook.hook-2.command \
844 "$(sentinel_detector sentinel hook.order)" &&
845
846 test_config hook.jobs 1 &&
847
848 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
849 echo serial >expect &&
850 test_cmp expect hook.order
851 '
852
853 test_expect_success 'hook.jobs=2 config runs hooks in parallel' '
854 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
855 test_when_finished "rm -rf .git/hooks" &&
856
857 mkdir -p .git/hooks &&
858 write_sentinel_hook .git/hooks/test-hook &&
859
860 test_config hook.hook-2.event test-hook &&
861 test_config hook.hook-2.command \
862 "$(sentinel_detector sentinel hook.order)" &&
863 test_config hook.hook-2.parallel true &&
864
865 test_config hook.jobs 2 &&
866
867 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
868 echo parallel >expect &&
869 test_cmp expect hook.order
870 '
871
872 test_expect_success 'hook.<name>.parallel=true enables parallel execution' '
873 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
874 test_config hook.hook-1.event test-hook &&
875 test_config hook.hook-1.command \
876 "touch sentinel.started; sleep 2; touch sentinel.done" &&
877 test_config hook.hook-1.parallel true &&
878 test_config hook.hook-2.event test-hook &&
879 test_config hook.hook-2.command \
880 "$(sentinel_detector sentinel hook.order)" &&
881 test_config hook.hook-2.parallel true &&
882
883 test_config hook.jobs 2 &&
884
885 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
886 echo parallel >expect &&
887 test_cmp expect hook.order
888 '
889
890 test_expect_success 'hook.<name>.parallel=false (default) forces serial execution' '
891 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
892 test_config hook.hook-1.event test-hook &&
893 test_config hook.hook-1.command \
894 "touch sentinel.started; sleep 2; touch sentinel.done" &&
895 test_config hook.hook-2.event test-hook &&
896 test_config hook.hook-2.command \
897 "$(sentinel_detector sentinel hook.order)" &&
898
899 test_config hook.jobs 2 &&
900
901 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
902 echo serial >expect &&
903 test_cmp expect hook.order
904 '
905
906 test_expect_success 'one non-parallel hook forces the whole event to run serially' '
907 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
908 test_config hook.hook-1.event test-hook &&
909 test_config hook.hook-1.command \
910 "touch sentinel.started; sleep 2; touch sentinel.done" &&
911 test_config hook.hook-1.parallel true &&
912 test_config hook.hook-2.event test-hook &&
913 test_config hook.hook-2.command \
914 "$(sentinel_detector sentinel hook.order)" &&
915 # hook-2 has no parallel=true: should force serial for all
916
917 test_config hook.jobs 2 &&
918
919 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
920 echo serial >expect &&
921 test_cmp expect hook.order
922 '
923
924 test_expect_success 'client hooks: pre-push parallel execution merges stdout to stderr' '
925 test_when_finished "rm -rf remote-par stdout.actual stderr.actual" &&
926 git init --bare remote-par &&
927 git remote add origin-par remote-par &&
928 test_commit par-commit &&
929 mkdir -p .git/hooks &&
930 setup_hooks pre-push &&
931 test_config hook.jobs 2 &&
932 git push origin-par HEAD:main >stdout.actual 2>stderr.actual &&
933 check_stdout_merged_to_stderr pre-push
934 '
935
936 test_expect_success 'client hooks: pre-push runs in parallel when hook.jobs > 1' '
937 test_when_finished "rm -rf repo-parallel remote-parallel" &&
938 git init --bare remote-parallel &&
939 git init repo-parallel &&
940 git -C repo-parallel remote add origin ../remote-parallel &&
941 test_commit -C repo-parallel A &&
942
943 write_sentinel_hook repo-parallel/.git/hooks/pre-push &&
944 git -C repo-parallel config hook.hook-2.event pre-push &&
945 git -C repo-parallel config hook.hook-2.command \
946 "$(sentinel_detector sentinel hook.order)" &&
947 git -C repo-parallel config hook.hook-2.parallel true &&
948
949 git -C repo-parallel config hook.jobs 2 &&
950
951 git -C repo-parallel push origin HEAD >out 2>err &&
952 echo parallel >expect &&
953 test_cmp expect repo-parallel/hook.order
954 '
955
956 test_expect_success 'hook.jobs=2 is ignored for force-serial hooks (pre-commit)' '
957 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
958 test_config hook.hook-1.event pre-commit &&
959 test_config hook.hook-1.command \
960 "touch sentinel.started; sleep 2; touch sentinel.done" &&
961 test_config hook.hook-1.parallel true &&
962 test_config hook.hook-2.event pre-commit &&
963 test_config hook.hook-2.command \
964 "$(sentinel_detector sentinel hook.order)" &&
965 test_config hook.hook-2.parallel true &&
966 test_config hook.jobs 2 &&
967 git commit --allow-empty -m "test: verify force-serial on pre-commit" &&
968 echo serial >expect &&
969 test_cmp expect hook.order
970 '
971
972 test_expect_success 'hook.<event>.jobs overrides hook.jobs for that event' '
973 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
974 test_config hook.hook-1.event test-hook &&
975 test_config hook.hook-1.command \
976 "touch sentinel.started; sleep 2; touch sentinel.done" &&
977 test_config hook.hook-1.parallel true &&
978 test_config hook.hook-2.event test-hook &&
979 test_config hook.hook-2.command \
980 "$(sentinel_detector sentinel hook.order)" &&
981 test_config hook.hook-2.parallel true &&
982
983 # Global hook.jobs=1 (serial), but per-event override allows parallel.
984 test_config hook.jobs 1 &&
985 test_config hook.test-hook.jobs 2 &&
986
987 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
988 echo parallel >expect &&
989 test_cmp expect hook.order
990 '
991
992 test_expect_success 'hook.<event>.jobs=1 forces serial even when hook.jobs>1' '
993 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
994 test_config hook.hook-1.event test-hook &&
995 test_config hook.hook-1.command \
996 "touch sentinel.started; sleep 2; touch sentinel.done" &&
997 test_config hook.hook-1.parallel true &&
998 test_config hook.hook-2.event test-hook &&
999 test_config hook.hook-2.command \
1000 "$(sentinel_detector sentinel hook.order)" &&
1001 test_config hook.hook-2.parallel true &&
1002
1003 # Global hook.jobs=4 allows parallel, but per-event override forces serial.
1004 test_config hook.jobs 4 &&
1005 test_config hook.test-hook.jobs 1 &&
1006
1007 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1008 echo serial >expect &&
1009 test_cmp expect hook.order
1010 '
1011
1012 test_expect_success 'hook.<event>.jobs still requires hook.<name>.parallel=true' '
1013 test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
1014 test_config hook.hook-1.event test-hook &&
1015 test_config hook.hook-1.command \
1016 "touch sentinel.started; sleep 2; touch sentinel.done" &&
1017 # hook-1 intentionally has no parallel=true
1018 test_config hook.hook-2.event test-hook &&
1019 test_config hook.hook-2.command \
1020 "$(sentinel_detector sentinel hook.order)" &&
1021 # hook-2 also has no parallel=true
1022
1023 # Per-event jobs=2 but no hook has parallel=true: must still run serially.
1024 test_config hook.test-hook.jobs 2 &&
1025
1026 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1027 echo serial >expect &&
1028 test_cmp expect hook.order
1029 '
1030
1031 test_expect_success 'hook.<friendly-name>.jobs warns when name has .command' '
1032 test_config hook.my-hook.command "true" &&
1033 test_config hook.my-hook.jobs 2 &&
1034 git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err &&
1035 test_grep "hook.my-hook.jobs.*friendly-name" err
1036 '
1037
1038 test_expect_success 'hook.<friendly-name>.jobs warns when name has .event' '
1039 test_config hook.my-hook.event test-hook &&
1040 test_config hook.my-hook.command "true" &&
1041 test_config hook.my-hook.jobs 2 &&
1042 git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err &&
1043 test_grep "hook.my-hook.jobs.*friendly-name" err
1044 '
1045
1046 test_expect_success 'hook.<friendly-name>.jobs warns when name has .parallel' '
1047 test_config hook.my-hook.event test-hook &&
1048 test_config hook.my-hook.command "true" &&
1049 test_config hook.my-hook.parallel true &&
1050 test_config hook.my-hook.jobs 2 &&
1051 git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err &&
1052 test_grep "hook.my-hook.jobs.*friendly-name" err
1053 '
1054
1055 test_expect_success 'hook.<event>.jobs does not warn for a real event name' '
1056 test_config hook.test-hook.jobs 2 &&
1057 git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err &&
1058 test_grep ! "friendly-name" err
1059 '
1060
1061 test_expect_success 'hook.jobs=-1 resolves to online_cpus()' '
1062 test_config hook.hook-1.event test-hook &&
1063 test_config hook.hook-1.command "true" &&
1064 test_config hook.hook-1.parallel true &&
1065
1066 test_config hook.jobs -1 &&
1067
1068 cpus=$(test-tool online-cpus) &&
1069 GIT_TRACE2_EVENT="$(pwd)/trace.txt" \
1070 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1071 grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
1072 '
1073
1074 test_expect_success 'hook.<event>.jobs=-1 resolves to online_cpus()' '
1075 test_config hook.hook-1.event test-hook &&
1076 test_config hook.hook-1.command "true" &&
1077 test_config hook.hook-1.parallel true &&
1078
1079 test_config hook.test-hook.jobs -1 &&
1080
1081 cpus=$(test-tool online-cpus) &&
1082 GIT_TRACE2_EVENT="$(pwd)/trace.txt" \
1083 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1084 grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
1085 '
1086
1087 test_expect_success 'git hook run -j-1 resolves to online_cpus()' '
1088 test_config hook.hook-1.event test-hook &&
1089 test_config hook.hook-1.command "true" &&
1090 test_config hook.hook-1.parallel true &&
1091
1092 cpus=$(test-tool online-cpus) &&
1093 GIT_TRACE2_EVENT="$(pwd)/trace.txt" \
1094 git hook run --allow-unknown-hook-name -j-1 test-hook >out 2>err &&
1095 grep "\"region_enter\".*\"hook\".*\"test-hook\".*\"max:$cpus\"" trace.txt
1096 '
1097
1098 test_expect_success 'hook.jobs rejects values less than -1' '
1099 test_config hook.jobs -2 &&
1100 git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err &&
1101 test_grep "hook.jobs must be a positive integer or -1" err
1102 '
1103
1104 test_expect_success 'hook.<event>.jobs rejects values less than -1' '
1105 test_config hook.test-hook.jobs -5 &&
1106 git hook run --allow-unknown-hook-name --ignore-missing test-hook >out 2>err &&
1107 test_grep "hook.test-hook.jobs must be a positive integer or -1" err
1108 '
1109
1110 test_expect_success 'hook.<event>.enabled=false skips all hooks for event' '
1111 test_config hook.hook-1.event test-hook &&
1112 test_config hook.hook-1.command "echo ran" &&
1113 test_config hook.test-hook.enabled false &&
1114 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1115 test_must_be_empty out
1116 '
1117
1118 test_expect_success 'hook.<event>.enabled=true does not suppress hooks' '
1119 test_config hook.hook-1.event test-hook &&
1120 test_config hook.hook-1.command "echo ran" &&
1121 test_config hook.test-hook.enabled true &&
1122 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1123 test_grep "ran" err
1124 '
1125
1126 test_expect_success 'hook.<event>.enabled=false does not affect other events' '
1127 test_config hook.hook-1.event test-hook &&
1128 test_config hook.hook-1.command "echo ran" &&
1129 test_config hook.other-event.enabled false &&
1130 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1131 test_grep "ran" err
1132 '
1133
1134 test_expect_success 'hook.<friendly-name>.enabled=false still disables that hook' '
1135 test_config hook.hook-1.event test-hook &&
1136 test_config hook.hook-1.command "echo hook-1" &&
1137 test_config hook.hook-2.event test-hook &&
1138 test_config hook.hook-2.command "echo hook-2" &&
1139 test_config hook.hook-1.enabled false &&
1140 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1141 test_grep ! "hook-1" err &&
1142 test_grep "hook-2" err
1143 '
1144
1145 test_expect_success 'git hook list shows event-disabled hooks as event-disabled' '
1146 test_config hook.hook-1.event test-hook &&
1147 test_config hook.hook-1.command "echo ran" &&
1148 test_config hook.hook-2.event test-hook &&
1149 test_config hook.hook-2.command "echo ran" &&
1150 test_config hook.test-hook.enabled false &&
1151 git hook list --allow-unknown-hook-name test-hook >actual &&
1152 test_grep "^event-disabled hook-1$" actual &&
1153 test_grep "^event-disabled hook-2$" actual
1154 '
1155
1156 test_expect_success 'git hook list shows scope with event-disabled' '
1157 test_config hook.hook-1.event test-hook &&
1158 test_config hook.hook-1.command "echo ran" &&
1159 test_config hook.test-hook.enabled false &&
1160 git hook list --allow-unknown-hook-name --show-scope test-hook >actual &&
1161 test_grep "^local event-disabled hook-1$" actual
1162 '
1163
1164 test_expect_success 'git hook list still shows hooks when event is disabled' '
1165 test_config hook.hook-1.event test-hook &&
1166 test_config hook.hook-1.command "echo ran" &&
1167 test_config hook.test-hook.enabled false &&
1168 git hook list --allow-unknown-hook-name test-hook >actual &&
1169 test_grep "event-disabled" actual
1170 '
1171
1172 test_expect_success 'friendly-name matching known event name is rejected' '
1173 test_config hook.pre-commit.event pre-commit &&
1174 test_config hook.pre-commit.command "echo oops" &&
1175 test_must_fail git hook run pre-commit 2>err &&
1176 test_grep "collides with a known event name" err
1177 '
1178
1179 test_expect_success 'friendly-name matching known event name is rejected even for different event' '
1180 test_config hook.pre-commit.event post-commit &&
1181 test_config hook.pre-commit.command "echo oops" &&
1182 test_must_fail git hook run post-commit 2>err &&
1183 test_grep "collides with a known event name" err
1184 '
1185
1186 test_expect_success 'friendly-name matching unknown event warns' '
1187 test_config hook.test-hook.event test-hook &&
1188 test_config hook.test-hook.command "echo ran" &&
1189 git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1190 test_grep "same as its event" err
1191 '
1192
1193 test_expect_success 'hooks in parallel that do not read input' '
1194 # Add this to our $PATH to avoid having to write the whole trash
1195 # directory into our config options, which would require quoting.
1196 mkdir bin &&
1197 PATH=$PWD/bin:$PATH &&
1198
1199 write_script bin/hook-fast <<-\EOF &&
1200 # This hook does not read its input, so the parent process
1201 # may see SIGPIPE if it is not ignored. It should happen
1202 # relatively quickly.
1203 exit 0
1204 EOF
1205
1206 write_script bin/hook-slow <<-\EOF &&
1207 # This hook is slow, so we expect it to still be running
1208 # when the other hook has exited (and the parent has a pipe error
1209 # writing to it).
1210 #
1211 # So we want to be slow enough that we expect this to happen, but not
1212 # so slow that the test takes forever. 1 second is probably enough
1213 # in practice (and if it is occasionally not on a loaded system, we
1214 # will err on the side of having the test pass).
1215 sleep 1
1216 exit 0
1217 EOF
1218
1219 git init --bare parallel.git &&
1220 git -C parallel.git config hook.fast.command "hook-fast" &&
1221 git -C parallel.git config hook.fast.event pre-receive &&
1222 git -C parallel.git config hook.fast.parallel true &&
1223 git -C parallel.git config hook.slow.command "hook-slow" &&
1224 git -C parallel.git config hook.slow.event pre-receive &&
1225 git -C parallel.git config hook.slow.parallel true &&
1226 git -C parallel.git config hook.jobs 2 &&
1227
1228 git push ./parallel.git "+refs/heads/*:refs/heads/*"
1229 '
1230
1231 test_done