[PATCH v3 06/12] hook: allow disabling config hooks
- From
Adrian Ratiu <adrian.ratiu@collabora.com>
- Date
- Mar 1, 2026, 18:44 UTC
- Message-ID
- <20260301184500.1488433-7-adrian.ratiu@collabora.com>
- In-Reply-To
- <20260301184500.1488433-1-adrian.ratiu@collabora.com>
Hooks specified via configs are always enabled, however users might want to disable them without removing from the config, like locally disabling a global hook.
Add a hook.<friendly-name>.enabled config which defaults to true and can be optionally set for each configured hook.
Suggested-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> --- Documentation/config/hook.adoc | 7 +++++++ hook.c | 24 ++++++++++++++++++++++++ t/t1800-hook.sh | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+)
diff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc index 7ac1b079cc..4bbda5636d 100644 --- a/Documentation/config/hook.adoc +++ b/Documentation/config/hook.adoc @@ -13,3 +13,10 @@ hook.<friendly-name>.event:: specified event, the associated `hook.<friendly-name>.command` is executed. This is a multi-valued key. To run `hook.<friendly-name>` on multiple events, specify the key more than once. See linkgit:git-hook[1]. + +hook.<friendly-name>.enabled:: + Whether the hook `hook.<friendly-name>` is enabled. Defaults to `true`. + Set to `false` to disable the hook without removing its + configuration. This is particularly useful when a hook is defined + in a system or global config file and needs to be disabled for a + specific repository. See linkgit:git-hook[1]. diff --git a/hook.c b/hook.c index b67ec4ff22..24eb330cac 100644 --- a/hook.c +++ b/hook.c @@ -105,10 +105,12 @@ static void list_hooks_add_default(struct repository *r, const char *hookname, * 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. */ struct hook_all_config_cb { struct strmap commands; struct strmap event_hooks; + struct string_list disabled_hooks; }; /* repo_config() callback that collects all hook.* configuration in one pass. */ @@ -148,6 +150,21 @@ static int hook_config_lookup_all(const char *key, const char *value, char *old = strmap_put(&data->commands, hook_name, xstrdup(value)); free(old); + } else if (!strcmp(subkey, "enabled")) { + switch (git_parse_maybe_bool(value)) { + case 0: /* disabled */ + if (!unsorted_string_list_lookup(&data->disabled_hooks, + hook_name)) + string_list_append(&data->disabled_hooks, + hook_name); + break; + case 1: /* enabled: undo a prior disabled entry */ + unsorted_string_list_remove(&data->disabled_hooks, + hook_name); + break; + default: + break; /* ignore unrecognised values */ + } } free(hook_name); @@ -184,6 +201,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache) strmap_init(&cb_data.commands); strmap_init(&cb_data.event_hooks); + string_list_init_dup(&cb_data.disabled_hooks); /* Parse all configs in one run. */ repo_config(r, hook_config_lookup_all, &cb_data); @@ -200,6 +218,11 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache) const char *hname = hook_names->items[i].string; char *command; + /* filter out disabled hooks */ + if (unsorted_string_list_lookup(&cb_data.disabled_hooks, + hname)) + continue; + command = strmap_get(&cb_data.commands, hname); if (!command) die(_("'hook.%s.command' must be configured or " @@ -215,6 +238,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache) } strmap_clear(&cb_data.commands, 1); + string_list_clear(&cb_data.disabled_hooks, 0); strmap_for_each_entry(&cb_data.event_hooks, &iter, e) { string_list_clear(e->value, 0); free(e->value); diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh index 3a95cfe16d..fb8cfc8137 100755 --- a/t/t1800-hook.sh +++ b/t/t1800-hook.sh @@ -343,6 +343,38 @@ test_expect_success 'rejects hooks with no commands configured' ' test_grep "hook.broken.command" actual ' +test_expect_success 'disabled hook is not run' ' + test_config hook.skipped.event "test-hook" && + test_config hook.skipped.command "echo \"Should not run\"" && + test_config hook.skipped.enabled false && + + git hook run --ignore-missing test-hook 2>actual && + test_must_be_empty actual +' + +test_expect_success 'disabled hook does not appear in git hook list' ' + test_config hook.active.event "pre-commit" && + test_config hook.active.command "echo active" && + test_config hook.inactive.event "pre-commit" && + test_config hook.inactive.command "echo inactive" && + test_config hook.inactive.enabled false && + + git hook list pre-commit >actual && + test_grep "active" actual && + test_grep ! "inactive" actual +' + +test_expect_success 'globally disabled hook can be re-enabled locally' ' + test_config_global hook.global-hook.event "test-hook" && + test_config_global hook.global-hook.command "echo \"global-hook ran\"" && + test_config_global hook.global-hook.enabled false && + test_config hook.global-hook.enabled true && + + echo "global-hook ran" >expected && + git hook run test-hook 2>actual && + test_cmp expected actual +' + test_expect_success 'git hook run a hook with a bad shebang' ' test_when_finished "rm -rf bad-hooks" && mkdir bad-hooks &&
-- 2.52.0.732.gb351b5166d.dirty