Skip to content

Commit a333477

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 267b7e5 commit a333477

4 files changed

Lines changed: 120 additions & 6 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.<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
git knows they cannot safely be parallelized: `applypatch-msg`,
4160
`pre-commit`, `prepare-commit-msg`, `commit-msg`, `post-commit`,

hook.c

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,16 @@ struct hook_config_cache_entry {
133133
* event_hooks: event-name to list of friendly-names map.
134134
* disabled_hooks: set of friendly-names with hook.name.enabled = false.
135135
* parallel_hooks: friendly-name to parallel flag.
136+
* event_jobs: event-name to per-event jobs count (heap-allocated unsigned int *,
137+
* where NULL == unset).
136138
* jobs: value of the global hook.jobs key. Defaults to 0 if unset.
137139
*/
138140
struct hook_all_config_cb {
139141
struct strmap commands;
140142
struct strmap event_hooks;
141143
struct string_list disabled_hooks;
142144
struct strmap parallel_hooks;
145+
struct strmap event_jobs;
143146
unsigned int jobs;
144147
};
145148

@@ -222,6 +225,20 @@ static int hook_config_lookup_all(const char *key, const char *value,
222225
int v = git_parse_maybe_bool(value);
223226
if (v >= 0)
224227
strmap_put(&data->parallel_hooks, hook_name, (void *)(uintptr_t)v);
228+
} else if (!strcmp(subkey, "jobs")) {
229+
unsigned int v;
230+
if (!git_parse_uint(value, &v))
231+
warning(_("hook.%s.jobs must be a positive integer, ignoring: '%s'"),
232+
hook_name, value);
233+
else if (!v)
234+
warning(_("hook.%s.jobs must be positive, ignoring: 0"), hook_name);
235+
else {
236+
unsigned int *old;
237+
unsigned int *p = xmalloc(sizeof(*p));
238+
*p = v;
239+
old = strmap_put(&data->event_jobs, hook_name, p);
240+
free(old);
241+
}
225242
}
226243

227244
free(hook_name);
@@ -252,6 +269,7 @@ void hook_cache_clear(struct hook_config_cache *cache)
252269
free(hooks);
253270
}
254271
strmap_clear(&cache->hooks, 0);
272+
strmap_clear(&cache->event_jobs, 1); /* free heap-allocated unsigned int * values */
255273
}
256274

257275
/* Populate `cache` with the complete hook configuration */
@@ -266,6 +284,7 @@ static void build_hook_config_map(struct repository *r,
266284
strmap_init(&cb_data.event_hooks);
267285
string_list_init_dup(&cb_data.disabled_hooks);
268286
strmap_init(&cb_data.parallel_hooks);
287+
strmap_init(&cb_data.event_jobs);
269288

270289
/* Parse all configs in one run, capturing hook.* including hook.jobs. */
271290
repo_config(r, hook_config_lookup_all, &cb_data);
@@ -305,6 +324,7 @@ static void build_hook_config_map(struct repository *r,
305324
}
306325

307326
cache->jobs = cb_data.jobs;
327+
cache->event_jobs = cb_data.event_jobs;
308328

309329
strmap_clear(&cb_data.commands, 1);
310330
strmap_clear(&cb_data.parallel_hooks, 0); /* values are uintptr_t, not heap ptrs */
@@ -513,6 +533,7 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
513533
/* Determine how many jobs to use for hook execution. */
514534
static unsigned int get_hook_jobs(struct repository *r,
515535
struct run_hooks_opt *options,
536+
const char *hook_name,
516537
struct string_list *hook_list)
517538
{
518539
unsigned int jobs;
@@ -529,22 +550,36 @@ static unsigned int get_hook_jobs(struct repository *r,
529550
return 1;
530551

531552
/*
532-
* Resolve effective job count: -jN (when given) overrides config.
533-
* Default to 1 when both config an -jN are missing.
553+
* Resolve effective job count: -j N (when given) overrides config.
554+
* hook.<event>.jobs overrides hook.jobs.
555+
* Unset configs and -jN default to 1.
534556
*/
535-
if (options->jobs > 1)
557+
if (options->jobs > 1) {
536558
jobs = options->jobs;
537-
else if (r && r->gitdir && r->hook_config_cache)
559+
} else if (r && r->gitdir && r->hook_config_cache) {
538560
/* Use the already-parsed cache (in-repo) */
561+
unsigned int *event_jobs = strmap_get(&r->hook_config_cache->event_jobs,
562+
hook_name);
539563
jobs = r->hook_config_cache->jobs ? r->hook_config_cache->jobs : 1;
540-
else
564+
if (event_jobs)
565+
jobs = *event_jobs;
566+
} else {
541567
/* No cache present (out-of-repo call), use direct cfg lookup */
568+
unsigned int event_jobs;
569+
char *key;
542570
jobs = repo_config_get_uint(r, "hook.jobs", &jobs) ? 1 : jobs;
571+
key = xstrfmt("hook.%s.jobs", hook_name);
572+
if (!repo_config_get_uint(r, key, &event_jobs) && event_jobs)
573+
jobs = event_jobs;
574+
free(key);
575+
}
543576

544577
/*
545578
* Cap to serial any configured hook not marked as parallel = true.
546579
* This enforces the parallel = false default, even for "traditional"
547580
* hooks from the hookdir which cannot be marked parallel = true.
581+
* The same restriction applies whether jobs came from hook.jobs or
582+
* hook.<event>.jobs.
548583
*/
549584
for (size_t i = 0; jobs > 1 && i < hook_list->nr; i++) {
550585
struct hook *h = hook_list->items[i].util;
@@ -566,7 +601,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
566601
.options = options,
567602
};
568603
int ret = 0;
569-
unsigned int jobs = get_hook_jobs(r, options, hook_list);
604+
unsigned int jobs = get_hook_jobs(r, options, hook_name, hook_list);
570605
const struct run_process_parallel_opts opts = {
571606
.tr2_category = "hook",
572607
.tr2_label = hook_name,

hook.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free);
222222
*/
223223
struct hook_config_cache {
224224
struct strmap hooks; /* maps event name -> string_list of hooks */
225+
struct strmap event_jobs; /* maps event name -> heap-allocated unsigned int * */
225226
unsigned int jobs; /* hook.jobs config value; 0 if unset (defaults to serial) */
226227
};
227228

t/t1800-hook.sh

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

823+
test_expect_success 'hook.<event>.jobs overrides hook.jobs for that event' '
824+
test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
825+
test_config hook.hook-1.event test-hook &&
826+
test_config hook.hook-1.command \
827+
"touch sentinel.started; sleep 2; touch sentinel.done" &&
828+
test_config hook.hook-1.parallel true &&
829+
test_config hook.hook-2.event test-hook &&
830+
test_config hook.hook-2.command \
831+
"$(sentinel_detector sentinel hook.order)" &&
832+
test_config hook.hook-2.parallel true &&
833+
834+
# Global hook.jobs=1 (serial), but per-event override allows parallel.
835+
test_config hook.jobs 1 &&
836+
test_config hook.test-hook.jobs 2 &&
837+
838+
git hook run test-hook >out 2>err &&
839+
echo parallel >expect &&
840+
test_cmp expect hook.order
841+
'
842+
843+
test_expect_success 'hook.<event>.jobs=1 forces serial even when hook.jobs>1' '
844+
test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
845+
test_config hook.hook-1.event test-hook &&
846+
test_config hook.hook-1.command \
847+
"touch sentinel.started; sleep 2; touch sentinel.done" &&
848+
test_config hook.hook-1.parallel true &&
849+
test_config hook.hook-2.event test-hook &&
850+
test_config hook.hook-2.command \
851+
"$(sentinel_detector sentinel hook.order)" &&
852+
test_config hook.hook-2.parallel true &&
853+
854+
# Global hook.jobs=4 allows parallel, but per-event override forces serial.
855+
test_config hook.jobs 4 &&
856+
test_config hook.test-hook.jobs 1 &&
857+
858+
git hook run test-hook >out 2>err &&
859+
echo serial >expect &&
860+
test_cmp expect hook.order
861+
'
862+
863+
test_expect_success 'hook.<event>.jobs still requires hook.<name>.parallel=true' '
864+
test_when_finished "rm -f sentinel.started sentinel.done hook.order" &&
865+
test_config hook.hook-1.event test-hook &&
866+
test_config hook.hook-1.command \
867+
"touch sentinel.started; sleep 2; touch sentinel.done" &&
868+
# hook-1 intentionally has no parallel=true
869+
test_config hook.hook-2.event test-hook &&
870+
test_config hook.hook-2.command \
871+
"$(sentinel_detector sentinel hook.order)" &&
872+
# hook-2 also has no parallel=true
873+
874+
# Per-event jobs=2 but no hook has parallel=true: must still run serially.
875+
test_config hook.test-hook.jobs 2 &&
876+
877+
git hook run test-hook >out 2>err &&
878+
echo serial >expect &&
879+
test_cmp expect hook.order
880+
'
881+
823882
test_done

0 commit comments

Comments
 (0)