hook: show config scope in git hook list

Users running "git hook list" can see which hooks are configured but have no way to tell at which config scope (local, global, system...) each hook was defined. Store the scope from ctx->kvi->scope in the single-pass config callback, then carry it through the cache to the hook structs, so we can expose it to users via the "git hook list --show-scope" flag, which mirrors the existing git config --show-scope convention. Without the flag the output is unchanged. The scope is printed as a tab-separated prefix (like "git config --show-scope"), making it unambiguously machine-parseable even when the friendly name contains spaces. Example usage: $ git hook list --show-scope pre-commit global linter local no-leaks hook from hookdir Traditional hooks from the hookdir are unaffected by --show-scope since the config scope concept does not apply to them. 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 committed Mar 25, 2026 at 21:55 UTC b66efad2b1f53755a80699dc39f94e2b15d6af67
5 files changed +63 -8
Documentation/git-hook.adoc
+8 -2
@@ -9,7 +9,7 @@ SYNOPSIS
9 --------
10 [verse]
11 'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
12 -'git hook' list [-z] <hook-name>
12 +'git hook' list [-z] [--show-scope] <hook-name>
13
14 DESCRIPTION
15 -----------
@@ -113,7 +113,7 @@ Any positional arguments to the hook should be passed after a
113 mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
114 linkgit:githooks[5] for arguments hooks might expect (if any).
115
116 -list [-z]::
116 +list [-z] [--show-scope]::
117 Print a list of hooks which will be run on `<hook-name>` event. If no
118 hooks are configured for that event, print a warning and return 1.
119 Use `-z` to terminate output lines with NUL instead of newlines.
@@ -134,6 +134,12 @@ OPTIONS
134 -z::
135 Terminate "list" output lines with NUL instead of newlines.
136
137 +--show-scope::
138 + For "list"; prefix each configured hook's friendly name with a
139 + tab-separated config scope (e.g. `local`, `global`, `system`),
140 + mirroring the output style of `git config --show-scope`. Traditional
141 + hooks from the hookdir are unaffected.
142 +
143 WRAPPERS
144 --------
145
builtin/hook.c
+12 -2
@@ -9,7 +9,7 @@
9 #define BUILTIN_HOOK_RUN_USAGE \
10 N_("git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
11 #define BUILTIN_HOOK_LIST_USAGE \
12 - N_("git hook list [-z] <hook-name>")
12 + N_("git hook list [-z] [--show-scope] <hook-name>")
13
14 static const char * const builtin_hook_usage[] = {
15 BUILTIN_HOOK_RUN_USAGE,
@@ -33,11 +33,14 @@ static int list(int argc, const char **argv, const char *prefix,
33 struct string_list_item *item;
34 const char *hookname = NULL;
35 int line_terminator = '\n';
36 + int show_scope = 0;
37 int ret = 0;
38
39 struct option list_options[] = {
40 OPT_SET_INT('z', NULL, &line_terminator,
41 N_("use NUL as line terminator"), '\0'),
42 + OPT_BOOL(0, "show-scope", &show_scope,
43 + N_("show the config scope that defined each hook")),
44 OPT_END(),
45 };
46
@@ -70,7 +73,14 @@ static int list(int argc, const char **argv, const char *prefix,
73 printf("%s%c", _("hook from hookdir"), line_terminator);
74 break;
75 case HOOK_CONFIGURED:
73 - printf("%s%c", h->u.configured.friendly_name, line_terminator);
76 + if (show_scope)
77 + printf("%s\t%s%c",
78 + config_scope_name(h->u.configured.scope),
79 + h->u.configured.friendly_name,
80 + line_terminator);
81 + else
82 + printf("%s%c", h->u.configured.friendly_name,
83 + line_terminator);
84 break;
85 default:
86 BUG("unknown hook kind");
hook.c
+20 -4
@@ -110,11 +110,11 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,
110
111 /*
112 * Cache entry stored as the .util pointer of string_list items inside the
113 - * hook config cache. For now carries only the command for the hook. Next
114 - * commits will add more data.
113 + * hook config cache.
114 */
115 struct hook_config_cache_entry {
116 char *command;
117 + enum config_scope scope;
118 };
119
120 /*
@@ -131,7 +131,7 @@ struct hook_all_config_cb {
131
132 /* repo_config() callback that collects all hook.* configuration in one pass. */
133 static int hook_config_lookup_all(const char *key, const char *value,
134 - const struct config_context *ctx UNUSED,
134 + const struct config_context *ctx,
135 void *cb_data)
136 {
137 struct hook_all_config_cb *data = cb_data;
@@ -168,7 +168,19 @@ static int hook_config_lookup_all(const char *key, const char *value,
168
169 /* Re-insert if necessary to preserve last-seen order. */
170 unsorted_string_list_remove(hooks, hook_name, 0);
171 - string_list_append(hooks, hook_name);
171 +
172 + if (!ctx->kvi)
173 + BUG("hook config callback called without key-value info");
174 +
175 + /*
176 + * Stash the config scope in the util pointer for
177 + * later retrieval in build_hook_config_map(). This
178 + * intermediate struct is transient and never leaves
179 + * that function, so we pack the enum value into the
180 + * pointer rather than heap-allocating a wrapper.
181 + */
182 + string_list_append(hooks, hook_name)->util =
183 + (void *)(uintptr_t)ctx->kvi->scope;
184 }
185 } else if (!strcmp(subkey, "command")) {
186 /* Store command overwriting the old value */
@@ -246,6 +258,8 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
258
259 for (size_t i = 0; i < hook_names->nr; i++) {
260 const char *hname = hook_names->items[i].string;
261 + enum config_scope scope =
262 + (enum config_scope)(uintptr_t)hook_names->items[i].util;
263 struct hook_config_cache_entry *entry;
264 char *command;
265
@@ -263,6 +277,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
277 /* util stores a cache entry; owned by the cache. */
278 CALLOC_ARRAY(entry, 1);
279 entry->command = xstrdup(command);
280 + entry->scope = scope;
281 string_list_append(hooks, hname)->util = entry;
282 }
283
@@ -344,6 +359,7 @@ static void list_hooks_add_configured(struct repository *r,
359 hook->kind = HOOK_CONFIGURED;
360 hook->u.configured.friendly_name = xstrdup(friendly_name);
361 hook->u.configured.command = xstrdup(entry->command);
362 + hook->u.configured.scope = entry->scope;
363
364 string_list_append(list, friendly_name)->util = hook;
365 }
hook.h
+2
@@ -1,5 +1,6 @@
1 #ifndef HOOK_H
2 #define HOOK_H
3 +#include "config.h"
4 #include "run-command.h"
5 #include "string-list.h"
6 #include "strmap.h"
@@ -29,6 +30,7 @@ struct hook {
30 struct {
31 const char *friendly_name;
32 const char *command;
33 + enum config_scope scope;
34 } configured;
35 } u;
36
t/t1800-hook.sh
+21
@@ -408,6 +408,27 @@ test_expect_success 'configured hooks run before hookdir hook' '
408 test_cmp expected actual
409 '
410
411 +test_expect_success 'git hook list --show-scope shows config scope' '
412 + setup_hookdir &&
413 + test_config_global hook.global-hook.command "echo global" &&
414 + test_config_global hook.global-hook.event pre-commit --add &&
415 + test_config hook.local-hook.command "echo local" &&
416 + test_config hook.local-hook.event pre-commit --add &&
417 +
418 + cat >expected <<-\EOF &&
419 + global global-hook
420 + local local-hook
421 + hook from hookdir
422 + EOF
423 + git hook list --show-scope pre-commit >actual &&
424 + test_cmp expected actual &&
425 +
426 + # without --show-scope the scope must not appear
427 + git hook list pre-commit >actual &&
428 + test_grep ! "^global " actual &&
429 + test_grep ! "^local " actual
430 +'
431 +
432 test_expect_success 'git hook run a hook with a bad shebang' '
433 test_when_finished "rm -rf bad-hooks" &&
434 mkdir bad-hooks &&