Skip to content

Commit 084a55b

Browse files
10ne1gitster
authored andcommitted
hook: add per-event jobs config
Add a hook.<event>.jobs count config that allows users to override the global hook.jobs setting for specific hook events. This allows finer-grained control over parallelism on a per-event basis. For example, to run `post-receive` hooks with up to 4 parallel jobs while keeping other events at their global default: [hook] post-receive.jobs = 4 Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 091d2db commit 084a55b

5 files changed

Lines changed: 123 additions & 5 deletions

File tree

Documentation/config/hook.adoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,28 @@ hook.<friendly-name>.parallel::
3333
found in the hooks directory do not need to, and run in parallel when
3434
the effective job count is greater than 1. See linkgit:git-hook[1].
3535

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+
3654
hook.jobs::
3755
Specifies how many hooks can be run simultaneously during parallelized
3856
hook execution. If unspecified, defaults to 1 (serial execution).
57+
Can be overridden on a per-event basis with `hook.<event>.jobs`.
3958
Some hooks always run sequentially regardless of this setting because
4059
they operate on shared data and cannot safely be parallelized:
4160
+

hook.c

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ struct hook_config_cache_entry {
125125
* event_hooks: event-name to list of friendly-names map.
126126
* disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.
127127
* parallel_hooks: friendly-name to parallel flag.
128+
* event_jobs: event-name to per-event jobs count (stored as uintptr_t, NULL == unset).
128129
* jobs: value of the global hook.jobs key. Defaults to 0 if unset (stored in r->hook_jobs).
129130
*/
130131
struct hook_all_config_cb {
131132
struct strmap commands;
132133
struct strmap event_hooks;
133134
struct string_list disabled_hooks;
134135
struct strmap parallel_hooks;
136+
struct strmap event_jobs;
135137
unsigned int jobs;
136138
};
137139

@@ -231,6 +233,18 @@ static int hook_config_lookup_all(const char *key, const char *value,
231233
warning(_("hook.%s.parallel must be a boolean,"
232234
" ignoring: '%s'"),
233235
hook_name, value);
236+
} else if (!strcmp(subkey, "jobs")) {
237+
unsigned int v;
238+
if (!git_parse_uint(value, &v))
239+
warning(_("hook.%s.jobs must be a positive integer,"
240+
" ignoring: '%s'"),
241+
hook_name, value);
242+
else if (!v)
243+
warning(_("hook.%s.jobs must be positive,"
244+
" ignoring: 0"), hook_name);
245+
else
246+
strmap_put(&data->event_jobs, hook_name,
247+
(void *)(uintptr_t)v);
234248
}
235249

236250
free(hook_name);
@@ -276,6 +290,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
276290
strmap_init(&cb_data.event_hooks);
277291
string_list_init_dup(&cb_data.disabled_hooks);
278292
strmap_init(&cb_data.parallel_hooks);
293+
strmap_init(&cb_data.event_jobs);
279294

280295
/* Parse all configs in one run, capturing hook.* including hook.jobs. */
281296
repo_config(r, hook_config_lookup_all, &cb_data);
@@ -323,8 +338,10 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
323338
strmap_put(cache, e->key, hooks);
324339
}
325340

326-
if (r)
341+
if (r) {
327342
r->hook_jobs = cb_data.jobs;
343+
r->event_jobs = cb_data.event_jobs;
344+
}
328345

329346
strmap_clear(&cb_data.commands, 1);
330347
strmap_clear(&cb_data.parallel_hooks, 0); /* values are uintptr_t, not heap ptrs */
@@ -587,6 +604,7 @@ static void warn_non_parallel_hooks_override(unsigned int jobs,
587604
/* Determine how many jobs to use for hook execution. */
588605
static unsigned int get_hook_jobs(struct repository *r,
589606
struct run_hooks_opt *options,
607+
const char *hook_name,
590608
struct string_list *hook_list)
591609
{
592610
/*
@@ -606,16 +624,34 @@ static unsigned int get_hook_jobs(struct repository *r,
606624
*/
607625
options->jobs = 1;
608626
if (r) {
609-
if (r->gitdir && r->hook_config_cache && r->hook_jobs)
610-
options->jobs = r->hook_jobs;
611-
else
627+
if (r->gitdir && r->hook_config_cache) {
628+
void *event_jobs;
629+
630+
if (r->hook_jobs)
631+
options->jobs = r->hook_jobs;
632+
633+
event_jobs = strmap_get(&r->event_jobs, hook_name);
634+
if (event_jobs)
635+
options->jobs = (unsigned int)(uintptr_t)event_jobs;
636+
} else {
637+
unsigned int event_jobs;
638+
char *key;
639+
612640
repo_config_get_uint(r, "hook.jobs", &options->jobs);
641+
642+
key = xstrfmt("hook.%s.jobs", hook_name);
643+
if (!repo_config_get_uint(r, key, &event_jobs) && event_jobs)
644+
options->jobs = event_jobs;
645+
free(key);
646+
}
613647
}
614648

615649
/*
616650
* Cap to serial any configured hook not marked as parallel = true.
617651
* This enforces the parallel = false default, even for "traditional"
618652
* hooks from the hookdir which cannot be marked parallel = true.
653+
* The same restriction applies whether jobs came from hook.jobs or
654+
* hook.<event>.jobs.
619655
*/
620656
for (size_t i = 0; i < hook_list->nr; i++) {
621657
struct hook *h = hook_list->items[i].util;
@@ -642,7 +678,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
642678
.options = options,
643679
};
644680
int ret = 0;
645-
unsigned int jobs = get_hook_jobs(r, options, hook_list);
681+
unsigned int jobs = get_hook_jobs(r, options, hook_name, hook_list);
646682
const struct run_process_parallel_opts opts = {
647683
.tr2_category = "hook",
648684
.tr2_label = hook_name,

repository.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ void repo_clear(struct repository *repo)
426426
hook_cache_clear(repo->hook_config_cache);
427427
FREE_AND_NULL(repo->hook_config_cache);
428428
}
429+
strmap_clear(&repo->event_jobs, 0); /* values are uintptr_t, not heap ptrs */
429430

430431
if (repo->promisor_remote_config) {
431432
promisor_remote_clear(repo->promisor_remote_config);

repository.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ struct repository {
175175
/* Cached value of hook.jobs config (0 if unset, defaults to serial). */
176176
unsigned int hook_jobs;
177177

178+
/* Cached map of event-name -> jobs count (as uintptr_t) from hook.<event>.jobs. */
179+
struct strmap event_jobs;
180+
178181
/* Configurations related to promisor remotes. */
179182
char *repository_format_partial_clone;
180183
struct promisor_remote_config *promisor_remote_config;

t/t1800-hook.sh

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,4 +969,63 @@ test_expect_success 'hook.jobs=2 is ignored for force-serial hooks (pre-commit)'
969969
test_cmp expect hook.order
970970
'
971971

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+
9721031
test_done

0 commit comments

Comments
 (0)