git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v6 08/12] hook: add per-event jobs config

From
Adrian Ratiu <adrian.ratiu@collabora.com>
Date
Apr 4, 2026, 08:29 UTC
Message-ID
<20260404082934.173788-9-adrian.ratiu@collabora.com>
In-Reply-To
<20260404082934.173788-1-adrian.ratiu@collabora.com>

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>
---
 Documentation/config/hook.adoc | 19 +++++++++++
 hook.c                         | 46 +++++++++++++++++++++++---
 repository.c                   |  1 +
 repository.h                   |  3 ++
 t/t1800-hook.sh                | 59 ++++++++++++++++++++++++++++++++++
 5 files changed, 123 insertions(+), 5 deletions(-)
diff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc
index 6f60775c28..d4fa29d936 100644
--- a/Documentation/config/hook.adoc
+++ b/Documentation/config/hook.adoc
@@ -33,9 +33,28 @@ hook.<friendly-name>.parallel::
 	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:
 +
diff --git a/hook.c b/hook.c
index c0b71322cf..d98b011563 100644
--- a/hook.c
+++ b/hook.c
@@ -125,6 +125,7 @@ struct hook_config_cache_entry {
  * 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 {
@@ -132,6 +133,7 @@ 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;
 };
 
@@ -231,6 +233,18 @@ static int hook_config_lookup_all(const char *key, const char *value,
 			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);
@@ -276,6 +290,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
 	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);
@@ -323,8 +338,10 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
 		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 */
@@ -587,6 +604,7 @@ static void warn_non_parallel_hooks_override(unsigned int jobs,
 /* 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)
 {
 	/*
@@ -606,16 +624,34 @@ static unsigned int get_hook_jobs(struct repository *r,
 	 */
 	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;
@@ -642,7 +678,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
 		.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,
diff --git a/repository.c b/repository.c
index 192d6dc9c4..4030db4460 100644
--- a/repository.c
+++ b/repository.c
@@ -426,6 +426,7 @@ void repo_clear(struct repository *repo)
 		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);
diff --git a/repository.h b/repository.h
index 58e46853d0..6b67ec02e2 100644
--- a/repository.h
+++ b/repository.h
@@ -175,6 +175,9 @@ struct repository {
 	/* 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;
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index a1734fd628..ab2b52bec6 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -969,4 +969,63 @@ test_expect_success 'hook.jobs=2 is ignored for force-serial hooks (pre-commit)'
 	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
-- 
2.52.0.732.gb351b5166d.dirty
Previous: Adrian RatiuNext: Adrian Ratiu
Message 92 of 113 in “Run hooks in parallel”
  1. 0/4 Run hooks in parallelAdrian Ratiu, Feb 4, 2026
  2. 1/4 config: add a repo_config_get_uint() helperAdrian Ratiu, Feb 4, 2026
  3. 3/4 hook: introduce extensions.hookStdoutToStderrAdrian Ratiu, Feb 4, 2026
  4. 2/4 hook: allow parallel hook executionAdrian Ratiu, Feb 4, 2026
  5. Patrick SteinhardtFeb 11, 2026
  6. Adrian RatiuFeb 12, 2026
  7. 4/4 hook: allow runtime enabling extensions.hookStdoutToStderrAdrian Ratiu, Feb 4, 2026
  8. Phillip WoodFeb 12, 2026
  9. Adrian RatiuFeb 12, 2026
  10. Phillip WoodFeb 13, 2026
  11. Adrian RatiuFeb 13, 2026
  12. 00/10 Run hooks in parallelAdrian Ratiu, Feb 22, 2026
  13. 02/10 config: add a repo_config_get_uint() helperAdrian Ratiu, Feb 22, 2026
  14. 03/10 hook: refactor hook_config_cache from strmap to named structAdrian Ratiu, Feb 22, 2026
  15. 04/10 hook: parse the hook.jobs configAdrian Ratiu, Feb 22, 2026
  16. 06/10 hook: mark non-parallelizable hooksAdrian Ratiu, Feb 22, 2026
  17. 01/10 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Feb 22, 2026
  18. 05/10 hook: allow parallel hook executionAdrian Ratiu, Feb 22, 2026
  19. 08/10 hook: add per-event jobs configAdrian Ratiu, Feb 22, 2026
  20. 09/10 hook: introduce extensions.hookStdoutToStderrAdrian Ratiu, Feb 22, 2026
  21. 10/10 hook: allow runtime enabling extensions.hookStdoutToStderrAdrian Ratiu, Feb 22, 2026
  22. 07/10 hook: add -j/--jobs option to git hook runAdrian Ratiu, Feb 22, 2026
  23. 0/9 Run hooks in parallelAdrian Ratiu, Mar 9, 2026
  24. 2/9 config: add a repo_config_get_uint() helperAdrian Ratiu, Mar 9, 2026
  25. 1/9 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Mar 9, 2026
  26. Junio C HamanoMar 15, 2026
  27. Junio C HamanoMar 15, 2026
  28. 3/9 hook: parse the hook.jobs configAdrian Ratiu, Mar 9, 2026
  29. Junio C HamanoMar 15, 2026
  30. 5/9 hook: mark non-parallelizable hooksAdrian Ratiu, Mar 9, 2026
  31. Junio C HamanoMar 15, 2026
  32. Adrian RatiuMar 18, 2026
  33. 6/9 hook: add -j/--jobs option to git hook runAdrian Ratiu, Mar 9, 2026
  34. Junio C HamanoMar 15, 2026
  35. Adrian RatiuMar 18, 2026
  36. 4/9 hook: allow parallel hook executionAdrian Ratiu, Mar 9, 2026
  37. Junio C HamanoMar 15, 2026
  38. Adrian RatiuMar 18, 2026
  39. 7/9 hook: add per-event jobs configAdrian Ratiu, Mar 9, 2026
  40. Junio C HamanoMar 16, 2026
  41. Adrian RatiuMar 18, 2026
  42. 8/9 hook: introduce extensions.hookStdoutToStderrAdrian Ratiu, Mar 9, 2026
  43. Junio C HamanoMar 16, 2026
  44. Adrian RatiuMar 18, 2026
  45. 9/9 hook: allow runtime enabling extensions.hookStdoutToStderrAdrian Ratiu, Mar 9, 2026
  46. 0/9 Run hooks in parallelAdrian Ratiu, Mar 20, 2026
  47. 1/9 config: add a repo_config_get_uint() helperAdrian Ratiu, Mar 20, 2026
  48. 2/9 hook: parse the hook.jobs configAdrian Ratiu, Mar 20, 2026
  49. Patrick SteinhardtMar 24, 2026
  50. Adrian RatiuMar 24, 2026
  51. 4/9 hook: allow pre-push parallel executionAdrian Ratiu, Mar 20, 2026
  52. 3/9 hook: allow parallel hook executionAdrian Ratiu, Mar 20, 2026
  53. Patrick SteinhardtMar 24, 2026
  54. 5/9 hook: mark non-parallelizable hooksAdrian Ratiu, Mar 20, 2026
  55. 6/9 hook: add -j/--jobs option to git hook runAdrian Ratiu, Mar 20, 2026
  56. Patrick SteinhardtMar 24, 2026
  57. 8/9 hook: warn when hook.<friendly-name>.jobs is setAdrian Ratiu, Mar 20, 2026
  58. Patrick SteinhardtMar 24, 2026
  59. 7/9 hook: add per-event jobs configAdrian Ratiu, Mar 20, 2026
  60. Patrick SteinhardtMar 24, 2026
  61. 9/9 hook: add hook.<event>.enabled switchAdrian Ratiu, Mar 20, 2026
  62. Patrick SteinhardtMar 24, 2026
  63. Adrian RatiuMar 25, 2026
  64. Junio C HamanoMar 20, 2026
  65. Adrian RatiuMar 23, 2026
  66. Patrick SteinhardtMar 24, 2026
  67. 00/12 Run hooks in parallelAdrian Ratiu, Mar 26, 2026
  68. 01/12 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Mar 26, 2026
  69. 02/12 config: add a repo_config_get_uint() helperAdrian Ratiu, Mar 26, 2026
  70. 03/12 hook: parse the hook.jobs configAdrian Ratiu, Mar 26, 2026
  71. 06/12 hook: mark non-parallelizable hooksAdrian Ratiu, Mar 26, 2026
  72. 07/12 hook: add -j/--jobs option to git hook runAdrian Ratiu, Mar 26, 2026
  73. Patrick SteinhardtMar 27, 2026
  74. 05/12 hook: allow pre-push parallel executionAdrian Ratiu, Mar 26, 2026
  75. 08/12 hook: add per-event jobs configAdrian Ratiu, Mar 26, 2026
  76. 04/12 hook: allow parallel hook executionAdrian Ratiu, Mar 26, 2026
  77. 09/12 hook: warn when hook.<friendly-name>.jobs is setAdrian Ratiu, Mar 26, 2026
  78. Patrick SteinhardtMar 27, 2026
  79. 11/12 hook: add hook.<event>.enabled switchAdrian Ratiu, Mar 26, 2026
  80. 10/12 hook: move is_known_hook() to hook.c for wider useAdrian Ratiu, Mar 26, 2026
  81. Patrick SteinhardtMar 27, 2026
  82. Adrian RatiuMar 27, 2026
  83. 12/12 hook: allow hook.jobs=-1 to use all available CPU coresAdrian Ratiu, Mar 26, 2026
  84. 00/12 Run hooks in parallelAdrian Ratiu, Apr 4, 2026
  85. 02/12 config: add a repo_config_get_uint() helperAdrian Ratiu, Apr 4, 2026
  86. 01/12 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Apr 4, 2026
  87. 03/12 hook: parse the hook.jobs configAdrian Ratiu, Apr 4, 2026
  88. 04/12 hook: allow parallel hook executionAdrian Ratiu, Apr 4, 2026
  89. 05/12 hook: allow pre-push parallel executionAdrian Ratiu, Apr 4, 2026
  90. 06/12 hook: mark non-parallelizable hooksAdrian Ratiu, Apr 4, 2026
  91. 07/12 hook: add -j/--jobs option to git hook runAdrian Ratiu, Apr 4, 2026
  92. 08/12 hook: add per-event jobs configAdrian Ratiu, Apr 4, 2026
  93. 09/12 hook: warn when hook.<friendly-name>.jobs is setAdrian Ratiu, Apr 4, 2026
  94. 10/12 hook: move is_known_hook() to hook.c for wider useAdrian Ratiu, Apr 4, 2026
  95. 11/12 hook: add hook.<event>.enabled switchAdrian Ratiu, Apr 4, 2026
  96. 12/12 hook: allow hook.jobs=-1 to use all available CPU coresAdrian Ratiu, Apr 4, 2026
  97. Junio C HamanoApr 6, 2026
  98. Patrick SteinhardtApr 8, 2026
  99. Junio C HamanoApr 8, 2026
  100. 00/13 Run hooks in parallelAdrian Ratiu, Apr 10, 2026
  101. 01/13 repository: fix repo_init() memleak due to missing _clear()Adrian Ratiu, Apr 10, 2026
  102. 02/13 config: add a repo_config_get_uint() helperAdrian Ratiu, Apr 10, 2026
  103. 03/13 hook: parse the hook.jobs configAdrian Ratiu, Apr 10, 2026
  104. 04/13 hook: allow parallel hook executionAdrian Ratiu, Apr 10, 2026
  105. 05/13 hook: allow pre-push parallel executionAdrian Ratiu, Apr 10, 2026
  106. 06/13 hook: mark non-parallelizable hooksAdrian Ratiu, Apr 10, 2026
  107. 07/13 hook: add -j/--jobs option to git hook runAdrian Ratiu, Apr 10, 2026
  108. 08/13 hook: add per-event jobs configAdrian Ratiu, Apr 10, 2026
  109. 09/13 hook: warn when hook.<friendly-name>.jobs is setAdrian Ratiu, Apr 10, 2026
  110. 10/13 hook: move is_known_hook() to hook.c for wider useAdrian Ratiu, Apr 10, 2026
  111. 11/13 hook: add hook.<event>.enabled switchAdrian Ratiu, Apr 10, 2026
  112. 12/13 hook: allow hook.jobs=-1 to use all available CPU coresAdrian Ratiu, Apr 10, 2026
  113. 13/13 t1800: test SIGPIPE with parallel hooksAdrian Ratiu, Apr 10, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.