From: Adrian Ratiu <adrian.ratiu@collabora.com>
To: git@vger.kernel.org
Cc: Emily Shaffer <emilyshaffer@google.com>,
Junio C Hamano <gitster@pobox.com>,
Patrick Steinhardt <ps@pks.im>,
"brian m . carlson" <sandals@crustytoothpaste.net>,
Adrian Ratiu <adrian.ratiu@collabora.com>
Subject: [PATCH v2 10/10] hook: show disabled hooks in "git hook list"
Date: Fri, 20 Mar 2026 13:52:11 +0200 [thread overview]
Message-ID: <20260320115211.177351-11-adrian.ratiu@collabora.com> (raw)
In-Reply-To: <20260320115211.177351-1-adrian.ratiu@collabora.com>
Disabled hooks were filtered out of the cache entirely, making them
invisible to "git hook list". Keep them in the cache with a new
"disabled" flag which is propagated to the respective struct hook.
"git hook list" now shows disabled hooks as tab-separated columns,
with the status as a prefix before the name (like scope with
--show-scope). With --show-scope it looks like:
$ git hook list --show-scope pre-commit
global linter
local disabled no-leaks
hook from hookdir
A disabled hook without a command issues a warning instead of the
fatal "hook.X.command must be configured" error. We could also throw
an error, however it seemd a bit excessive to me in this case.
Suggested-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>
---
builtin/hook.c | 20 ++++++++++--------
hook.c | 54 +++++++++++++++++++++++++++++++++----------------
hook.h | 1 +
t/t1800-hook.sh | 33 +++++++++++++++++++++++++++---
4 files changed, 80 insertions(+), 28 deletions(-)
diff --git a/builtin/hook.c b/builtin/hook.c
index 4cc65a0dc5..f671e7f91a 100644
--- a/builtin/hook.c
+++ b/builtin/hook.c
@@ -72,16 +72,20 @@ static int list(int argc, const char **argv, const char *prefix,
case HOOK_TRADITIONAL:
printf("%s%c", _("hook from hookdir"), line_terminator);
break;
- case HOOK_CONFIGURED:
- if (show_scope)
- printf("%s\t%s%c",
- config_scope_name(h->u.configured.scope),
- h->u.configured.friendly_name,
- line_terminator);
+ case HOOK_CONFIGURED: {
+ const char *name = h->u.configured.friendly_name;
+ const char *scope = show_scope ?
+ config_scope_name(h->u.configured.scope) : NULL;
+ if (scope)
+ printf("%s\t%s%s%c", scope,
+ h->u.configured.disabled ? "disabled\t" : "",
+ name, line_terminator);
else
- printf("%s%c", h->u.configured.friendly_name,
- line_terminator);
+ printf("%s%s%c",
+ h->u.configured.disabled ? "disabled\t" : "",
+ name, line_terminator);
break;
+ }
default:
BUG("unknown hook kind");
}
diff --git a/hook.c b/hook.c
index aa08c38c27..0e09b9a2bb 100644
--- a/hook.c
+++ b/hook.c
@@ -119,6 +119,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,
struct hook_config_cache_entry {
char *command;
enum config_scope scope;
+ unsigned int disabled:1;
};
/*
@@ -217,8 +218,10 @@ static int hook_config_lookup_all(const char *key, const char *value,
* every item's string is the hook's friendly-name and its util pointer is
* the corresponding command string. Both strings are owned by the map.
*
- * Disabled hooks and hooks missing a command are already filtered out at
- * parse time, so callers can iterate the list directly.
+ * Disabled hooks are kept in the cache with entry->disabled set, so that
+ * "git hook list" can display them. A non-disabled hook missing a command
+ * is fatal; a disabled hook missing a command emits a warning and is kept
+ * in the cache with entry->command = NULL.
*/
void hook_cache_clear(struct strmap *cache)
{
@@ -267,21 +270,26 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
struct hook_config_cache_entry *entry;
char *command;
- /* filter out disabled hooks */
- if (unsorted_string_list_lookup(&cb_data.disabled_hooks,
- hname))
- continue;
+ int is_disabled =
+ !!unsorted_string_list_lookup(
+ &cb_data.disabled_hooks, hname);
command = strmap_get(&cb_data.commands, hname);
- if (!command)
- die(_("'hook.%s.command' must be configured or "
- "'hook.%s.event' must be removed;"
- " aborting."), hname, hname);
+ if (!command) {
+ if (is_disabled)
+ warning(_("disabled hook '%s' has no "
+ "command configured"), hname);
+ else
+ die(_("'hook.%s.command' must be configured or "
+ "'hook.%s.event' must be removed;"
+ " aborting."), hname, hname);
+ }
/* util stores a cache entry; owned by the cache. */
CALLOC_ARRAY(entry, 1);
- entry->command = xstrdup(command);
+ entry->command = xstrdup_or_null(command);
entry->scope = scope;
+ entry->disabled = is_disabled;
string_list_append(hooks, hname)->util = entry;
}
@@ -361,8 +369,10 @@ static void list_hooks_add_configured(struct repository *r,
hook->kind = HOOK_CONFIGURED;
hook->u.configured.friendly_name = xstrdup(friendly_name);
- hook->u.configured.command = xstrdup(entry->command);
+ hook->u.configured.command =
+ entry->command ? xstrdup(entry->command) : NULL;
hook->u.configured.scope = entry->scope;
+ hook->u.configured.disabled = entry->disabled;
string_list_append(list, friendly_name)->util = hook;
}
@@ -400,7 +410,16 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,
int hook_exists(struct repository *r, const char *name)
{
struct string_list *hooks = list_hooks(r, name, NULL);
- int exists = hooks->nr > 0;
+ int exists = 0;
+
+ 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) {
+ exists = 1;
+ break;
+ }
+ }
string_list_clear_func(hooks, hook_free);
free(hooks);
return exists;
@@ -415,10 +434,11 @@ static int pick_next_hook(struct child_process *cp,
struct string_list *hook_list = hook_cb->hook_command_list;
struct hook *h;
- if (hook_cb->hook_to_run_index >= hook_list->nr)
- return 0;
-
- h = hook_list->items[hook_cb->hook_to_run_index++].util;
+ do {
+ 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);
cp->no_stdin = 1;
strvec_pushv(&cp->env, hook_cb->options->env.v);
diff --git a/hook.h b/hook.h
index 92e9faf9bb..7c8c3d471e 100644
--- a/hook.h
+++ b/hook.h
@@ -31,6 +31,7 @@ struct hook {
const char *friendly_name;
const char *command;
enum config_scope scope;
+ unsigned int disabled:1;
} configured;
} u;
diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh
index 22cca15fda..6f6fe88bea 100755
--- a/t/t1800-hook.sh
+++ b/t/t1800-hook.sh
@@ -357,7 +357,15 @@ test_expect_success 'disabled hook is not run' '
test_must_be_empty actual
'
-test_expect_success 'disabled hook does not appear in git hook list' '
+test_expect_success 'disabled hook with no command warns' '
+ test_config hook.nocommand.event "pre-commit" &&
+ test_config hook.nocommand.enabled false &&
+
+ git hook list pre-commit 2>actual &&
+ test_grep "disabled hook.*nocommand.*no command configured" actual
+'
+
+test_expect_success 'disabled hook appears as disabled 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" &&
@@ -365,8 +373,27 @@ test_expect_success 'disabled hook does not appear in git hook list' '
test_config hook.inactive.enabled false &&
git hook list pre-commit >actual &&
- test_grep "active" actual &&
- test_grep ! "inactive" actual
+ test_grep "^active$" actual &&
+ test_grep "^disabled inactive$" actual
+'
+
+test_expect_success 'disabled hook shows scope with --show-scope' '
+ test_config hook.myhook.event "pre-commit" &&
+ test_config hook.myhook.command "echo hi" &&
+ test_config hook.myhook.enabled false &&
+
+ git hook list --show-scope pre-commit >actual &&
+ test_grep "^local disabled myhook$" actual
+'
+
+test_expect_success 'disabled configured hook is not reported as existing by hook_exists' '
+ test_when_finished "rm -f git-bugreport-hook-exists-test.txt" &&
+ test_config hook.linter.event "pre-commit" &&
+ test_config hook.linter.command "echo lint" &&
+ test_config hook.linter.enabled false &&
+
+ git bugreport -s hook-exists-test &&
+ test_grep ! "pre-commit" git-bugreport-hook-exists-test.txt
'
test_expect_success 'globally disabled hook can be re-enabled locally' '
--
2.52.0.732.gb351b5166d.dirty
next prev parent reply other threads:[~2026-03-20 11:53 UTC|newest]
Thread overview: 71+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-09 0:54 [PATCH 00/10] config-hook cleanups and two small 'git hook list' features Adrian Ratiu
2026-03-09 0:54 ` [PATCH 01/10] hook: move unsorted_string_list_remove() to string-list.[ch] Adrian Ratiu
2026-03-10 19:56 ` SZEDER Gábor
2026-03-11 11:08 ` Adrian Ratiu
2026-03-09 0:54 ` [PATCH 02/10] hook: fix minor style issues Adrian Ratiu
2026-03-09 2:12 ` Eric Sunshine
2026-03-09 0:54 ` [PATCH 03/10] hook: rename cb_data_free/alloc -> hook_data_free/alloc Adrian Ratiu
2026-03-11 10:24 ` Patrick Steinhardt
2026-03-11 11:09 ` Adrian Ratiu
2026-03-09 0:54 ` [PATCH 04/10] hook: detect & emit two more bugs Adrian Ratiu
2026-03-09 0:54 ` [PATCH 05/10] hook: replace hook_list_clear() -> string_list_clear_func() Adrian Ratiu
2026-03-09 2:18 ` Eric Sunshine
2026-03-10 14:20 ` Adrian Ratiu
2026-03-09 0:54 ` [PATCH 06/10] hook: make consistent use of friendly-name in docs Adrian Ratiu
2026-03-09 0:54 ` [PATCH 07/10] t1800: add test to verify hook execution ordering Adrian Ratiu
2026-03-09 0:54 ` [PATCH 08/10] hook: refactor hook_config_cache from strmap to named struct Adrian Ratiu
2026-03-09 21:59 ` Junio C Hamano
2026-03-10 14:19 ` Adrian Ratiu
2026-03-09 0:54 ` [PATCH 09/10] hook: show config scope in git hook list Adrian Ratiu
2026-03-09 21:59 ` Junio C Hamano
2026-03-10 14:45 ` Adrian Ratiu
2026-03-11 10:24 ` Patrick Steinhardt
2026-03-11 11:47 ` Adrian Ratiu
2026-03-09 0:54 ` [PATCH 10/10] hook: show disabled hooks in "git hook list" Adrian Ratiu
2026-03-11 10:24 ` Patrick Steinhardt
2026-03-11 12:24 ` Adrian Ratiu
2026-03-11 13:53 ` Patrick Steinhardt
2026-03-09 20:14 ` [PATCH 00/10] config-hook cleanups and two small 'git hook list' features Junio C Hamano
2026-03-10 14:37 ` Adrian Ratiu
2026-03-09 20:27 ` Junio C Hamano
2026-03-20 11:52 ` [PATCH v2 " Adrian Ratiu
2026-03-20 11:52 ` [PATCH v2 01/10] hook: move unsorted_string_list_remove() to string-list.[ch] Adrian Ratiu
2026-03-20 11:52 ` [PATCH v2 02/10] hook: fix minor style issues Adrian Ratiu
2026-03-24 8:37 ` Patrick Steinhardt
2026-03-24 19:19 ` Adrian Ratiu
2026-03-20 11:52 ` [PATCH v2 03/10] hook: rename cb_data_free/alloc -> hook_data_free/alloc Adrian Ratiu
2026-03-20 11:52 ` [PATCH v2 04/10] hook: detect & emit two more bugs Adrian Ratiu
2026-03-20 11:52 ` [PATCH v2 05/10] hook: replace hook_list_clear() -> string_list_clear_func() Adrian Ratiu
2026-03-24 8:37 ` Patrick Steinhardt
2026-03-24 22:33 ` Adrian Ratiu
2026-03-25 5:26 ` Patrick Steinhardt
2026-03-20 11:52 ` [PATCH v2 06/10] hook: make consistent use of friendly-name in docs Adrian Ratiu
2026-03-20 11:52 ` [PATCH v2 07/10] t1800: add test to verify hook execution ordering Adrian Ratiu
2026-03-20 11:52 ` [PATCH v2 08/10] hook: introduce hook_config_cache_entry for per-hook data Adrian Ratiu
2026-03-20 11:52 ` [PATCH v2 09/10] hook: show config scope in git hook list Adrian Ratiu
2026-03-24 8:37 ` Patrick Steinhardt
2026-03-25 11:28 ` Adrian Ratiu
2026-03-20 11:52 ` Adrian Ratiu [this message]
2026-03-24 8:38 ` [PATCH v2 10/10] hook: show disabled hooks in "git hook list" Patrick Steinhardt
2026-03-24 16:14 ` Junio C Hamano
2026-03-24 19:23 ` Adrian Ratiu
2026-03-23 16:11 ` [PATCH v2 00/10] config-hook cleanups and two small 'git hook list' features Junio C Hamano
2026-03-24 8:38 ` Patrick Steinhardt
2026-03-24 18:56 ` Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 00/12] config-hook cleanups and three small git-hook features Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 01/12] hook: move unsorted_string_list_remove() to string-list.[ch] Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 02/12] builtin/receive-pack: properly init receive_hook strbuf Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 03/12] hook: fix minor style issues Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 04/12] hook: rename cb_data_free/alloc -> hook_data_free/alloc Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 05/12] hook: detect & emit two more bugs Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 06/12] hook: replace hook_list_clear() -> string_list_clear_func() Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 07/12] hook: make consistent use of friendly-name in docs Adrian Ratiu
2026-03-25 19:54 ` [PATCH v3 08/12] t1800: add test to verify hook execution ordering Adrian Ratiu
2026-03-25 19:55 ` [PATCH v3 09/12] hook: introduce hook_config_cache_entry for per-hook data Adrian Ratiu
2026-03-25 19:55 ` [PATCH v3 10/12] hook: show config scope in git hook list Adrian Ratiu
2026-03-25 19:55 ` [PATCH v3 11/12] hook: show disabled hooks in "git hook list" Adrian Ratiu
2026-03-25 19:55 ` [PATCH v3 12/12] hook: reject unknown hook names in git-hook(1) Adrian Ratiu
2026-03-25 21:17 ` [PATCH v3 00/12] config-hook cleanups and three small git-hook features Junio C Hamano
2026-03-26 10:21 ` Adrian Ratiu
2026-03-27 8:04 ` Patrick Steinhardt
2026-03-27 16:11 ` Junio C Hamano
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260320115211.177351-11-adrian.ratiu@collabora.com \
--to=adrian.ratiu@collabora.com \
--cc=emilyshaffer@google.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=ps@pks.im \
--cc=sandals@crustytoothpaste.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.