found in the hooks directory do not need to, and run in parallel when
the effective job count is greater than 1. See linkgit:git-hook[1].
+hook.<event>.jobs::
+ Specifies how many hooks can be run simultaneously for the `<event>`
+ hook event (e.g. `hook.post-receive.jobs = 4`). Overrides `hook.jobs`
+ for this specific event. The same parallelism restrictions apply: this
+ setting has no effect unless all configured hooks for the event have
+ `hook.<friendly-name>.parallel` set to `true`. Must be a positive int,
+ zero is rejected with a warning. See linkgit:git-hook[1].
++
+Note on naming: although this key resembles `hook.<friendly-name>.*`
+(a per-hook setting), `<event>` must be the event name, not a hook
+friendly name. The key component is stored literally and looked up by
+event name at runtime with no translation between the two namespaces.
+A key like `hook.my-hook.jobs` is stored under `"my-hook"` but the
+lookup at runtime uses the event name (e.g. `"post-receive"`), so
+`hook.my-hook.jobs` is silently ignored even when `my-hook` is
+registered for that event. Use `hook.post-receive.jobs` or any other
+valid event name when setting `hook.<event>.jobs`.
+
hook.jobs::
Specifies how many hooks can be run simultaneously during parallelized
hook execution. If unspecified, defaults to 1 (serial execution).
+ Can be overridden on a per-event basis with `hook.<event>.jobs`.
Some hooks always run sequentially regardless of this setting because
they operate on shared data and cannot safely be parallelized:
+
* event_hooks: event-name to list of friendly-names map.
* disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.
* parallel_hooks: friendly-name to parallel flag.
+ * event_jobs: event-name to per-event jobs count (stored as uintptr_t, NULL == unset).
* jobs: value of the global hook.jobs key. Defaults to 0 if unset (stored in r->hook_jobs).
*/
struct hook_all_config_cb {
struct strmap event_hooks;
struct string_list disabled_hooks;
struct strmap parallel_hooks;
+ struct strmap event_jobs;
unsigned int jobs;
};
warning(_("hook.%s.parallel must be a boolean,"
" ignoring: '%s'"),
hook_name, value);
+ } else if (!strcmp(subkey, "jobs")) {
+ unsigned int v;
+ if (!git_parse_uint(value, &v))
+ warning(_("hook.%s.jobs must be a positive integer,"
+ " ignoring: '%s'"),
+ hook_name, value);
+ else if (!v)
+ warning(_("hook.%s.jobs must be positive,"
+ " ignoring: 0"), hook_name);
+ else
+ strmap_put(&data->event_jobs, hook_name,
+ (void *)(uintptr_t)v);
}
free(hook_name);
strmap_init(&cb_data.event_hooks);
string_list_init_dup(&cb_data.disabled_hooks);
strmap_init(&cb_data.parallel_hooks);
+ strmap_init(&cb_data.event_jobs);
/* Parse all configs in one run, capturing hook.* including hook.jobs. */
repo_config(r, hook_config_lookup_all, &cb_data);
strmap_put(cache, e->key, hooks);
}
- if (r)
+ if (r) {
r->hook_jobs = cb_data.jobs;
+ r->event_jobs = cb_data.event_jobs;
+ }
strmap_clear(&cb_data.commands, 1);
strmap_clear(&cb_data.parallel_hooks, 0); /* values are uintptr_t, not heap ptrs */
/* Determine how many jobs to use for hook execution. */
static unsigned int get_hook_jobs(struct repository *r,
struct run_hooks_opt *options,
+ const char *hook_name,
struct string_list *hook_list)
{
/*
*/
options->jobs = 1;
if (r) {
- if (r->gitdir && r->hook_config_cache && r->hook_jobs)
- options->jobs = r->hook_jobs;
- else
+ if (r->gitdir && r->hook_config_cache) {
+ void *event_jobs;
+
+ if (r->hook_jobs)
+ options->jobs = r->hook_jobs;
+
+ event_jobs = strmap_get(&r->event_jobs, hook_name);
+ if (event_jobs)
+ options->jobs = (unsigned int)(uintptr_t)event_jobs;
+ } else {
+ unsigned int event_jobs;
+ char *key;
+
repo_config_get_uint(r, "hook.jobs", &options->jobs);
+
+ key = xstrfmt("hook.%s.jobs", hook_name);
+ if (!repo_config_get_uint(r, key, &event_jobs) && event_jobs)
+ options->jobs = event_jobs;
+ free(key);
+ }
}
/*
* Cap to serial any configured hook not marked as parallel = true.
* This enforces the parallel = false default, even for "traditional"
* hooks from the hookdir which cannot be marked parallel = true.
+ * The same restriction applies whether jobs came from hook.jobs or
+ * hook.<event>.jobs.
*/
for (size_t i = 0; i < hook_list->nr; i++) {
struct hook *h = hook_list->items[i].util;
.options = options,
};
int ret = 0;
- unsigned int jobs = get_hook_jobs(r, options, hook_list);
+ unsigned int jobs = get_hook_jobs(r, options, hook_name, hook_list);
const struct run_process_parallel_opts opts = {
.tr2_category = "hook",
.tr2_label = hook_name,
hook_cache_clear(repo->hook_config_cache);
FREE_AND_NULL(repo->hook_config_cache);
}
+ strmap_clear(&repo->event_jobs, 0); /* values are uintptr_t, not heap ptrs */
if (repo->promisor_remote_config) {
promisor_remote_clear(repo->promisor_remote_config);
/* Cached value of hook.jobs config (0 if unset, defaults to serial). */
unsigned int hook_jobs;
+ /* Cached map of event-name -> jobs count (as uintptr_t) from hook.<event>.jobs. */
+ struct strmap event_jobs;
+
/* Configurations related to promisor remotes. */
char *repository_format_partial_clone;
struct promisor_remote_config *promisor_remote_config;
test_cmp expect hook.order
'
+test_expect_success 'hook.<event>.jobs overrides hook.jobs for that event' '
+ 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" &&
+ test_config hook.hook-1.parallel true &&
+ 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 &&
+
+ # Global hook.jobs=1 (serial), but per-event override allows parallel.
+ test_config hook.jobs 1 &&
+ test_config hook.test-hook.jobs 2 &&
+
+ git hook run --allow-unknown-hook-name test-hook >out 2>err &&
+ echo parallel >expect &&
+ test_cmp expect hook.order
+'
+
+test_expect_success 'hook.<event>.jobs=1 forces serial even when hook.jobs>1' '
+ 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" &&
+ test_config hook.hook-1.parallel true &&
+ 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 &&
+
+ # Global hook.jobs=4 allows parallel, but per-event override forces serial.
+ test_config hook.jobs 4 &&
+ test_config hook.test-hook.jobs 1 &&
+
+ git hook run --allow-unknown-hook-name test-hook >out 2>err &&
+ echo serial >expect &&
+ test_cmp expect hook.order
+'
+
+test_expect_success 'hook.<event>.jobs still requires hook.<name>.parallel=true' '
+ 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
+
+ # Per-event jobs=2 but no hook has parallel=true: must still run serially.
+ test_config hook.test-hook.jobs 2 &&
+
+ git hook run --allow-unknown-hook-name test-hook >out 2>err &&
+ echo serial >expect &&
+ test_cmp expect hook.order
+'
+
test_done