hook: allow disabling config hooks

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.<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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Adrian Ratiu committed Feb 19, 2026 at 00:23 UTC 1ecce722cdb9c42dd4c69e45e02cb850cd558ef2
3 files changed +59
Documentation/config/hook.adoc
+7
@@ -13,3 +13,10 @@ hook.<name>.event::
13 specified event, the associated `hook.<name>.command` is executed.
14 This is a multi-valued key. To run `hook.<name>` on multiple
15 events, specify the key more than once. See linkgit:git-hook[1].
16 +
17 +hook.<name>.enabled::
18 + Whether the hook `hook.<name>` is enabled. Defaults to `true`.
19 + Set to `false` to disable the hook without removing its
20 + configuration. This is particularly useful when a hook is defined
21 + in a system or global config file and needs to be disabled for a
22 + specific repository. See linkgit:git-hook[1].
hook.c
+20
@@ -164,6 +164,21 @@ static int hook_config_lookup_all(const char *key, const char *value,
164 char *old = strmap_put(&data->commands, hook_name,
165 xstrdup(value));
166 free(old);
167 + } else if (!strcmp(subkey, "enabled")) {
168 + switch (git_parse_maybe_bool(value)) {
169 + case 0: /* disabled */
170 + if (!unsorted_string_list_lookup(&data->disabled_hooks,
171 + hook_name))
172 + string_list_append(&data->disabled_hooks,
173 + hook_name);
174 + break;
175 + case 1: /* enabled: undo a prior disabled entry */
176 + unsorted_string_list_remove(&data->disabled_hooks,
177 + hook_name);
178 + break;
179 + default:
180 + break; /* ignore unrecognised values */
181 + }
182 }
183
184 free(hook_name);
@@ -216,6 +231,11 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
231 const char *hname = hook_names->items[i].string;
232 char *command;
233
234 + /* filter out disabled hooks */
235 + if (unsorted_string_list_lookup(&cb_data.disabled_hooks,
236 + hname))
237 + continue;
238 +
239 command = strmap_get(&cb_data.commands, hname);
240 if (!command)
241 die(_("'hook.%s.command' must be configured or "
t/t1800-hook.sh
+32
@@ -318,6 +318,38 @@ test_expect_success 'rejects hooks with no commands configured' '
318 test_grep "hook.broken.command" actual
319 '
320
321 +test_expect_success 'disabled hook is not run' '
322 + test_config hook.skipped.event "test-hook" &&
323 + test_config hook.skipped.command "echo \"Should not run\"" &&
324 + test_config hook.skipped.enabled false &&
325 +
326 + git hook run --ignore-missing test-hook 2>actual &&
327 + test_must_be_empty actual
328 +'
329 +
330 +test_expect_success 'disabled hook does not appear in git hook list' '
331 + test_config hook.active.event "pre-commit" &&
332 + test_config hook.active.command "echo active" &&
333 + test_config hook.inactive.event "pre-commit" &&
334 + test_config hook.inactive.command "echo inactive" &&
335 + test_config hook.inactive.enabled false &&
336 +
337 + git hook list pre-commit >actual &&
338 + test_grep "active" actual &&
339 + test_grep ! "inactive" actual
340 +'
341 +
342 +test_expect_success 'globally disabled hook can be re-enabled locally' '
343 + test_config_global hook.global-hook.event "test-hook" &&
344 + test_config_global hook.global-hook.command "echo \"global-hook ran\"" &&
345 + test_config_global hook.global-hook.enabled false &&
346 + test_config hook.global-hook.enabled true &&
347 +
348 + echo "global-hook ran" >expected &&
349 + git hook run test-hook 2>actual &&
350 + test_cmp expected actual
351 +'
352 +
353 test_expect_success 'git hook run a hook with a bad shebang' '
354 test_when_finished "rm -rf bad-hooks" &&
355 mkdir bad-hooks &&