Skip to content

Commit a6c502a

Browse files
committed
Merge branch 'ar/parallel-hooks' into seen
* ar/parallel-hooks: hook: allow runtime enabling extensions.hookStdoutToStderr hook: introduce extensions.hookStdoutToStderr hook: add per-event jobs config hook: add -j/--jobs option to git hook run hook: mark non-parallelizable hooks hook: allow parallel hook execution hook: parse the hook.jobs config hook: refactor hook_config_cache from strmap to named struct config: add a repo_config_get_uint() helper repository: fix repo_init() memleak due to missing _clear()
2 parents 254d084 + 3b413bd commit a6c502a

22 files changed

Lines changed: 843 additions & 63 deletions

Documentation/config/extensions.adoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,21 @@ The extension can be enabled automatically for new repositories by setting
116116
`init.defaultSubmodulePathConfig` to `true`, for example by running
117117
`git config --global init.defaultSubmodulePathConfig true`.
118118
119+
hookStdoutToStderr:::
120+
If enabled, the stdout of all hooks is redirected to stderr. This
121+
enforces consistency, since by default most hooks already behave
122+
this way, with pre-push being the only known exception.
123+
+
124+
This is useful for parallel hook execution (see the `hook.jobs` config in
125+
linkgit:git-config[1]), as it allows the output of multiple hooks running
126+
in parallel to be grouped (de-interleaved) correctly.
127+
+
128+
Defaults to disabled. When disabled, `hook.jobs` has no effect for pre-push
129+
hooks, which will always be run sequentially.
130+
+
131+
The extension can also be enabled by setting `hook.forceStdoutToStderr`
132+
to `true` in the global configuration.
133+
119134
worktreeConfig:::
120135
If enabled, then worktrees will load config settings from the
121136
`$GIT_DIR/config.worktree` file in addition to the

Documentation/config/hook.adoc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,52 @@ hook.<name>.enabled::
2222
configuration. This is particularly useful when a hook is defined
2323
in a system or global config file and needs to be disabled for a
2424
specific repository. See linkgit:git-hook[1].
25+
26+
hook.<name>.parallel::
27+
Whether the hook `hook.<name>` may run in parallel with other hooks
28+
for the same event. Defaults to `false`. Set to `true` only when the
29+
hook script is safe to run concurrently with other hooks for the same
30+
event. If any hook for an event does not have this set to `true`,
31+
all hooks for that event run sequentially regardless of `hook.jobs`.
32+
Only configured (named) hooks need to declare this. Traditional hooks
33+
found in the hooks directory do not need to, and run in parallel when
34+
the effective job count is greater than 1. See linkgit:git-hook[1].
35+
36+
hook.<event>.jobs::
37+
Specifies how many hooks can be run simultaneously for the `<event>`
38+
hook event (e.g. `hook.post-receive.jobs = 4`). Overrides `hook.jobs`
39+
for this specific event. The same parallelism restrictions apply: this
40+
setting has no effect unless all configured hooks for the event have
41+
`hook.<friendly-name>.parallel` set to `true`. Must be a positive int,
42+
zero is rejected with a warning. See linkgit:git-hook[1].
43+
+
44+
Note on naming: although this key resembles `hook.<friendly-name>.*`
45+
(a per-hook setting), `<event>` must be the event name, not a hook
46+
friendly name. The key component is stored literally and looked up by
47+
event name at runtime with no translation between the two namespaces.
48+
A key like `hook.my-hook.jobs` is stored under `"my-hook"` but the
49+
lookup at runtime uses the event name (e.g. `"post-receive"`), so
50+
`hook.my-hook.jobs` is silently ignored even when `my-hook` is
51+
registered for that event. Use `hook.post-receive.jobs` or any other
52+
valid event name when setting `hook.<event>.jobs`.
53+
54+
hook.jobs::
55+
Specifies how many hooks can be run simultaneously during parallelized
56+
hook execution. If unspecified, defaults to 1 (serial execution).
57+
Can be overridden on a per-event basis with `hook.<event>.jobs`.
58+
Some hooks always run sequentially regardless of this setting because
59+
git knows they cannot safely be parallelized: `applypatch-msg`,
60+
`pre-commit`, `prepare-commit-msg`, `commit-msg`, `post-commit`,
61+
`post-checkout`, and `push-to-checkout`.
62+
+
63+
This setting has no effect unless all configured hooks for the event have
64+
`hook.<name>.parallel` set to `true`.
65+
+
66+
This has no effect for hooks requiring separate output streams (like `pre-push`)
67+
unless `extensions.hookStdoutToStderr` is enabled.
68+
69+
hook.forceStdoutToStderr::
70+
A boolean that enables the `extensions.hookStdoutToStderr` behavior
71+
(merging stdout to stderr for all hooks) globally. This effectively
72+
forces all hooks to behave as if the extension was enabled, allowing
73+
parallel execution for hooks like `pre-push`.

Documentation/git-hook.adoc

Lines changed: 16 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 [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
11+
'git hook' run [--ignore-missing] [--to-stdin=<path>] [(-j|--jobs) <n>]
12+
<hook-name> [-- <hook-args>]
1213
'git hook' list [-z] <hook-name>
1314

1415
DESCRIPTION
@@ -134,6 +135,18 @@ OPTIONS
134135
-z::
135136
Terminate "list" output lines with NUL instead of newlines.
136137

138+
-j::
139+
--jobs::
140+
Only valid for `run`.
141+
+
142+
Specify how many hooks to run simultaneously. If this flag is not specified,
143+
the value of the `hook.jobs` config is used, see linkgit:git-config[1]. If
144+
neither is specified, defaults to 1 (serial execution). Some hooks always run
145+
sequentially regardless of this flag or the `hook.jobs` config, because git
146+
knows they cannot safely run in parallel: `applypatch-msg`, `pre-commit`,
147+
`prepare-commit-msg`, `commit-msg`, `post-commit`, `post-checkout`, and
148+
`push-to-checkout`.
149+
137150
WRAPPERS
138151
--------
139152
@@ -156,7 +169,8 @@ running:
156169
git hook run mywrapper-start-tests \
157170
# providing something to stdin
158171
--stdin some-tempfile-123 \
159-
# execute hooks in serial
172+
# execute multiple hooks in parallel
173+
--jobs 3 \
160174
# plus some arguments of your own...
161175
-- \
162176
--testname bar \

builtin/am.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,9 +490,11 @@ static int run_applypatch_msg_hook(struct am_state *state)
490490

491491
assert(state->msg);
492492

493-
if (!state->no_verify)
494-
ret = run_hooks_l(the_repository, "applypatch-msg",
495-
am_path(state, "final-commit"), NULL);
493+
if (!state->no_verify) {
494+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
495+
strvec_push(&opt.args, am_path(state, "final-commit"));
496+
ret = run_hooks_opt(the_repository, "applypatch-msg", &opt);
497+
}
496498

497499
if (!ret) {
498500
FREE_AND_NULL(state->msg);

builtin/checkout.c

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "resolve-undo.h"
3232
#include "revision.h"
3333
#include "setup.h"
34+
#include "strvec.h"
3435
#include "submodule.h"
3536
#include "symlinks.h"
3637
#include "trace2.h"
@@ -123,13 +124,19 @@ static void branch_info_release(struct branch_info *info)
123124
static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
124125
int changed)
125126
{
126-
return run_hooks_l(the_repository, "post-checkout",
127-
oid_to_hex(old_commit ? &old_commit->object.oid : null_oid(the_hash_algo)),
128-
oid_to_hex(new_commit ? &new_commit->object.oid : null_oid(the_hash_algo)),
129-
changed ? "1" : "0", NULL);
130-
/* "new_commit" can be NULL when checking out from the index before
131-
a commit exists. */
127+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
132128

129+
/*
130+
* "new_commit" can be NULL when checking out from the index before
131+
* a commit exists.
132+
*/
133+
strvec_pushl(&opt.args,
134+
oid_to_hex(old_commit ? &old_commit->object.oid : null_oid(the_hash_algo)),
135+
oid_to_hex(new_commit ? &new_commit->object.oid : null_oid(the_hash_algo)),
136+
changed ? "1" : "0",
137+
NULL);
138+
139+
return run_hooks_opt(the_repository, "post-checkout", &opt);
133140
}
134141

135142
static int update_some(const struct object_id *oid, struct strbuf *base,

builtin/clone.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,7 @@ static int checkout(int submodule_progress,
648648
struct tree *tree;
649649
struct tree_desc t;
650650
int err = 0;
651+
struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
651652

652653
if (option_no_checkout)
653654
return 0;
@@ -698,8 +699,9 @@ static int checkout(int submodule_progress,
698699
if (write_locked_index(the_repository->index, &lock_file, COMMIT_LOCK))
699700
die(_("unable to write new index file"));
700701

701-
err |= run_hooks_l(the_repository, "post-checkout", oid_to_hex(null_oid(the_hash_algo)),
702-
oid_to_hex(&oid), "1", NULL);
702+
strvec_pushl(&hook_opt.args, oid_to_hex(null_oid(the_hash_algo)),
703+
oid_to_hex(&oid), "1", NULL);
704+
err |= run_hooks_opt(the_repository, "post-checkout", &hook_opt);
703705

704706
if (!err && (option_recurse_submodules.nr > 0)) {
705707
struct child_process cmd = CHILD_PROCESS_INIT;

builtin/hook.c

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

1111
#define BUILTIN_HOOK_RUN_USAGE \
12-
N_("git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
12+
N_("git hook run [--ignore-missing] [--to-stdin=<path>] [(-j|--jobs) <n>]\n" \
13+
"<hook-name> [-- <hook-args>]")
1314
#define BUILTIN_HOOK_LIST_USAGE \
1415
N_("git hook list [-z] <hook-name>")
1516

@@ -97,6 +98,8 @@ static int run(int argc, const char **argv, const char *prefix,
9798
N_("silently ignore missing requested <hook-name>")),
9899
OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"),
99100
N_("file to read into hooks' stdin")),
101+
OPT_UNSIGNED('j', "jobs", &opt.jobs,
102+
N_("run up to <n> hooks simultaneously")),
100103
OPT_END(),
101104
};
102105
int ret;

builtin/receive-pack.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1460,7 +1460,8 @@ static const char *push_to_checkout(unsigned char *hash,
14601460
struct strvec *env,
14611461
const char *work_tree)
14621462
{
1463-
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
1463+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
1464+
14641465
opt.invoked_hook = invoked_hook;
14651466

14661467
strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));

builtin/worktree.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ static int add_worktree(const char *path, const char *refname,
609609
* is_junk is cleared, but do return appropriate code when hook fails.
610610
*/
611611
if (!ret && opts->checkout && !opts->orphan) {
612-
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
612+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
613613

614614
strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
615615
strvec_pushl(&opt.args,

commit.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1979,7 +1979,7 @@ size_t ignored_log_message_bytes(const char *buf, size_t len)
19791979
int run_commit_hook(int editor_is_used, const char *index_file,
19801980
int *invoked_hook, const char *name, ...)
19811981
{
1982-
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
1982+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
19831983
va_list args;
19841984
const char *arg;
19851985

0 commit comments

Comments
 (0)