From: Adrian Ratiu <adrian.ratiu@collabora•com>
To: git@vger•kernel.org
Cc: "Jeff King" <peff@peff•net>,
"Emily Shaffer" <emilyshaffer@google•com>,
"Junio C Hamano" <gitster@pobox•com>,
"Patrick Steinhardt" <ps@pks•im>,
"Josh Steadmon" <steadmon@google•com>,
"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail•com>,
"brian m . carlson" <sandals@crustytoothpaste•net>,
"Ævar Arnfjörð Bjarmason" <avarab@gmail•com>,
"Adrian Ratiu" <adrian.ratiu@collabora•com>
Subject: [PATCH v2 07/10] hook: add -j/--jobs option to git hook run
Date: Sun, 22 Feb 2026 02:29:01 +0200 [thread overview]
Message-ID: <20260222002904.1879356-8-adrian.ratiu@collabora.com> (raw)
In-Reply-To: <20260222002904.1879356-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 | 18 +++++-
builtin/hook.c | 5 +-
hook.c | 14 ++--
t/t1800-hook.sh | 123 ++++++++++++++++++++++++++++++++++--
4 files changed, 144 insertions(+), 16 deletions(-)
diff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc
index 12d2701b52..c9492f6f8e 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 [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
+'git hook' run [--ignore-missing] [--to-stdin=<path>] [(-j|--jobs) <n>]
+ <hook-name> [-- <hook-args>]
'git hook' list [-z] <hook-name>
DESCRIPTION
@@ -134,6 +135,18 @@ OPTIONS
-z::
Terminate "list" output lines with NUL instead of newlines.
+-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). 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
--------
@@ -156,7 +169,8 @@ running:
git hook run 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 83020dfb4f..1fdde80113 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -9,7 +9,8 @@
#include "abspath.h"
#define BUILTIN_HOOK_RUN_USAGE \
- N_("git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
+ N_("git hook run [--ignore-missing] [--to-stdin=<path>] [(-j|--jobs) <n>]\n" \
+ "<hook-name> [-- <hook-args>]")
#define BUILTIN_HOOK_LIST_USAGE \
N_("git hook list [-z] <hook-name>")
@@ -97,6 +98,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 3d624e2bcd..6214276e3d 100644
--- a/hook.c
+++ b/hook.c
@@ -524,15 +524,17 @@ static unsigned int get_hook_jobs(struct repository *r,
if (!options->stdout_to_stderr)
return 1;
- /* An explicit job count (FORCE_SERIAL jobs=1, or -j from CLI). */
- if (options->jobs)
- return options->jobs;
+ /* Pinned serial: FORCE_SERIAL (internal) or explicit -j1 from CLI. */
+ if (options->jobs == 1)
+ return 1;
/*
- * Use hook.jobs from the already-parsed config cache (in-repo), or
- * fall back to a direct config lookup (out-of-repo). Default to 1.
+ * Resolve effective job count: -jN (when given) overrides config.
+ * Default to 1 when both config an -jN are missing.
*/
- if (r && r->gitdir && r->hook_config_cache)
+ if (options->jobs > 1)
+ jobs = options->jobs;
+ else if (r && r->gitdir && r->hook_config_cache)
/* Use the already-parsed cache (in-repo) */
jobs = r->hook_config_cache->jobs ? r->hook_config_cache->jobs : 1;
else
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 0d17969cdf..a6913b8c62 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -239,10 +239,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 &&
@@ -260,12 +270,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' '
@@ -604,6 +623,96 @@ 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 -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" &&
--
2.52.0.732.gb351b5166d.dirty
next prev parent reply other threads:[~2026-02-22 0:30 UTC|newest]
Thread overview: 113+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-04 17:33 [PATCH 0/4] Run hooks in parallel Adrian Ratiu
2026-02-04 17:33 ` [PATCH 1/4] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-02-04 17:33 ` [PATCH 2/4] hook: allow parallel hook execution Adrian Ratiu
2026-02-11 12:41 ` Patrick Steinhardt
2026-02-12 12:25 ` Adrian Ratiu
2026-02-04 17:33 ` [PATCH 3/4] hook: introduce extensions.hookStdoutToStderr Adrian Ratiu
2026-02-04 17:33 ` [PATCH 4/4] hook: allow runtime enabling extensions.hookStdoutToStderr Adrian Ratiu
2026-02-12 10:43 ` [PATCH 0/4] Run hooks in parallel Phillip Wood
2026-02-12 14:24 ` Adrian Ratiu
2026-02-13 14:39 ` Phillip Wood
2026-02-13 17:21 ` Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 00/10] " Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 01/10] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 02/10] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 03/10] hook: refactor hook_config_cache from strmap to named struct Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 04/10] hook: parse the hook.jobs config Adrian Ratiu
2026-02-22 0:28 ` [PATCH v2 05/10] hook: allow parallel hook execution Adrian Ratiu
2026-02-22 0:29 ` [PATCH v2 06/10] hook: mark non-parallelizable hooks Adrian Ratiu
2026-02-22 0:29 ` Adrian Ratiu [this message]
2026-02-22 0:29 ` [PATCH v2 08/10] hook: add per-event jobs config Adrian Ratiu
2026-02-22 0:29 ` [PATCH v2 09/10] hook: introduce extensions.hookStdoutToStderr Adrian Ratiu
2026-02-22 0:29 ` [PATCH v2 10/10] hook: allow runtime enabling extensions.hookStdoutToStderr Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 0/9] Run hooks in parallel Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 1/9] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-03-15 4:55 ` Junio C Hamano
2026-03-15 5:05 ` Junio C Hamano
2026-03-09 13:37 ` [PATCH v3 2/9] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 3/9] hook: parse the hook.jobs config Adrian Ratiu
2026-03-15 16:13 ` Junio C Hamano
2026-03-09 13:37 ` [PATCH v3 4/9] hook: allow parallel hook execution Adrian Ratiu
2026-03-15 20:46 ` Junio C Hamano
2026-03-18 18:02 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 5/9] hook: mark non-parallelizable hooks Adrian Ratiu
2026-03-15 20:56 ` Junio C Hamano
2026-03-18 18:40 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 6/9] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-03-15 21:00 ` Junio C Hamano
2026-03-18 19:00 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 7/9] hook: add per-event jobs config Adrian Ratiu
2026-03-16 18:40 ` Junio C Hamano
2026-03-18 19:21 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 8/9] hook: introduce extensions.hookStdoutToStderr Adrian Ratiu
2026-03-16 18:44 ` Junio C Hamano
2026-03-18 19:50 ` Adrian Ratiu
2026-03-09 13:37 ` [PATCH v3 9/9] hook: allow runtime enabling extensions.hookStdoutToStderr Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 0/9] Run hooks in parallel Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 1/9] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 2/9] hook: parse the hook.jobs config Adrian Ratiu
2026-03-24 9:07 ` Patrick Steinhardt
2026-03-24 18:59 ` Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 3/9] hook: allow parallel hook execution Adrian Ratiu
2026-03-24 9:07 ` Patrick Steinhardt
2026-03-20 13:53 ` [PATCH v4 4/9] hook: allow pre-push parallel execution Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 5/9] hook: mark non-parallelizable hooks Adrian Ratiu
2026-03-20 13:53 ` [PATCH v4 6/9] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-03-24 9:07 ` Patrick Steinhardt
2026-03-20 13:53 ` [PATCH v4 7/9] hook: add per-event jobs config Adrian Ratiu
2026-03-24 9:08 ` Patrick Steinhardt
2026-03-20 13:53 ` [PATCH v4 8/9] hook: warn when hook.<friendly-name>.jobs is set Adrian Ratiu
2026-03-24 9:08 ` Patrick Steinhardt
2026-03-20 13:53 ` [PATCH v4 9/9] hook: add hook.<event>.enabled switch Adrian Ratiu
2026-03-24 9:08 ` Patrick Steinhardt
2026-03-25 18:43 ` Adrian Ratiu
2026-03-20 17:24 ` [PATCH v4 0/9] Run hooks in parallel Junio C Hamano
2026-03-23 15:07 ` Adrian Ratiu
2026-03-24 9:07 ` Patrick Steinhardt
2026-03-26 10:18 ` [PATCH v5 00/12] " Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 01/12] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 02/12] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 03/12] hook: parse the hook.jobs config Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 04/12] hook: allow parallel hook execution Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 05/12] hook: allow pre-push parallel execution Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 06/12] hook: mark non-parallelizable hooks Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 07/12] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-03-27 14:46 ` Patrick Steinhardt
2026-03-26 10:18 ` [PATCH v5 08/12] hook: add per-event jobs config Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 09/12] hook: warn when hook.<friendly-name>.jobs is set Adrian Ratiu
2026-03-27 14:46 ` Patrick Steinhardt
2026-03-26 10:18 ` [PATCH v5 10/12] hook: move is_known_hook() to hook.c for wider use Adrian Ratiu
2026-03-27 14:46 ` Patrick Steinhardt
2026-03-27 15:59 ` Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 11/12] hook: add hook.<event>.enabled switch Adrian Ratiu
2026-03-26 10:18 ` [PATCH v5 12/12] hook: allow hook.jobs=-1 to use all available CPU cores Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 00/12] Run hooks in parallel Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 01/12] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 02/12] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 03/12] hook: parse the hook.jobs config Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 04/12] hook: allow parallel hook execution Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 05/12] hook: allow pre-push parallel execution Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 06/12] hook: mark non-parallelizable hooks Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 07/12] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 08/12] hook: add per-event jobs config Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 09/12] hook: warn when hook.<friendly-name>.jobs is set Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 10/12] hook: move is_known_hook() to hook.c for wider use Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 11/12] hook: add hook.<event>.enabled switch Adrian Ratiu
2026-04-04 8:29 ` [PATCH v6 12/12] hook: allow hook.jobs=-1 to use all available CPU cores Adrian Ratiu
2026-04-06 16:24 ` [PATCH v6 00/12] Run hooks in parallel Junio C Hamano
2026-04-08 10:17 ` Patrick Steinhardt
2026-04-08 16:57 ` Junio C Hamano
2026-04-10 9:05 ` [PATCH v7 00/13] " Adrian Ratiu
2026-04-10 9:05 ` [PATCH v7 01/13] repository: fix repo_init() memleak due to missing _clear() Adrian Ratiu
2026-04-10 9:05 ` [PATCH v7 02/13] config: add a repo_config_get_uint() helper Adrian Ratiu
2026-04-10 9:05 ` [PATCH v7 03/13] hook: parse the hook.jobs config Adrian Ratiu
2026-04-10 9:05 ` [PATCH v7 04/13] hook: allow parallel hook execution Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 05/13] hook: allow pre-push parallel execution Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 06/13] hook: mark non-parallelizable hooks Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 07/13] hook: add -j/--jobs option to git hook run Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 08/13] hook: add per-event jobs config Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 09/13] hook: warn when hook.<friendly-name>.jobs is set Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 10/13] hook: move is_known_hook() to hook.c for wider use Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 11/13] hook: add hook.<event>.enabled switch Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 12/13] hook: allow hook.jobs=-1 to use all available CPU cores Adrian Ratiu
2026-04-10 9:06 ` [PATCH v7 13/13] t1800: test SIGPIPE with parallel hooks Adrian Ratiu
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260222002904.1879356-8-adrian.ratiu@collabora.com \
--to=adrian.ratiu@collabora$(echo .)com \
--cc=avarab@gmail$(echo .)com \
--cc=emilyshaffer@google$(echo .)com \
--cc=git@vger$(echo .)kernel.org \
--cc=gitster@pobox$(echo .)com \
--cc=kristofferhaugsbakk@fastmail$(echo .)com \
--cc=peff@peff$(echo .)net \
--cc=ps@pks$(echo .)im \
--cc=sandals@crustytoothpaste$(echo .)net \
--cc=steadmon@google$(echo .)com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox