@@ -116,19 +116,22 @@ struct hook_config_cache_entry {
116116 char * command ;
117117 enum config_scope scope ;
118118 bool disabled ;
119+ bool parallel ;
119120};
120121
121122/*
122123 * Callback struct to collect all hook.* keys in a single config pass.
123124 * commands: friendly-name to command map.
124125 * event_hooks: event-name to list of friendly-names map.
125126 * disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.
127+ * parallel_hooks: friendly-name to parallel flag.
126128 * jobs: value of the global hook.jobs key. Defaults to 0 if unset (stored in r->hook_jobs).
127129 */
128130struct hook_all_config_cb {
129131 struct strmap commands ;
130132 struct strmap event_hooks ;
131133 struct string_list disabled_hooks ;
134+ struct strmap parallel_hooks ;
132135 unsigned int jobs ;
133136};
134137
@@ -219,6 +222,15 @@ static int hook_config_lookup_all(const char *key, const char *value,
219222 default :
220223 break ; /* ignore unrecognised values */
221224 }
225+ } else if (!strcmp (subkey , "parallel" )) {
226+ int v = git_parse_maybe_bool (value );
227+ if (v >= 0 )
228+ strmap_put (& data -> parallel_hooks , hook_name ,
229+ (void * )(uintptr_t )v );
230+ else
231+ warning (_ ("hook.%s.parallel must be a boolean,"
232+ " ignoring: '%s'" ),
233+ hook_name , value );
222234 }
223235
224236 free (hook_name );
@@ -263,6 +275,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
263275 strmap_init (& cb_data .commands );
264276 strmap_init (& cb_data .event_hooks );
265277 string_list_init_dup (& cb_data .disabled_hooks );
278+ strmap_init (& cb_data .parallel_hooks );
266279
267280 /* Parse all configs in one run, capturing hook.* including hook.jobs. */
268281 repo_config (r , hook_config_lookup_all , & cb_data );
@@ -282,6 +295,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
282295 struct hook_config_cache_entry * entry ;
283296 char * command ;
284297
298+ bool is_par = !!strmap_get (& cb_data .parallel_hooks , hname );
285299 bool is_disabled =
286300 !!unsorted_string_list_lookup (
287301 & cb_data .disabled_hooks , hname );
@@ -302,6 +316,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
302316 entry -> command = xstrdup_or_null (command );
303317 entry -> scope = scope ;
304318 entry -> disabled = is_disabled ;
319+ entry -> parallel = is_par ;
305320 string_list_append (hooks , hname )-> util = entry ;
306321 }
307322
@@ -312,6 +327,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
312327 r -> hook_jobs = cb_data .jobs ;
313328
314329 strmap_clear (& cb_data .commands , 1 );
330+ strmap_clear (& cb_data .parallel_hooks , 0 ); /* values are uintptr_t, not heap ptrs */
315331 string_list_clear (& cb_data .disabled_hooks , 0 );
316332 strmap_for_each_entry (& cb_data .event_hooks , & iter , e ) {
317333 string_list_clear (e -> value , 0 );
@@ -389,6 +405,7 @@ static void list_hooks_add_configured(struct repository *r,
389405 entry -> command ? xstrdup (entry -> command ) : NULL ;
390406 hook -> u .configured .scope = entry -> scope ;
391407 hook -> u .configured .disabled = entry -> disabled ;
408+ hook -> parallel = entry -> parallel ;
392409
393410 string_list_append (list , friendly_name )-> util = hook ;
394411 }
@@ -538,21 +555,75 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
538555 strvec_clear (& options -> args );
539556}
540557
558+ /* Determine how many jobs to use for hook execution. */
559+ static unsigned int get_hook_jobs (struct repository * r ,
560+ struct run_hooks_opt * options ,
561+ struct string_list * hook_list )
562+ {
563+ /*
564+ * Hooks needing separate output streams must run sequentially.
565+ * Next commit will allow parallelizing these as well.
566+ */
567+ if (!options -> stdout_to_stderr )
568+ return 1 ;
569+
570+ /*
571+ * An explicit job count overrides everything else: this covers both
572+ * FORCE_SERIAL callers (for hooks that must never run in parallel)
573+ * and the -j flag from the CLI. The CLI override is intentional: users
574+ * may want to serialize hooks declared parallel or to parallelize more
575+ * aggressively than the default.
576+ */
577+ if (options -> jobs )
578+ return options -> jobs ;
579+
580+ /*
581+ * Use hook.jobs from the already-parsed config cache (in-repo), or
582+ * fallback to a direct config lookup (out-of-repo).
583+ * Default to 1 (serial execution) on failure.
584+ */
585+ options -> jobs = 1 ;
586+ if (r ) {
587+ if (r -> gitdir && r -> hook_config_cache && r -> hook_jobs )
588+ options -> jobs = r -> hook_jobs ;
589+ else
590+ repo_config_get_uint (r , "hook.jobs" , & options -> jobs );
591+ }
592+
593+ /*
594+ * Cap to serial any configured hook not marked as parallel = true.
595+ * This enforces the parallel = false default, even for "traditional"
596+ * hooks from the hookdir which cannot be marked parallel = true.
597+ */
598+ for (size_t i = 0 ; i < hook_list -> nr ; i ++ ) {
599+ struct hook * h = hook_list -> items [i ].util ;
600+ if (h -> kind == HOOK_CONFIGURED && !h -> parallel ) {
601+ options -> jobs = 1 ;
602+ break ;
603+ }
604+ }
605+
606+ return options -> jobs ;
607+ }
608+
541609int run_hooks_opt (struct repository * r , const char * hook_name ,
542610 struct run_hooks_opt * options )
543611{
612+ struct string_list * hook_list = list_hooks (r , hook_name , options );
544613 struct hook_cb_data cb_data = {
545614 .rc = 0 ,
546615 .hook_name = hook_name ,
616+ .hook_command_list = hook_list ,
547617 .options = options ,
548618 };
549619 int ret = 0 ;
620+ unsigned int jobs = get_hook_jobs (r , options , hook_list );
550621 const struct run_process_parallel_opts opts = {
551622 .tr2_category = "hook" ,
552623 .tr2_label = hook_name ,
553624
554- .processes = options -> jobs ,
555- .ungroup = options -> jobs == 1 ,
625+ .processes = jobs ,
626+ .ungroup = jobs == 1 ,
556627
557628 .get_next_task = pick_next_hook ,
558629 .start_failure = notify_start_failure ,
@@ -568,9 +639,6 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
568639 if (options -> path_to_stdin && options -> feed_pipe )
569640 BUG ("options path_to_stdin and feed_pipe are mutually exclusive" );
570641
571- if (!options -> jobs )
572- BUG ("run_hooks_opt must be called with options.jobs >= 1" );
573-
574642 /*
575643 * Ensure cb_data copy and free functions are either provided together,
576644 * or neither one is provided.
@@ -581,7 +649,6 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
581649 if (options -> invoked_hook )
582650 * options -> invoked_hook = 0 ;
583651
584- cb_data .hook_command_list = list_hooks (r , hook_name , options );
585652 if (!cb_data .hook_command_list -> nr ) {
586653 if (options -> error_if_missing )
587654 ret = error ("cannot find a hook named %s" , hook_name );
0 commit comments