Add a hook.<event>.enabled config key that disables all hooks for
a given event, when set to false, acting as a high-level switch
above the existing per-hook hook.<friendly-name>.enabled.
Event-disabled hooks are shown in "git hook list" with an
"event-disabled" tab-separated prefix before the name:
$ git hook list test-hook
event-disabled hook-1
event-disabled hook-2
With --show-scope:
$ git hook list --show-scope test-hook
local event-disabled hook-1
When a hook is both per-hook disabled and event-disabled, only
"event-disabled" is shown: the event-level switch is the more
relevant piece of information, and the per-hook "disabled" status
will surface once the event is re-enabled.
Using an event name as a friendly-name (e.g. hook.<event>.enabled)
can cause ambiguity, so a fatal error is issued when using a known
event name and a warning is issued for unknown event name, since
a collision cannot be detected with certainty for unknown events.
Suggested-by: Patrick Steinhardt <ps@pks.im>
Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Adrian Ratiu committedApr 10, 2026 at 12:06 UTCdcfb5af67e7d7156c4d1ede66de18088c990356c
7 files changed+165-11
Documentation/config/hook.adoc
+20
index d4fa29d936..e0db3afa19 100644--- a/Documentation/config/hook.adoc+++ b/Documentation/config/hook.adoc@@ -15,6 +15,12 @@ hook.<friendly-name>.event:: events, specify the key more than once. An empty value resets the list of events, clearing any previously defined events for `hook.<friendly-name>`. See linkgit:git-hook[1].+++The `<friendly-name>` must not be the same as a known hook event name+(e.g. do not use `hook.pre-commit.event`). Using a known event name as+a friendly-name is a fatal error because it creates an ambiguity with+`hook.<event>.enabled` and `hook.<event>.jobs`. For unknown event names,+a warning is issued when `<friendly-name>` matches the event value. hook.<friendly-name>.enabled:: Whether the hook `hook.<friendly-name>` is enabled. Defaults to `true`.@@ -33,6 +39,20 @@ 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>.enabled::+ Switch to enable or disable all hooks for the `<event>` hook event.+ When set to `false`, no hooks fire for that event, regardless of any+ per-hook `hook.<friendly-name>.enabled` settings. Defaults to `true`.+ See linkgit:git-hook[1].+++Note on naming: `<event>` must be the event name (e.g. `pre-commit`),+not a hook friendly-name. Since using a known event name as a+friendly-name is disallowed (see `hook.<friendly-name>.event` above),+there is no ambiguity between event-level and per-hook `.enabled`+settings for known events. For unknown events, if a friendly-name+matches the event name despite the warning, `.enabled` is treated+as per-hook only.+ 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`
builtin/hook.c
+14-6
index 1839412dca..8e47e22e2a 100644--- a/builtin/hook.c+++ b/builtin/hook.c@@ -87,14 +87,22 @@ static int list(int argc, const char **argv, const char *prefix, const char *name = h->u.configured.friendly_name; const char *scope = show_scope ? config_scope_name(h->u.configured.scope) : NULL;+ /*+ * Show the most relevant disable reason. Event-level+ * takes precedence: if the whole event is off, that+ * is what the user needs to know. The per-hook+ * "disabled" surfaces once the event is re-enabled.+ */+ const char *disability =+ h->u.configured.event_disabled ? "event-disabled\t" :+ h->u.configured.disabled ? "disabled\t" :+ ""; if (scope)- printf("%s\t%s%s%c", scope,- h->u.configured.disabled ? "disabled\t" : "",- name, line_terminator);+ printf("%s\t%s%s%c", scope, disability, name,+ line_terminator); else- printf("%s%s%c",- h->u.configured.disabled ? "disabled\t" : "",- name, line_terminator);+ printf("%s%s%c", disability, name,+ line_terminator); break; } default:
hook.c
+42-5
index 19076f8f2b..bc990d4ed4 100644--- a/hook.c+++ b/hook.c@@ -133,7 +133,9 @@ struct hook_config_cache_entry { * Callback struct to collect all hook.* keys in a single config pass. * commands: friendly-name to command map. * event_hooks: event-name to list of friendly-names map.- * disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.+ * disabled_hooks: set of all names with hook.<name>.enabled = false; after+ * parsing, names that are not friendly-names become event-level+ * disables stored in r->disabled_events. This collects all. * 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).@@ -189,8 +191,21 @@ static int hook_config_lookup_all(const char *key, const char *value, strmap_for_each_entry(&data->event_hooks, &iter, e) unsorted_string_list_remove(e->value, hook_name, 0); } else {- struct string_list *hooks =- strmap_get(&data->event_hooks, value);+ struct string_list *hooks;++ if (is_known_hook(hook_name))+ die(_("hook friendly-name '%s' collides with "+ "a known event name; please choose a "+ "different friendly-name"),+ hook_name);++ if (!strcmp(hook_name, value))+ warning(_("hook friendly-name '%s' is the "+ "same as its event; this may cause "+ "ambiguity with hook.%s.enabled"),+ hook_name, hook_name);++ hooks = strmap_get(&data->event_hooks, value); if (!hooks) { CALLOC_ARRAY(hooks, 1);@@ -345,6 +360,22 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache) warn_jobs_on_friendly_names(&cb_data);+ /*+ * Populate disabled_events: names in disabled_hooks that are not+ * friendly-names are event-level switches (hook.<event>.enabled = false).+ * Names that are friendly-names are already handled per-hook via the+ * hook_config_cache_entry.disabled flag below.+ */+ if (r) {+ string_list_clear(&r->disabled_events, 0);+ string_list_init_dup(&r->disabled_events);+ for (size_t i = 0; i < cb_data.disabled_hooks.nr; i++) {+ const char *n = cb_data.disabled_hooks.items[i].string;+ if (!is_friendly_name(&cb_data, n))+ string_list_append(&r->disabled_events, n);+ }+ }+ /* Construct the cache from parsed configs. */ strmap_for_each_entry(&cb_data.event_hooks, &iter, e) { struct string_list *hook_names = e->value;@@ -446,6 +477,8 @@ static void list_hooks_add_configured(struct repository *r, { struct strmap *cache = get_hook_config_cache(r); struct string_list *configured_hooks = strmap_get(cache, hookname);+ bool event_is_disabled = r ? !!unsorted_string_list_lookup(&r->disabled_events,+ hookname) : 0; /* Iterate through configured hooks and initialize internal states */ for (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {@@ -472,6 +505,7 @@ static void list_hooks_add_configured(struct repository *r, entry->command ? xstrdup(entry->command) : NULL; hook->u.configured.scope = entry->scope; hook->u.configured.disabled = entry->disabled;+ hook->u.configured.event_disabled = event_is_disabled; hook->parallel = entry->parallel; string_list_append(list, friendly_name)->util = hook;@@ -484,6 +518,8 @@ static void list_hooks_add_configured(struct repository *r, if (!r || !r->gitdir) { hook_cache_clear(cache); free(cache);+ if (r)+ string_list_clear(&r->disabled_events, 0); } }@@ -515,7 +551,7 @@ int hook_exists(struct repository *r, const char *name) for (size_t i = 0; i < hooks->nr; i++) { struct hook *h = hooks->items[i].util; if (h->kind == HOOK_TRADITIONAL ||- !h->u.configured.disabled) {+ (!h->u.configured.disabled && !h->u.configured.event_disabled)) { exists = 1; break; }@@ -538,7 +574,8 @@ static int pick_next_hook(struct child_process *cp, if (hook_cb->hook_to_run_index >= hook_list->nr) return 0; h = hook_list->items[hook_cb->hook_to_run_index++].util;- } while (h->kind == HOOK_CONFIGURED && h->u.configured.disabled);+ } while (h->kind == HOOK_CONFIGURED &&+ (h->u.configured.disabled || h->u.configured.event_disabled)); cp->no_stdin = 1; strvec_pushv(&cp->env, hook_cb->options->env.v);