git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v6 07/12] hook: add -j/--jobs option to git hook run

From
Adrian Ratiu <adrian.ratiu@collabora.com>
Date
Apr 4, 2026, 08:29 UTC
Message-ID
<20260404082934.173788-8-adrian.ratiu@collabora.com>
In-Reply-To
<20260404082934.173788-1-adrian.ratiu@collabora.com>
From: Emily Shaffer <emilyshaffer@google.com>

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>
---
 Documentation/git-hook.adoc |  23 +++++-
 builtin/hook.c              |   5 +-
 hook.c                      |  17 +++++
 t/t1800-hook.sh             | 135 ++++++++++++++++++++++++++++++++++--
 4 files changed, 170 insertions(+), 10 deletions(-)
diff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc
index 318c637bd8..46ea52db55 100644
--- a/Documentation/git-hook.adoc
+++ b/Documentation/git-hook.adoc
@@ -8,7 +8,8 @@ git-hook - Run git hooks
 SYNOPSIS
 --------
 [verse]
-'git hook' run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
+'git hook' run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] [(-j|--jobs) <n>]
+	<hook-name> [-- <hook-args>]
 'git hook' list [--allow-unknown-hook-name] [-z] [--show-scope] <hook-name>
 
 DESCRIPTION
@@ -147,6 +148,23 @@ OPTIONS
 	mirroring the output style of `git config --show-scope`. Traditional
 	hooks from the hookdir are unaffected.
 
+-j::
+--jobs::
+	Only valid for `run`.
++
+Specify how many hooks to run simultaneously. If this flag is not specified,
+the value of the `hook.jobs` config is used, see linkgit:git-config[1]. If
+neither is specified, defaults to 1 (serial execution).
++
+When greater than 1, it overrides the per-hook `hook.<friendly-name>.parallel`
+setting, allowing all hooks for the event to run concurrently, even if they
+are not individually marked as parallel.
++
+Some hooks always run sequentially regardless of this flag or the
+`hook.jobs` config, because git knows they cannot safely run in parallel:
+`applypatch-msg`, `pre-commit`, `prepare-commit-msg`, `commit-msg`,
+`post-commit`, `post-checkout`, and `push-to-checkout`.
+
 WRAPPERS
 --------
 
@@ -169,7 +187,8 @@ running:
 git hook run --allow-unknown-hook-name mywrapper-start-tests \
   # providing something to stdin
   --stdin some-tempfile-123 \
-  # execute hooks in serial
+  # execute multiple hooks in parallel
+  --jobs 3 \
   # plus some arguments of your own...
   -- \
   --testname bar \
diff --git a/builtin/hook.c b/builtin/hook.c
index c0585587e5..bea0668b47 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -8,7 +8,8 @@
 #include "parse-options.h"
 
 #define BUILTIN_HOOK_RUN_USAGE \
-	N_("git hook run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
+	N_("git hook run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] [(-j|--jobs) <n>]\n" \
+	   "<hook-name> [-- <hook-args>]")
 #define BUILTIN_HOOK_LIST_USAGE \
 	N_("git hook list [--allow-unknown-hook-name] [-z] [--show-scope] <hook-name>")
 
@@ -132,6 +133,8 @@ static int run(int argc, const char **argv, const char *prefix,
 			 N_("silently ignore missing requested <hook-name>")),
 		OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"),
 			   N_("file to read into hooks' stdin")),
+		OPT_UNSIGNED('j', "jobs", &opt.jobs,
+			    N_("run up to <n> hooks simultaneously")),
 		OPT_END(),
 	};
 	int ret;
diff --git a/hook.c b/hook.c
index 25762b6c8d..c0b71322cf 100644
--- a/hook.c
+++ b/hook.c
@@ -568,6 +568,22 @@ static void merge_output_if_parallel(struct run_hooks_opt *options)
 		options->stdout_to_stderr = 1;
 }
 
+static void warn_non_parallel_hooks_override(unsigned int jobs,
+					     struct string_list *hook_list)
+{
+	/* Don't warn for hooks running sequentially. */
+	if (jobs == 1)
+		return;
+
+	for (size_t i = 0; i < hook_list->nr; i++) {
+		struct hook *h = hook_list->items[i].util;
+		if (h->kind == HOOK_CONFIGURED && !h->parallel)
+			warning(_("hook '%s' is not marked as parallel=true, "
+				  "running in parallel anyway due to -j%u"),
+				h->u.configured.friendly_name, jobs);
+	}
+}
+
 /* Determine how many jobs to use for hook execution. */
 static unsigned int get_hook_jobs(struct repository *r,
 				  struct run_hooks_opt *options,
@@ -611,6 +627,7 @@ static unsigned int get_hook_jobs(struct repository *r,
 
 cleanup:
 	merge_output_if_parallel(options);
+	warn_non_parallel_hooks_override(options->jobs, hook_list);
 	return options->jobs;
 }
 
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 6e8b1ad588..a1734fd628 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -268,10 +268,20 @@ test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
 '
 
 test_hook_tty () {
-	cat >expect <<-\EOF
-	STDOUT TTY
-	STDERR TTY
-	EOF
+	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 &&
@@ -289,12 +299,21 @@ test_hook_tty () {
 	test_cmp expect repo/actual
 }
 
-test_expect_success TTY 'git hook run: stdout and stderr are connected to a TTY' '
-	test_hook_tty hook run pre-commit
+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 commit -m"B.new"
+	test_hook_tty tty commit -m"B.new"
 '
 
 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
 	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 &&
+	grep "hook .hook-1. is not marked as parallel=true" err &&
+	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" &&
 
-- 
2.52.0.732.gb351b5166d.dirty
Previous: Adrian RatiuNext: Adrian Ratiu
Message 91 of 113 in “Run hooks in parallel”
  1. 0/4 Run hooks in parallelAdrian Ratiu, Feb 4, 2026
  2. 1/4 config: add a repo_config_get_uint() helperAdrian Ratiu, Feb 4, 2026
  3. 3/4 hook: introduce extensions.hookStdoutToStderrAdrian Ratiu, Feb 4, 2026
  4. 2/4 hook: allow parallel hook executionAdrian Ratiu, Feb 4, 2026
  5. Patrick SteinhardtFeb 11, 2026
  6. Adrian RatiuFeb 12, 2026
  7. 4/4 hook: allow runtime enabling extensions.hookStdoutToStderrAdrian Ratiu, Feb 4, 2026
  8. Phillip WoodFeb 12, 2026
  9. Adrian RatiuFeb 12, 2026
  10. Phillip WoodFeb 13, 2026
  11. Adrian RatiuFeb 13, 2026
  12. 00/10 Run hooks in parallelAdrian Ratiu, Feb 22, 2026
  13. 02/10 config: add a repo_config_get_uint() helperAdrian Ratiu, Feb 22, 2026
  14. 03/10 hook: refactor hook_config_cache from strmap to named structAdrian Ratiu, Feb 22, 2026
  15. 04/10 hook: parse the hook.jobs configAdrian Ratiu, Feb 22, 2026
  16. 06/10 hook: mark non-parallelizable hooksAdrian Ratiu, Feb 22, 2026
  17. 01/10 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Feb 22, 2026
  18. 05/10 hook: allow parallel hook executionAdrian Ratiu, Feb 22, 2026
  19. 08/10 hook: add per-event jobs configAdrian Ratiu, Feb 22, 2026
  20. 09/10 hook: introduce extensions.hookStdoutToStderrAdrian Ratiu, Feb 22, 2026
  21. 10/10 hook: allow runtime enabling extensions.hookStdoutToStderrAdrian Ratiu, Feb 22, 2026
  22. 07/10 hook: add -j/--jobs option to git hook runAdrian Ratiu, Feb 22, 2026
  23. 0/9 Run hooks in parallelAdrian Ratiu, Mar 9, 2026
  24. 2/9 config: add a repo_config_get_uint() helperAdrian Ratiu, Mar 9, 2026
  25. 1/9 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Mar 9, 2026
  26. Junio C HamanoMar 15, 2026
  27. Junio C HamanoMar 15, 2026
  28. 3/9 hook: parse the hook.jobs configAdrian Ratiu, Mar 9, 2026
  29. Junio C HamanoMar 15, 2026
  30. 5/9 hook: mark non-parallelizable hooksAdrian Ratiu, Mar 9, 2026
  31. Junio C HamanoMar 15, 2026
  32. Adrian RatiuMar 18, 2026
  33. 6/9 hook: add -j/--jobs option to git hook runAdrian Ratiu, Mar 9, 2026
  34. Junio C HamanoMar 15, 2026
  35. Adrian RatiuMar 18, 2026
  36. 4/9 hook: allow parallel hook executionAdrian Ratiu, Mar 9, 2026
  37. Junio C HamanoMar 15, 2026
  38. Adrian RatiuMar 18, 2026
  39. 7/9 hook: add per-event jobs configAdrian Ratiu, Mar 9, 2026
  40. Junio C HamanoMar 16, 2026
  41. Adrian RatiuMar 18, 2026
  42. 8/9 hook: introduce extensions.hookStdoutToStderrAdrian Ratiu, Mar 9, 2026
  43. Junio C HamanoMar 16, 2026
  44. Adrian RatiuMar 18, 2026
  45. 9/9 hook: allow runtime enabling extensions.hookStdoutToStderrAdrian Ratiu, Mar 9, 2026
  46. 0/9 Run hooks in parallelAdrian Ratiu, Mar 20, 2026
  47. 1/9 config: add a repo_config_get_uint() helperAdrian Ratiu, Mar 20, 2026
  48. 2/9 hook: parse the hook.jobs configAdrian Ratiu, Mar 20, 2026
  49. Patrick SteinhardtMar 24, 2026
  50. Adrian RatiuMar 24, 2026
  51. 4/9 hook: allow pre-push parallel executionAdrian Ratiu, Mar 20, 2026
  52. 3/9 hook: allow parallel hook executionAdrian Ratiu, Mar 20, 2026
  53. Patrick SteinhardtMar 24, 2026
  54. 5/9 hook: mark non-parallelizable hooksAdrian Ratiu, Mar 20, 2026
  55. 6/9 hook: add -j/--jobs option to git hook runAdrian Ratiu, Mar 20, 2026
  56. Patrick SteinhardtMar 24, 2026
  57. 8/9 hook: warn when hook.<friendly-name>.jobs is setAdrian Ratiu, Mar 20, 2026
  58. Patrick SteinhardtMar 24, 2026
  59. 7/9 hook: add per-event jobs configAdrian Ratiu, Mar 20, 2026
  60. Patrick SteinhardtMar 24, 2026
  61. 9/9 hook: add hook.<event>.enabled switchAdrian Ratiu, Mar 20, 2026
  62. Patrick SteinhardtMar 24, 2026
  63. Adrian RatiuMar 25, 2026
  64. Junio C HamanoMar 20, 2026
  65. Adrian RatiuMar 23, 2026
  66. Patrick SteinhardtMar 24, 2026
  67. 00/12 Run hooks in parallelAdrian Ratiu, Mar 26, 2026
  68. 01/12 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Mar 26, 2026
  69. 02/12 config: add a repo_config_get_uint() helperAdrian Ratiu, Mar 26, 2026
  70. 03/12 hook: parse the hook.jobs configAdrian Ratiu, Mar 26, 2026
  71. 06/12 hook: mark non-parallelizable hooksAdrian Ratiu, Mar 26, 2026
  72. 07/12 hook: add -j/--jobs option to git hook runAdrian Ratiu, Mar 26, 2026
  73. Patrick SteinhardtMar 27, 2026
  74. 05/12 hook: allow pre-push parallel executionAdrian Ratiu, Mar 26, 2026
  75. 08/12 hook: add per-event jobs configAdrian Ratiu, Mar 26, 2026
  76. 04/12 hook: allow parallel hook executionAdrian Ratiu, Mar 26, 2026
  77. 09/12 hook: warn when hook.<friendly-name>.jobs is setAdrian Ratiu, Mar 26, 2026
  78. Patrick SteinhardtMar 27, 2026
  79. 11/12 hook: add hook.<event>.enabled switchAdrian Ratiu, Mar 26, 2026
  80. 10/12 hook: move is_known_hook() to hook.c for wider useAdrian Ratiu, Mar 26, 2026
  81. Patrick SteinhardtMar 27, 2026
  82. Adrian RatiuMar 27, 2026
  83. 12/12 hook: allow hook.jobs=-1 to use all available CPU coresAdrian Ratiu, Mar 26, 2026
  84. 00/12 Run hooks in parallelAdrian Ratiu, Apr 4, 2026
  85. 02/12 config: add a repo_config_get_uint() helperAdrian Ratiu, Apr 4, 2026
  86. 01/12 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Apr 4, 2026
  87. 03/12 hook: parse the hook.jobs configAdrian Ratiu, Apr 4, 2026
  88. 04/12 hook: allow parallel hook executionAdrian Ratiu, Apr 4, 2026
  89. 05/12 hook: allow pre-push parallel executionAdrian Ratiu, Apr 4, 2026
  90. 06/12 hook: mark non-parallelizable hooksAdrian Ratiu, Apr 4, 2026
  91. 07/12 hook: add -j/--jobs option to git hook runAdrian Ratiu, Apr 4, 2026
  92. 08/12 hook: add per-event jobs configAdrian Ratiu, Apr 4, 2026
  93. 09/12 hook: warn when hook.<friendly-name>.jobs is setAdrian Ratiu, Apr 4, 2026
  94. 10/12 hook: move is_known_hook() to hook.c for wider useAdrian Ratiu, Apr 4, 2026
  95. 11/12 hook: add hook.<event>.enabled switchAdrian Ratiu, Apr 4, 2026
  96. 12/12 hook: allow hook.jobs=-1 to use all available CPU coresAdrian Ratiu, Apr 4, 2026
  97. Junio C HamanoApr 6, 2026
  98. Patrick SteinhardtApr 8, 2026
  99. Junio C HamanoApr 8, 2026
  100. 00/13 Run hooks in parallelAdrian Ratiu, Apr 10, 2026
  101. 01/13 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Apr 10, 2026
  102. 02/13 config: add a repo_config_get_uint() helperAdrian Ratiu, Apr 10, 2026
  103. 03/13 hook: parse the hook.jobs configAdrian Ratiu, Apr 10, 2026
  104. 04/13 hook: allow parallel hook executionAdrian Ratiu, Apr 10, 2026
  105. 05/13 hook: allow pre-push parallel executionAdrian Ratiu, Apr 10, 2026
  106. 06/13 hook: mark non-parallelizable hooksAdrian Ratiu, Apr 10, 2026
  107. 07/13 hook: add -j/--jobs option to git hook runAdrian Ratiu, Apr 10, 2026
  108. 08/13 hook: add per-event jobs configAdrian Ratiu, Apr 10, 2026
  109. 09/13 hook: warn when hook.<friendly-name>.jobs is setAdrian Ratiu, Apr 10, 2026
  110. 10/13 hook: move is_known_hook() to hook.c for wider useAdrian Ratiu, Apr 10, 2026
  111. 11/13 hook: add hook.<event>.enabled switchAdrian Ratiu, Apr 10, 2026
  112. 12/13 hook: allow hook.jobs=-1 to use all available CPU coresAdrian Ratiu, Apr 10, 2026
  113. 13/13 t1800: test SIGPIPE with parallel hooksAdrian Ratiu, Apr 10, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.