Skip to content

Commit 091d2db

Browse files
nasamuffingitster
authored andcommitted
hook: add -j/--jobs option to git hook run
Expose the parallel job count as a command-line flag so callers can request parallelism without relying only on the hook.jobs config. Add tests covering serial/parallel execution and TTY behaviour under -j1 vs -jN. Signed-off-by: Emily Shaffer <emilyshaffer@google.com> Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent ae25764 commit 091d2db

4 files changed

Lines changed: 170 additions & 10 deletions

File tree

Documentation/git-hook.adoc

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ git-hook - Run git hooks
88
SYNOPSIS
99
--------
1010
[verse]
11-
'git hook' run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
11+
'git hook' run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] [(-j|--jobs) <n>]
12+
<hook-name> [-- <hook-args>]
1213
'git hook' list [--allow-unknown-hook-name] [-z] [--show-scope] <hook-name>
1314

1415
DESCRIPTION
@@ -147,6 +148,23 @@ OPTIONS
147148
mirroring the output style of `git config --show-scope`. Traditional
148149
hooks from the hookdir are unaffected.
149150

151+
-j::
152+
--jobs::
153+
Only valid for `run`.
154+
+
155+
Specify how many hooks to run simultaneously. If this flag is not specified,
156+
the value of the `hook.jobs` config is used, see linkgit:git-config[1]. If
157+
neither is specified, defaults to 1 (serial execution).
158+
+
159+
When greater than 1, it overrides the per-hook `hook.<friendly-name>.parallel`
160+
setting, allowing all hooks for the event to run concurrently, even if they
161+
are not individually marked as parallel.
162+
+
163+
Some hooks always run sequentially regardless of this flag or the
164+
`hook.jobs` config, because git knows they cannot safely run in parallel:
165+
`applypatch-msg`, `pre-commit`, `prepare-commit-msg`, `commit-msg`,
166+
`post-commit`, `post-checkout`, and `push-to-checkout`.
167+
150168
WRAPPERS
151169
--------
152170
@@ -169,7 +187,8 @@ running:
169187
git hook run --allow-unknown-hook-name mywrapper-start-tests \
170188
# providing something to stdin
171189
--stdin some-tempfile-123 \
172-
# execute hooks in serial
190+
# execute multiple hooks in parallel
191+
--jobs 3 \
173192
# plus some arguments of your own...
174193
-- \
175194
--testname bar \

builtin/hook.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
#include "parse-options.h"
99

1010
#define BUILTIN_HOOK_RUN_USAGE \
11-
N_("git hook run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
11+
N_("git hook run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] [(-j|--jobs) <n>]\n" \
12+
"<hook-name> [-- <hook-args>]")
1213
#define BUILTIN_HOOK_LIST_USAGE \
1314
N_("git hook list [--allow-unknown-hook-name] [-z] [--show-scope] <hook-name>")
1415

@@ -132,6 +133,8 @@ static int run(int argc, const char **argv, const char *prefix,
132133
N_("silently ignore missing requested <hook-name>")),
133134
OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"),
134135
N_("file to read into hooks' stdin")),
136+
OPT_UNSIGNED('j', "jobs", &opt.jobs,
137+
N_("run up to <n> hooks simultaneously")),
135138
OPT_END(),
136139
};
137140
int ret;

hook.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,22 @@ static void merge_output_if_parallel(struct run_hooks_opt *options)
568568
options->stdout_to_stderr = 1;
569569
}
570570

571+
static void warn_non_parallel_hooks_override(unsigned int jobs,
572+
struct string_list *hook_list)
573+
{
574+
/* Don't warn for hooks running sequentially. */
575+
if (jobs == 1)
576+
return;
577+
578+
for (size_t i = 0; i < hook_list->nr; i++) {
579+
struct hook *h = hook_list->items[i].util;
580+
if (h->kind == HOOK_CONFIGURED && !h->parallel)
581+
warning(_("hook '%s' is not marked as parallel=true, "
582+
"running in parallel anyway due to -j%u"),
583+
h->u.configured.friendly_name, jobs);
584+
}
585+
}
586+
571587
/* Determine how many jobs to use for hook execution. */
572588
static unsigned int get_hook_jobs(struct repository *r,
573589
struct run_hooks_opt *options,
@@ -611,6 +627,7 @@ static unsigned int get_hook_jobs(struct repository *r,
611627

612628
cleanup:
613629
merge_output_if_parallel(options);
630+
warn_non_parallel_hooks_override(options->jobs, hook_list);
614631
return options->jobs;
615632
}
616633

t/t1800-hook.sh

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,20 @@ test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
268268
'
269269

270270
test_hook_tty () {
271-
cat >expect <<-\EOF
272-
STDOUT TTY
273-
STDERR TTY
274-
EOF
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
275285

276286
test_when_finished "rm -rf repo" &&
277287
git init repo &&
@@ -289,12 +299,21 @@ test_hook_tty () {
289299
test_cmp expect repo/actual
290300
}
291301

292-
test_expect_success TTY 'git hook run: stdout and stderr are connected to a TTY' '
293-
test_hook_tty hook run pre-commit
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
294313
'
295314

296315
test_expect_success TTY 'git commit: stdout and stderr are connected to a TTY' '
297-
test_hook_tty commit -m"B.new"
316+
test_hook_tty tty commit -m"B.new"
298317
'
299318

300319
test_expect_success 'git hook list orders by config order' '
@@ -709,6 +728,108 @@ test_expect_success 'server push-to-checkout hook expects stdout redirected to s
709728
check_stdout_merged_to_stderr push-to-checkout
710729
'
711730

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+
712833
test_expect_success 'hook.jobs=1 config runs hooks in series' '
713834
test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
714835

0 commit comments

Comments
 (0)