{"thread":{"id":"65171","subject":"[PATCH 00/10] config-hook cleanups and two small 'git hook list' features","startedAt":"2026-03-09T00:54:51Z","lastAt":"2026-03-27T16:11:45Z","messageCount":71,"participants":["Adrian Ratiu","Eric Sunshine","Junio C Hamano","SZEDER Gábor","Patrick Steinhardt"],"isPatch":true,"patchVersion":1,"patchTotal":10},"messages":[{"id":"538231","messageId":"20260309005416.2760030-1-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":null,"subject":"[PATCH 00/10] config-hook cleanups and two small 'git hook list' features","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:06Z","receivedAt":"2026-03-09T00:54:51Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Hello everyone,\n\nAs promised I've spun-off v3 of the config series [1] into its own standalone\npatch series after v2 landed in next.\n\nThis is mostly minor cleanups and refactorings + two minor feature additions\nto `git hook list`, which resulted from the previous review discussions:\n\n1. The ability to show the config scope (--show-scope).\n2. The ability to show which hooks are disabled.\n\nThis is based on next because the config hooks support is only in next.\n\nI have pushed the branch to Github [2] and provided a clean CI run [3].\n\nBig thank-you's to all who contributed to this up to now,\nAdrian\n\n1: https://lore.kernel.org/git/20260204165126.1548805-1-adrian.ratiu@collabora.com/T/#mafc63b5b32a78e48a703cccca3b7d6d9fe668fa2\n2: https://github.com/10ne1/git/tree/dev/aratiu/config-cleanups-v1\n3: https://github.com/10ne1/git/actions/runs/22833022192\n\nAdrian Ratiu (10):\n  hook: move unsorted_string_list_remove() to string-list.[ch]\n  hook: fix minor style issues\n  hook: rename cb_data_free/alloc -> hook_data_free/alloc\n  hook: detect & emit two more bugs\n  hook: replace hook_list_clear() -> string_list_clear_func()\n  hook: make consistent use of friendly-name in docs\n  t1800: add test to verify hook execution ordering\n  hook: refactor hook_config_cache from strmap to named struct\n  hook: show config scope in git hook list\n  hook: show disabled hooks in \"git hook list\"\n\n Documentation/config/hook.adoc |  30 ++---\n Documentation/git-hook.adoc    |  15 ++-\n builtin/hook.c                 |  28 +++--\n builtin/receive-pack.c         |  11 +-\n hook.c                         | 220 +++++++++++++++++++++------------\n hook.h                         |  39 ++++--\n refs.c                         |   3 +-\n repository.h                   |   3 +-\n string-list.c                  |   9 ++\n string-list.h                  |   8 ++\n t/t1800-hook.sh                |  83 ++++++++++++-\n transport.c                    |   3 +-\n 12 files changed, 321 insertions(+), 131 deletions(-)\n\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538232","messageId":"20260309005416.2760030-2-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 01/10] hook: move unsorted_string_list_remove() to string-list.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:07Z","receivedAt":"2026-03-09T00:54:51Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Move the convenience wrapper from hook to string-list since\nit's a more suitable place. Add a doc comment to the header.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c        | 8 --------\n string-list.c | 9 +++++++++\n string-list.h | 8 ++++++++\n 3 files changed, 17 insertions(+), 8 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 2c8252b2c4..313a6b9937 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -110,14 +110,6 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tstring_list_append(hook_list, hook_path)->util = h;\n }\n \n-static void unsorted_string_list_remove(struct string_list *list,\n-\t\t\t\t\tconst char *str)\n-{\n-\tstruct string_list_item *item = unsorted_string_list_lookup(list, str);\n-\tif (item)\n-\t\tunsorted_string_list_delete_item(list, item - list->items, 0);\n-}\n-\n /*\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\ndiff --git a/string-list.c b/string-list.c\nindex fffa2ad4b6..d260b873c8 100644\n--- a/string-list.c\n+++ b/string-list.c\n@@ -281,6 +281,15 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_\n \tlist->nr--;\n }\n \n+void unsorted_string_list_remove(struct string_list *list, const char *str,\n+\t\t\t\t int free_util)\n+{\n+\tstruct string_list_item *item = unsorted_string_list_lookup(list, str);\n+\tif (item)\n+\t\tunsorted_string_list_delete_item(list, item - list->items,\n+\t\t\t\t\t\t free_util);\n+}\n+\n /*\n  * append a substring [p..end] to list; return number of things it\n  * appended to the list.\ndiff --git a/string-list.h b/string-list.h\nindex 3ad862a187..b86ee7c099 100644\n--- a/string-list.h\n+++ b/string-list.h\n@@ -265,6 +265,14 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list,\n  */\n void unsorted_string_list_delete_item(struct string_list *list, int i, int free_util);\n \n+/**\n+ * Remove the first item matching `str` from an unsorted string_list.\n+ * No-op if `str` is not found. If `free_util` is non-zero, the `util`\n+ * pointer of the removed item is freed before deletion.\n+ */\n+void unsorted_string_list_remove(struct string_list *list, const char *str,\n+\t\t\t\t int free_util);\n+\n /**\n  * Split string into substrings on characters in `delim` and append the\n  * substrings to `list`.  The input string is not modified.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538233","messageId":"20260309005416.2760030-3-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 02/10] hook: fix minor style issues","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:08Z","receivedAt":"2026-03-09T00:54:53Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Fix some minor style nits pointed by Patrick and Junio:\n  * Use CALLOC_ARRAY instead of xcalloc.\n  * Init struct members during declaration.\n  * Simplify if condition boolean logic.\n  * Missing curly braces in if/else stmts.\n  * Unnecessary header includes.\n  * Capitalization in error/warn messages.\n  * Comment spelling: free'd -> freed.\n\nThese contain no logic changes, the code behaves the same as before.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c         |  6 ++----\n builtin/receive-pack.c | 11 +++++++----\n hook.c                 | 25 +++++++++++++------------\n refs.c                 |  3 ++-\n t/t1800-hook.sh        |  2 +-\n transport.c            |  3 ++-\n 6 files changed, 27 insertions(+), 23 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 83020dfb4f..c622a7399c 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -5,8 +5,6 @@\n #include \"gettext.h\"\n #include \"hook.h\"\n #include \"parse-options.h\"\n-#include \"strvec.h\"\n-#include \"abspath.h\"\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n@@ -51,7 +49,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \t * arguments later they probably should be caught by parse_options.\n \t */\n \tif (argc != 1)\n-\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\tusage_msg_opt(_(\"you must specify a hook event name to list.\"),\n \t\t\t      builtin_hook_list_usage, list_options);\n \n \thookname = argv[0];\n@@ -59,7 +57,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \thead = list_hooks(repo, hookname, NULL);\n \n \tif (!head->nr) {\n-\t\twarning(_(\"No hooks found for event '%s'\"), hookname);\n+\t\twarning(_(\"no hooks found for event '%s'\"), hookname);\n \t\tret = 1; /* no hooks found */\n \t\tgoto cleanup;\n \t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex d6225df890..4b63ccdfa3 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -904,7 +904,8 @@ static int feed_receive_hook_cb(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_\n static void *receive_hook_feed_state_alloc(void *feed_pipe_ctx)\n {\n \tstruct receive_hook_feed_state *init_state = feed_pipe_ctx;\n-\tstruct receive_hook_feed_state *data = xcalloc(1, sizeof(*data));\n+\tstruct receive_hook_feed_state *data;\n+\tCALLOC_ARRAY(data, 1);\n \tdata->report = init_state->report;\n \tdata->cmd = init_state->cmd;\n \tdata->skip_broken = init_state->skip_broken;\n@@ -928,7 +929,11 @@ static int run_receive_hook(struct command *commands,\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tstruct command *iter = commands;\n-\tstruct receive_hook_feed_state feed_init_state = { 0 };\n+\tstruct receive_hook_feed_state feed_init_state = {\n+\t\t.cmd = commands,\n+\t\t.skip_broken = skip_broken,\n+\t\t.buf = STRBUF_INIT,\n+\t};\n \tstruct async sideband_async;\n \tint sideband_async_started = 0;\n \tint saved_stderr = -1;\n@@ -961,8 +966,6 @@ static int run_receive_hook(struct command *commands,\n \tprepare_sideband_async(&sideband_async, &saved_stderr, &sideband_async_started);\n \n \t/* set up stdin callback */\n-\tfeed_init_state.cmd = commands;\n-\tfeed_init_state.skip_broken = skip_broken;\n \topt.feed_pipe_ctx = &feed_init_state;\n \topt.feed_pipe = feed_receive_hook_cb;\n \topt.feed_pipe_cb_data_alloc = receive_hook_feed_state_alloc;\ndiff --git a/hook.c b/hook.c\nindex 313a6b9937..c5a6788dd5 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -57,9 +57,9 @@ static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n \tif (!h)\n \t\treturn;\n \n-\tif (h->kind == HOOK_TRADITIONAL)\n+\tif (h->kind == HOOK_TRADITIONAL) {\n \t\tfree((void *)h->u.traditional.path);\n-\telse if (h->kind == HOOK_CONFIGURED) {\n+\t} else if (h->kind == HOOK_CONFIGURED) {\n \t\tfree((void *)h->u.configured.friendly_name);\n \t\tfree((void *)h->u.configured.command);\n \t}\n@@ -91,7 +91,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tif (!hook_path)\n \t\treturn;\n \n-\th = xcalloc(1, sizeof(struct hook));\n+\tCALLOC_ARRAY(h, 1);\n \n \t/*\n \t * If the hook is to run in a specific dir, a relative path can\n@@ -154,7 +154,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\t\tstrmap_get(&data->event_hooks, value);\n \n \t\t\tif (!hooks) {\n-\t\t\t\thooks = xcalloc(1, sizeof(*hooks));\n+\t\t\t\tCALLOC_ARRAY(hooks, 1);\n \t\t\t\tstring_list_init_dup(hooks);\n \t\t\t\tstrmap_put(&data->event_hooks, value, hooks);\n \t\t\t}\n@@ -227,7 +227,8 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t/* Construct the cache from parsed configs. */\n \tstrmap_for_each_entry(&cb_data.event_hooks, &iter, e) {\n \t\tstruct string_list *hook_names = e->value;\n-\t\tstruct string_list *hooks = xcalloc(1, sizeof(*hooks));\n+\t\tstruct string_list *hooks;\n+\t\tCALLOC_ARRAY(hooks, 1);\n \n \t\tstring_list_init_dup(hooks);\n \n@@ -281,7 +282,7 @@ static struct strmap *get_hook_config_cache(struct repository *r)\n \t\t * it just once on the first call.\n \t\t */\n \t\tif (!r->hook_config_cache) {\n-\t\t\tr->hook_config_cache = xcalloc(1, sizeof(*cache));\n+\t\t\tCALLOC_ARRAY(r->hook_config_cache, 1);\n \t\t\tstrmap_init(r->hook_config_cache);\n \t\t\tbuild_hook_config_map(r, r->hook_config_cache);\n \t\t}\n@@ -289,9 +290,9 @@ static struct strmap *get_hook_config_cache(struct repository *r)\n \t} else {\n \t\t/*\n \t\t * Out-of-repo calls (no gitdir) allocate and return a temporary\n-\t\t * map cache which gets free'd immediately by the caller.\n+\t\t * cache which gets freed immediately by the caller.\n \t\t */\n-\t\tcache = xcalloc(1, sizeof(*cache));\n+\t\tCALLOC_ARRAY(cache, 1);\n \t\tstrmap_init(cache);\n \t\tbuild_hook_config_map(r, cache);\n \t}\n@@ -311,7 +312,8 @@ static void list_hooks_add_configured(struct repository *r,\n \tfor (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {\n \t\tconst char *friendly_name = configured_hooks->items[i].string;\n \t\tconst char *command = configured_hooks->items[i].util;\n-\t\tstruct hook *hook = xcalloc(1, sizeof(struct hook));\n+\t\tstruct hook *hook;\n+\t\tCALLOC_ARRAY(hook, 1);\n \n \t\tif (options && options->feed_pipe_cb_data_alloc)\n \t\t\thook->feed_pipe_cb_data =\n@@ -343,7 +345,7 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n \tif (!hookname)\n \t\tBUG(\"null hookname was provided to hook_list()!\");\n \n-\thook_head = xmalloc(sizeof(struct string_list));\n+\tCALLOC_ARRAY(hook_head, 1);\n \tstring_list_init_dup(hook_head);\n \n \t/* Add hooks from the config, e.g. hook.myhook.event = pre-commit */\n@@ -493,8 +495,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,\n \t * Ensure cb_data copy and free functions are either provided together,\n \t * or neither one is provided.\n \t */\n-\tif ((options->feed_pipe_cb_data_alloc && !options->feed_pipe_cb_data_free) ||\n-\t    (!options->feed_pipe_cb_data_alloc && options->feed_pipe_cb_data_free))\n+\tif (!options->feed_pipe_cb_data_alloc != !options->feed_pipe_cb_data_free)\n \t\tBUG(\"feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together\");\n \n \tif (options->invoked_hook)\ndiff --git a/refs.c b/refs.c\nindex 6fb8f9d10c..33c7961802 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2591,7 +2591,8 @@ static int transaction_hook_feed_stdin(int hook_stdin_fd, void *pp_cb, void *pp_\n \n static void *transaction_feed_cb_data_alloc(void *feed_pipe_ctx UNUSED)\n {\n-\tstruct transaction_feed_cb_data *data = xmalloc(sizeof(*data));\n+\tstruct transaction_feed_cb_data *data;\n+\tCALLOC_ARRAY(data, 1);\n \tstrbuf_init(&data->buf, 0);\n \tdata->index = 0;\n \treturn data;\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex b1583e9ef9..952bf97b86 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -34,7 +34,7 @@ test_expect_success 'git hook usage' '\n \n test_expect_success 'git hook list: nonexistent hook' '\n \tcat >stderr.expect <<-\\EOF &&\n-\twarning: No hooks found for event '\\''test-hook'\\''\n+\twarning: no hooks found for event '\\''test-hook'\\''\n \tEOF\n \ttest_expect_code 1 git hook list test-hook 2>stderr.actual &&\n \ttest_cmp stderr.expect stderr.actual\ndiff --git a/transport.c b/transport.c\nindex 107f4fa5dc..56a4015389 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1360,7 +1360,8 @@ static int pre_push_hook_feed_stdin(int hook_stdin_fd, void *pp_cb UNUSED, void\n \n static void *pre_push_hook_data_alloc(void *feed_pipe_ctx)\n {\n-\tstruct feed_pre_push_hook_data *data = xmalloc(sizeof(*data));\n+\tstruct feed_pre_push_hook_data *data;\n+\tCALLOC_ARRAY(data, 1);\n \tstrbuf_init(&data->buf, 0);\n \tdata->refs = (struct ref *)feed_pipe_ctx;\n \treturn data;\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538234","messageId":"20260309005416.2760030-4-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 03/10] hook: rename cb_data_free/alloc -> hook_data_free/alloc","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:09Z","receivedAt":"2026-03-09T00:54:56Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Rename the hook callback function types to use the hook prefix.\n\nThis is a style fix with no logic changes.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c |  4 ++--\n hook.h | 10 +++++-----\n 2 files changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex c5a6788dd5..bc1c45b16d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -52,7 +52,7 @@ const char *find_hook(struct repository *r, const char *name)\n \treturn path.buf;\n }\n \n-static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n+static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n {\n \tif (!h)\n \t\treturn;\n@@ -70,7 +70,7 @@ static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n \tfree(h);\n }\n \n-void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free)\n+void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free)\n {\n \tstruct string_list_item *item;\n \ndiff --git a/hook.h b/hook.h\nindex e949f5d488..e514c1b45b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -43,8 +43,8 @@ struct hook {\n \tvoid *feed_pipe_cb_data;\n };\n \n-typedef void (*cb_data_free_fn)(void *data);\n-typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n+typedef void (*hook_data_free_fn)(void *data);\n+typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n \n struct run_hooks_opt\n {\n@@ -132,14 +132,14 @@ struct run_hooks_opt\n \t *\n \t * The `feed_pipe_ctx` pointer can be used to pass initialization data.\n \t */\n-\tcb_data_alloc_fn feed_pipe_cb_data_alloc;\n+\thook_data_alloc_fn feed_pipe_cb_data_alloc;\n \n \t/**\n \t * Called to free the memory initialized by `feed_pipe_cb_data_alloc`.\n \t *\n \t * Must always be provided when `feed_pipe_cb_data_alloc` is provided.\n \t */\n-\tcb_data_free_fn feed_pipe_cb_data_free;\n+\thook_data_free_fn feed_pipe_cb_data_free;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -189,7 +189,7 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n  * Frees the memory allocated for the hook list, including the `struct hook`\n  * items and their internal state.\n  */\n-void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free);\n+void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free);\n \n /**\n  * Frees the hook configuration cache stored in `struct repository`.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538235","messageId":"20260309005416.2760030-5-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 04/10] hook: detect & emit two more bugs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:10Z","receivedAt":"2026-03-09T00:54:56Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Trigger a bug when an unknown hook type is encountered while\nsetting up hook execution.\n\nAlso issue a bug if a configured hook is enabled without a cmd.\n\nMostly useful for defensive coding.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex bc1c45b16d..b8ed4d79e2 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -408,7 +408,11 @@ static int pick_next_hook(struct child_process *cp,\n \t} else if (h->kind == HOOK_CONFIGURED) {\n \t\t/* to enable oneliners, let config-specified hooks run in shell. */\n \t\tcp->use_shell = true;\n+\t\tif (!h->u.configured.command)\n+\t\t\tBUG(\"non-disabled HOOK_CONFIGURED hook has no command\");\n \t\tstrvec_push(&cp->args, h->u.configured.command);\n+\t} else {\n+\t\tBUG(\"unknown hook kind\");\n \t}\n \n \tif (!cp->args.nr)\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538236","messageId":"20260309005416.2760030-6-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 05/10] hook: replace hook_list_clear() -> string_list_clear_func()","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:11Z","receivedAt":"2026-03-09T00:54:59Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Replace the custom function with string_list_clear_func() which\nis a more common pattern for clearing a string_list.\n\nTo be able to do this, rework hook_clear() into hook_free(), so\nit can be passed to string_list_clear_func().\n\nA slight complication is the need to keep a copy of the internal\ncb data free() pointer, however I think it's worth it since the\nAPI becomes cleaner, e.g. no more calls with NULL function args\nlike hook_list_clear(hooks, NULL).\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c |  2 +-\n hook.c         | 50 +++++++++++++++++++++++++++++---------------------\n hook.h         | 20 ++++++++++++++------\n 3 files changed, 44 insertions(+), 28 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex c622a7399c..8fc647a4de 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -78,7 +78,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \t}\n \n cleanup:\n-\thook_list_clear(head, NULL);\n+\tstring_list_clear_func(head, hook_free);\n \tfree(head);\n \treturn ret;\n }\ndiff --git a/hook.c b/hook.c\nindex b8ed4d79e2..f6bb1999ae 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -52,8 +52,14 @@ const char *find_hook(struct repository *r, const char *name)\n \treturn path.buf;\n }\n \n-static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n+/*\n+ * Frees a struct hook stored as the util pointer of a string_list_item.\n+ * Suitable for use as a string_list_clear_func_t callback.\n+ */\n+void hook_free(void *p, const char *str UNUSED)\n {\n+\tstruct hook *h = p;\n+\n \tif (!h)\n \t\treturn;\n \n@@ -64,22 +70,12 @@ static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n \t\tfree((void *)h->u.configured.command);\n \t}\n \n-\tif (cb_data_free)\n-\t\tcb_data_free(h->feed_pipe_cb_data);\n+\tif (h->data_free && h->feed_pipe_cb_data)\n+\t\th->data_free(h->feed_pipe_cb_data);\n \n \tfree(h);\n }\n \n-void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free)\n-{\n-\tstruct string_list_item *item;\n-\n-\tfor_each_string_list_item(item, hooks)\n-\t\thook_clear(item->util, cb_data_free);\n-\n-\tstring_list_clear(hooks, 0);\n-}\n-\n /* Helper to detect and add default \"traditional\" hooks from the hookdir. */\n static void list_hooks_add_default(struct repository *r, const char *hookname,\n \t\t\t\t   struct string_list *hook_list,\n@@ -100,9 +96,15 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tif (options && options->dir)\n \t\thook_path = absolute_path(hook_path);\n \n-\t/* Setup per-hook internal state cb data */\n-\tif (options && options->feed_pipe_cb_data_alloc)\n+\t/*\n+\t * Setup per-hook internal state callback data.\n+\t * When provided, the alloc/free callbacks are always provided\n+\t * together, so use them to alloc/free the internal hook state.\n+\t */\n+\tif (options && options->feed_pipe_cb_data_alloc) {\n \t\th->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);\n+\t\th->data_free = options->feed_pipe_cb_data_free;\n+\t}\n \n \th->kind = HOOK_TRADITIONAL;\n \th->u.traditional.path = xstrdup(hook_path);\n@@ -148,7 +150,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\tstruct strmap_entry *e;\n \n \t\t\tstrmap_for_each_entry(&data->event_hooks, &iter, e)\n-\t\t\t\tunsorted_string_list_remove(e->value, hook_name);\n+\t\t\t\tunsorted_string_list_remove(e->value, hook_name, 0);\n \t\t} else {\n \t\t\tstruct string_list *hooks =\n \t\t\t\tstrmap_get(&data->event_hooks, value);\n@@ -160,7 +162,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\t}\n \n \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n-\t\t\tunsorted_string_list_remove(hooks, hook_name);\n+\t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n \t\t\tstring_list_append(hooks, hook_name);\n \t\t}\n \t} else if (!strcmp(subkey, \"command\")) {\n@@ -178,7 +180,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\tbreak;\n \t\tcase 1: /* enabled: undo a prior disabled entry */\n \t\t\tunsorted_string_list_remove(&data->disabled_hooks,\n-\t\t\t\t\t\t    hook_name);\n+\t\t\t\t\t\t    hook_name, 0);\n \t\t\tbreak;\n \t\tdefault:\n \t\t\tbreak; /* ignore unrecognised values */\n@@ -315,10 +317,16 @@ static void list_hooks_add_configured(struct repository *r,\n \t\tstruct hook *hook;\n \t\tCALLOC_ARRAY(hook, 1);\n \n-\t\tif (options && options->feed_pipe_cb_data_alloc)\n+\t\t/*\n+\t\t * When provided, the alloc/free callbacks are always provided\n+\t\t * together, so use them to alloc/free the internal hook state.\n+\t\t */\n+\t\tif (options && options->feed_pipe_cb_data_alloc) {\n \t\t\thook->feed_pipe_cb_data =\n \t\t\t\toptions->feed_pipe_cb_data_alloc(\n \t\t\t\t\toptions->feed_pipe_ctx);\n+\t\t\thook->data_free = options->feed_pipe_cb_data_free;\n+\t\t}\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n@@ -361,7 +369,7 @@ int hook_exists(struct repository *r, const char *name)\n {\n \tstruct string_list *hooks = list_hooks(r, name, NULL);\n \tint exists = hooks->nr > 0;\n-\thook_list_clear(hooks, NULL);\n+\tstring_list_clear_func(hooks, hook_free);\n \tfree(hooks);\n \treturn exists;\n }\n@@ -515,7 +523,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,\n \trun_processes_parallel(&opts);\n \tret = cb_data.rc;\n cleanup:\n-\thook_list_clear(cb_data.hook_command_list, options->feed_pipe_cb_data_free);\n+\tstring_list_clear_func(cb_data.hook_command_list, hook_free);\n \tfree(cb_data.hook_command_list);\n \trun_hooks_opt_clear(options);\n \treturn ret;\ndiff --git a/hook.h b/hook.h\nindex e514c1b45b..168c6495a4 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -7,6 +7,9 @@\n \n struct repository;\n \n+typedef void (*hook_data_free_fn)(void *data);\n+typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n+\n /**\n  * Represents a hook command to be run.\n  * Hooks can be:\n@@ -41,10 +44,15 @@ struct hook {\n \t * Only useful when using `run_hooks_opt.feed_pipe`, otherwise ignore it.\n \t */\n \tvoid *feed_pipe_cb_data;\n-};\n \n-typedef void (*hook_data_free_fn)(void *data);\n-typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n+\t/**\n+\t * Callback to free `feed_pipe_cb_data`.\n+\t *\n+\t * It is called automatically and points to the `feed_pipe_cb_data_free`\n+\t * provided via the `run_hook_opt` parameter.\n+\t */\n+\thook_data_free_fn data_free;\n+};\n \n struct run_hooks_opt\n {\n@@ -186,10 +194,10 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n \t\t\t       struct run_hooks_opt *options);\n \n /**\n- * Frees the memory allocated for the hook list, including the `struct hook`\n- * items and their internal state.\n+ * Frees a struct hook stored as the util pointer of a string_list_item.\n+ * Suitable for use as a string_list_clear_func_t callback.\n  */\n-void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free);\n+void hook_free(void *p, const char *str UNUSED);\n \n /**\n  * Frees the hook configuration cache stored in `struct repository`.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538237","messageId":"20260309005416.2760030-7-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 06/10] hook: make consistent use of friendly-name in docs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:12Z","receivedAt":"2026-03-09T00:55:01Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Both `name` and `friendly-name` is being used. Standardize on\n`friendly-name` for consistency since name is rather generic,\neven when used in the hooks namespace.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/hook.adoc | 30 +++++++++++++++---------------\n Documentation/git-hook.adoc    |  6 +++---\n hook.c                         |  2 +-\n hook.h                         |  2 +-\n 4 files changed, 20 insertions(+), 20 deletions(-)\n\ndiff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc\nindex 64e845a260..9e78f26439 100644\n--- a/Documentation/config/hook.adoc\n+++ b/Documentation/config/hook.adoc\n@@ -1,23 +1,23 @@\n-hook.<name>.command::\n-\tThe command to execute for `hook.<name>`. `<name>` is a unique\n-\t\"friendly\" name that identifies this hook. (The hook events that\n-\ttrigger the command are configured with `hook.<name>.event`.) The\n-\tvalue can be an executable path or a shell oneliner. If more than\n-\tone value is specified for the same `<name>`, only the last value\n-\tparsed is used. See linkgit:git-hook[1].\n+hook.<friendly-name>.command::\n+\tThe command to execute for `hook.<friendly-name>`. `<friendly-name>`\n+\tis a unique name that identifies this hook. The hook events that\n+\ttrigger the command are configured with `hook.<friendly-name>.event`.\n+\tThe value can be an executable path or a shell oneliner. If more than\n+\tone value is specified for the same `<friendly-name>`, only the last\n+\tvalue parsed is used. See linkgit:git-hook[1].\n \n-hook.<name>.event::\n-\tThe hook events that trigger `hook.<name>`. The value is the name\n-\tof a hook event, like \"pre-commit\" or \"update\". (See\n+hook.<friendly-name>.event::\n+\tThe hook events that trigger `hook.<friendly-name>`. The value is the\n+\tname of a hook event, like \"pre-commit\" or \"update\". (See\n \tlinkgit:githooks[5] for a complete list of hook events.) On the\n-\tspecified event, the associated `hook.<name>.command` is executed.\n-\tThis is a multi-valued key. To run `hook.<name>` on multiple\n+\tspecified event, the associated `hook.<friendly-name>.command` is executed.\n+\tThis is a multi-valued key. To run `hook.<friendly-name>` on multiple\n \tevents, specify the key more than once. An empty value resets\n \tthe list of events, clearing any previously defined events for\n-\t`hook.<name>`. See linkgit:git-hook[1].\n+\t`hook.<friendly-name>`. See linkgit:git-hook[1].\n \n-hook.<name>.enabled::\n-\tWhether the hook `hook.<name>` is enabled. Defaults to `true`.\n+hook.<friendly-name>.enabled::\n+\tWhether the hook `hook.<friendly-name>` is enabled. Defaults to `true`.\n \tSet to `false` to disable the hook without removing its\n \tconfiguration. This is particularly useful when a hook is defined\n \tin a system or global config file and needs to be disabled for a\ndiff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\nindex 12d2701b52..966388660a 100644\n--- a/Documentation/git-hook.adoc\n+++ b/Documentation/git-hook.adoc\n@@ -44,7 +44,7 @@ event`), and then `~/bin/spellchecker` will have a chance to check your commit\n message (during the `commit-msg` hook event).\n \n Commands are run in the order Git encounters their associated\n-`hook.<name>.event` configs during the configuration parse (see\n+`hook.<friendly-name>.event` configs during the configuration parse (see\n linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be\n added, only one `hook.linter.command` event is valid - Git uses \"last-one-wins\"\n to determine which command to run.\n@@ -76,10 +76,10 @@ first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It\n would evaluate the output of each when deciding whether to proceed with the\n commit.\n \n-For a full list of hook events which you can set your `hook.<name>.event` to,\n+For a full list of hook events which you can set your `hook.<friendly-name>.event` to,\n and how hooks are invoked during those events, see linkgit:githooks[5].\n \n-Git will ignore any `hook.<name>.event` that specifies an event it doesn't\n+Git will ignore any `hook.<friendly-name>.event` that specifies an event it doesn't\n recognize. This is intended so that tools which wrap Git can use the hook\n infrastructure to run their own hooks; see \"WRAPPERS\" for more guidance.\n \ndiff --git a/hook.c b/hook.c\nindex f6bb1999ae..7f89ae9cc2 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -116,7 +116,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\n  * event_hooks: event-name to list of friendly-names map.\n- * disabled_hooks: set of friendly-names with hook.name.enabled = false.\n+ * disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.\n  */\n struct hook_all_config_cb {\n \tstruct strmap commands;\ndiff --git a/hook.h b/hook.h\nindex 168c6495a4..49b40d949b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -14,7 +14,7 @@ typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n  * Represents a hook command to be run.\n  * Hooks can be:\n  * 1. \"traditional\" (found in the hooks directory)\n- * 2. \"configured\" (defined in Git's configuration via hook.<name>.event).\n+ * 2. \"configured\" (defined in Git's configuration via hook.<friendly-name>.event).\n  * The 'kind' field determines which part of the union 'u' is valid.\n  */\n struct hook {\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538238","messageId":"20260309005416.2760030-9-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 08/10] hook: refactor hook_config_cache from strmap to named struct","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:14Z","receivedAt":"2026-03-09T00:55:02Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Replace the raw `struct strmap *hook_config_cache` in `struct repository`\nwith a `struct hook_config_cache` which wraps the strmap in a named field.\n\nReplace the bare `char *command` util pointer stored in each string_list\nitem with a heap-allocated `struct hook_config_cache_entry` that carries\nthat command string.\n\nThis is just a refactoring with no behavior changes, to give the cache\nstruct room to grow so it can carry the additional hook metadata we'll\nbe adding in the following commits.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c       | 61 +++++++++++++++++++++++++++++++++-------------------\n hook.h       | 10 ++++++++-\n repository.h |  3 ++-\n 3 files changed, 50 insertions(+), 24 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 7f89ae9cc2..4fe50aa38c 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -112,6 +112,15 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tstring_list_append(hook_list, hook_path)->util = h;\n }\n \n+/*\n+ * Cache entry stored as the .util pointer of string_list items inside the\n+ * hook config cache. For now carries only the command for the hook. Next\n+ * commits will add more data.\n+ */\n+struct hook_config_cache_entry {\n+\tchar *command;\n+};\n+\n /*\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\n@@ -194,26 +203,32 @@ static int hook_config_lookup_all(const char *key, const char *value,\n /*\n  * The hook config cache maps each hook event name to a string_list where\n  * every item's string is the hook's friendly-name and its util pointer is\n- * the corresponding command string. Both strings are owned by the map.\n+ * a hook_config_cache_entry. All strings are owned by the map.\n  *\n  * Disabled hooks and hooks missing a command are already filtered out at\n  * parse time, so callers can iterate the list directly.\n  */\n-void hook_cache_clear(struct strmap *cache)\n+void hook_cache_clear(struct hook_config_cache *cache)\n {\n \tstruct hashmap_iter iter;\n \tstruct strmap_entry *e;\n \n-\tstrmap_for_each_entry(cache, &iter, e) {\n+\tstrmap_for_each_entry(&cache->hooks, &iter, e) {\n \t\tstruct string_list *hooks = e->value;\n-\t\tstring_list_clear(hooks, 1); /* free util (command) pointers */\n+\t\tfor (size_t i = 0; i < hooks->nr; i++) {\n+\t\t\tstruct hook_config_cache_entry *entry = hooks->items[i].util;\n+\t\t\tfree(entry->command);\n+\t\t\tfree(entry);\n+\t\t}\n+\t\tstring_list_clear(hooks, 0);\n \t\tfree(hooks);\n \t}\n-\tstrmap_clear(cache, 0);\n+\tstrmap_clear(&cache->hooks, 0);\n }\n \n /* Populate `cache` with the complete hook configuration */\n-static void build_hook_config_map(struct repository *r, struct strmap *cache)\n+static void build_hook_config_map(struct repository *r,\n+\t\t\t\t  struct hook_config_cache *cache)\n {\n \tstruct hook_all_config_cb cb_data;\n \tstruct hashmap_iter iter;\n@@ -236,6 +251,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \n \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n \t\t\tconst char *hname = hook_names->items[i].string;\n+\t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n \t\t\t/* filter out disabled hooks */\n@@ -249,12 +265,13 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t\t\t\t      \"'hook.%s.event' must be removed;\"\n \t\t\t\t      \" aborting.\"), hname, hname);\n \n-\t\t\t/* util stores the command; owned by the cache. */\n-\t\t\tstring_list_append(hooks, hname)->util =\n-\t\t\t\txstrdup(command);\n+\t\t\t/* util stores a cache entry; owned by the cache. */\n+\t\t\tCALLOC_ARRAY(entry, 1);\n+\t\t\tentry->command = xstrdup(command);\n+\t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n-\t\tstrmap_put(cache, e->key, hooks);\n+\t\tstrmap_put(&cache->hooks, e->key, hooks);\n \t}\n \n \tstrmap_clear(&cb_data.commands, 1);\n@@ -267,25 +284,25 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n }\n \n /*\n- * Return the hook config map for `r`, populating it first if needed.\n+ * Return the hook config cache for `r`, populating it first if needed.\n  *\n  * Out-of-repo calls (r->gitdir == NULL) allocate and return a temporary\n- * cache map; the caller is responsible for freeing it with\n+ * cache; the caller is responsible for freeing it with\n  * hook_cache_clear() + free().\n  */\n-static struct strmap *get_hook_config_cache(struct repository *r)\n+static struct hook_config_cache *get_hook_config_cache(struct repository *r)\n {\n-\tstruct strmap *cache = NULL;\n+\tstruct hook_config_cache *cache = NULL;\n \n \tif (r && r->gitdir) {\n \t\t/*\n-\t\t * For in-repo calls, the map is stored in r->hook_config_cache,\n-\t\t * so repeated invocations don't parse the configs, so allocate\n+\t\t * For in-repo calls, the cache is stored in r->hook_config_cache,\n+\t\t * so repeated invocations don't parse the configs; allocate\n \t\t * it just once on the first call.\n \t\t */\n \t\tif (!r->hook_config_cache) {\n \t\t\tCALLOC_ARRAY(r->hook_config_cache, 1);\n-\t\t\tstrmap_init(r->hook_config_cache);\n+\t\t\tstrmap_init(&r->hook_config_cache->hooks);\n \t\t\tbuild_hook_config_map(r, r->hook_config_cache);\n \t\t}\n \t\tcache = r->hook_config_cache;\n@@ -295,7 +312,7 @@ static struct strmap *get_hook_config_cache(struct repository *r)\n \t\t * cache which gets freed immediately by the caller.\n \t\t */\n \t\tCALLOC_ARRAY(cache, 1);\n-\t\tstrmap_init(cache);\n+\t\tstrmap_init(&cache->hooks);\n \t\tbuild_hook_config_map(r, cache);\n \t}\n \n@@ -307,13 +324,13 @@ static void list_hooks_add_configured(struct repository *r,\n \t\t\t\t      struct string_list *list,\n \t\t\t\t      struct run_hooks_opt *options)\n {\n-\tstruct strmap *cache = get_hook_config_cache(r);\n-\tstruct string_list *configured_hooks = strmap_get(cache, hookname);\n+\tstruct hook_config_cache *cache = get_hook_config_cache(r);\n+\tstruct string_list *configured_hooks = strmap_get(&cache->hooks, hookname);\n \n \t/* Iterate through configured hooks and initialize internal states */\n \tfor (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {\n \t\tconst char *friendly_name = configured_hooks->items[i].string;\n-\t\tconst char *command = configured_hooks->items[i].util;\n+\t\tstruct hook_config_cache_entry *entry = configured_hooks->items[i].util;\n \t\tstruct hook *hook;\n \t\tCALLOC_ARRAY(hook, 1);\n \n@@ -330,7 +347,7 @@ static void list_hooks_add_configured(struct repository *r,\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n-\t\thook->u.configured.command = xstrdup(command);\n+\t\thook->u.configured.command = xstrdup(entry->command);\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\ndiff --git a/hook.h b/hook.h\nindex 49b40d949b..4d0c22f1dc 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -199,11 +199,19 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n  */\n void hook_free(void *p, const char *str UNUSED);\n \n+/**\n+ * Persistent cache for hook configuration, stored on `struct repository`.\n+ * Populated lazily on first hook use and freed by repo_clear().\n+ */\n+struct hook_config_cache {\n+\tstruct strmap hooks; /* maps event name -> string_list of hooks */\n+};\n+\n /**\n  * Frees the hook configuration cache stored in `struct repository`.\n  * Called by repo_clear().\n  */\n-void hook_cache_clear(struct strmap *cache);\n+void hook_cache_clear(struct hook_config_cache *cache);\n \n /**\n  * Returns the path to the hook file, or NULL if the hook is missing\ndiff --git a/repository.h b/repository.h\nindex 078059a6e0..3fd73d2c54 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -12,6 +12,7 @@ struct lock_file;\n struct pathspec;\n struct object_database;\n struct submodule_cache;\n+struct hook_config_cache;\n struct promisor_remote_config;\n struct remote_state;\n \n@@ -170,7 +171,7 @@ struct repository {\n \t * Lazily-populated cache mapping hook event names to configured hooks.\n \t * NULL until first hook use.\n \t */\n-\tstruct strmap *hook_config_cache;\n+\tstruct hook_config_cache *hook_config_cache;\n \n \t/* Configurations related to promisor remotes. */\n \tchar *repository_format_partial_clone;\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538239","messageId":"20260309005416.2760030-8-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 07/10] t1800: add test to verify hook execution ordering","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:13Z","receivedAt":"2026-03-09T00:55:04Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"There is a documented expectation that configured hooks are\nrun before the hook from the hookdir. Add a test for it.\n\nWhile at it, I noticed that `git hook list -h` runs twice\nin the `git hook usage` test, so remove one invocation.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/t1800-hook.sh | 29 ++++++++++++++++++++++++++++-\n 1 file changed, 28 insertions(+), 1 deletion(-)\n\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 952bf97b86..7eee84fc39 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -25,7 +25,6 @@ test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook &&\n \ttest_expect_code 129 git hook run &&\n \ttest_expect_code 129 git hook run -h &&\n-\ttest_expect_code 129 git hook list -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n \ttest_expect_code 129 git hook list -h &&\n@@ -381,6 +380,34 @@ test_expect_success 'globally disabled hook can be re-enabled locally' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'configured hooks run before hookdir hook' '\n+\tsetup_hookdir &&\n+\ttest_config hook.first.event \"pre-commit\" &&\n+\ttest_config hook.first.command \"echo first\" &&\n+\ttest_config hook.second.event \"pre-commit\" &&\n+\ttest_config hook.second.command \"echo second\" &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tfirst\n+\tsecond\n+\thook from hookdir\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# \"Legacy Hook\" is the output of the hookdir pre-commit script\n+\t# written by setup_hookdir() above.\n+\tcat >expected <<-\\EOF &&\n+\tfirst\n+\tsecond\n+\t\"Legacy Hook\"\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_when_finished \"rm -rf bad-hooks\" &&\n \tmkdir bad-hooks &&\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538240","messageId":"20260309005416.2760030-10-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 09/10] hook: show config scope in git hook list","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:15Z","receivedAt":"2026-03-09T00:55:05Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Users running \"git hook list\" can see which hooks are configured but\nhave no way to tell at which config scope (local, global, system...)\neach hook was defined.\n\nStore the scope from ctx->kvi->scope in the single-pass config callback,\nthen carry it through the cache to the hook structs, so we can expose it\nto users via the \"git hook list --show-scope\" flag, which mirrors the\nexisting git config --show-scope convention.\n\nWithout the flag the output is unchanged.\n\nExample usage:\n$ git hook list --show-scope pre-commit\nlinter (global)\nno-leaks (local)\nhook from hookdir\n\nTraditional hooks from the hookdir are unaffected by --show-scope since\nthe config scope concept does not apply to them.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/git-hook.adoc |  9 +++++++--\n builtin/hook.c              | 14 ++++++++++++--\n hook.c                      | 24 ++++++++++++++++++++----\n hook.h                      |  2 ++\n t/t1800-hook.sh             | 19 +++++++++++++++++++\n 5 files changed, 60 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\nindex 966388660a..4d4e728327 100644\n--- a/Documentation/git-hook.adoc\n+++ b/Documentation/git-hook.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [verse]\n 'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\n-'git hook' list [-z] <hook-name>\n+'git hook' list [-z] [--show-scope] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -113,7 +113,7 @@ Any positional arguments to the hook should be passed after a\n mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See\n linkgit:githooks[5] for arguments hooks might expect (if any).\n \n-list [-z]::\n+list [-z] [--show-scope]::\n \tPrint a list of hooks which will be run on `<hook-name>` event. If no\n \thooks are configured for that event, print a warning and return 1.\n \tUse `-z` to terminate output lines with NUL instead of newlines.\n@@ -134,6 +134,11 @@ OPTIONS\n -z::\n \tTerminate \"list\" output lines with NUL instead of newlines.\n \n+--show-scope::\n+\tFor \"list\"; print the config scope (e.g. `local`, `global`, `system`)\n+\tin parentheses after the friendly name of each configured hook, to show\n+\twhere it was defined. Traditional hooks from the hookdir are unaffected.\n+\n WRAPPERS\n --------\n \ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 8fc647a4de..c806640361 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -9,7 +9,7 @@\n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n #define BUILTIN_HOOK_LIST_USAGE \\\n-\tN_(\"git hook list [-z] <hook-name>\")\n+\tN_(\"git hook list [-z] [--show-scope] <hook-name>\")\n \n static const char * const builtin_hook_usage[] = {\n \tBUILTIN_HOOK_RUN_USAGE,\n@@ -33,11 +33,14 @@ static int list(int argc, const char **argv, const char *prefix,\n \tstruct string_list_item *item;\n \tconst char *hookname = NULL;\n \tint line_terminator = '\\n';\n+\tint show_scope = 0;\n \tint ret = 0;\n \n \tstruct option list_options[] = {\n \t\tOPT_SET_INT('z', NULL, &line_terminator,\n \t\t\t    N_(\"use NUL as line terminator\"), '\\0'),\n+\t\tOPT_BOOL(0, \"show-scope\", &show_scope,\n+\t\t\t N_(\"show the config scope that defined each hook\")),\n \t\tOPT_END(),\n \t};\n \n@@ -70,7 +73,14 @@ static int list(int argc, const char **argv, const char *prefix,\n \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n \t\t\tbreak;\n \t\tcase HOOK_CONFIGURED:\n-\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name, line_terminator);\n+\t\t\tif (show_scope)\n+\t\t\t\tprintf(\"%s (%s)%c\",\n+\t\t\t\t       h->u.configured.friendly_name,\n+\t\t\t\t       config_scope_name(h->u.configured.scope),\n+\t\t\t\t       line_terminator);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n+\t\t\t\t       line_terminator);\n \t\t\tbreak;\n \t\tdefault:\n \t\t\tBUG(\"unknown hook kind\");\ndiff --git a/hook.c b/hook.c\nindex 4fe50aa38c..2c03baeaac 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -114,11 +114,11 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \n /*\n  * Cache entry stored as the .util pointer of string_list items inside the\n- * hook config cache. For now carries only the command for the hook. Next\n- * commits will add more data.\n+ * hook config cache.\n  */\n struct hook_config_cache_entry {\n \tchar *command;\n+\tenum config_scope scope;\n };\n \n /*\n@@ -135,7 +135,7 @@ struct hook_all_config_cb {\n \n /* repo_config() callback that collects all hook.* configuration in one pass. */\n static int hook_config_lookup_all(const char *key, const char *value,\n-\t\t\t\t  const struct config_context *ctx UNUSED,\n+\t\t\t\t  const struct config_context *ctx,\n \t\t\t\t  void *cb_data)\n {\n \tstruct hook_all_config_cb *data = cb_data;\n@@ -172,7 +172,19 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \n \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n \t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n-\t\t\tstring_list_append(hooks, hook_name);\n+\n+\t\t\tif (!ctx->kvi)\n+\t\t\t\tBUG(\"hook config callback called without key-value info\");\n+\n+\t\t\t/*\n+\t\t\t * Stash the config scope in the util pointer for\n+\t\t\t * later retrieval in build_hook_config_map(). This\n+\t\t\t * intermediate struct is transient and never leaves\n+\t\t\t * that function, so we pack the enum value into the\n+\t\t\t * pointer rather than heap-allocating a wrapper.\n+\t\t\t */\n+\t\t\tstring_list_append(hooks, hook_name)->util =\n+\t\t\t\t(void *)(uintptr_t)ctx->kvi->scope;\n \t\t}\n \t} else if (!strcmp(subkey, \"command\")) {\n \t\t/* Store command overwriting the old value */\n@@ -251,6 +263,8 @@ static void build_hook_config_map(struct repository *r,\n \n \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n \t\t\tconst char *hname = hook_names->items[i].string;\n+\t\t\tenum config_scope scope =\n+\t\t\t\t(enum config_scope)(uintptr_t)hook_names->items[i].util;\n \t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n@@ -268,6 +282,7 @@ static void build_hook_config_map(struct repository *r,\n \t\t\t/* util stores a cache entry; owned by the cache. */\n \t\t\tCALLOC_ARRAY(entry, 1);\n \t\t\tentry->command = xstrdup(command);\n+\t\t\tentry->scope = scope;\n \t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n@@ -348,6 +363,7 @@ static void list_hooks_add_configured(struct repository *r,\n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n \t\thook->u.configured.command = xstrdup(entry->command);\n+\t\thook->u.configured.scope = entry->scope;\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\ndiff --git a/hook.h b/hook.h\nindex 4d0c22f1dc..0d711ed21a 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,5 +1,6 @@\n #ifndef HOOK_H\n #define HOOK_H\n+#include \"config.h\"\n #include \"strvec.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n@@ -29,6 +30,7 @@ struct hook {\n \t\tstruct {\n \t\t\tconst char *friendly_name;\n \t\t\tconst char *command;\n+\t\t\tenum config_scope scope;\n \t\t} configured;\n \t} u;\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 7eee84fc39..aed07575e3 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -408,6 +408,25 @@ test_expect_success 'configured hooks run before hookdir hook' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list --show-scope shows config scope' '\n+\ttest_config_global hook.global-hook.command \"echo global\" &&\n+\ttest_config_global hook.global-hook.event test-hook --add &&\n+\ttest_config hook.local-hook.command \"echo local\" &&\n+\ttest_config hook.local-hook.event test-hook --add &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tglobal-hook (global)\n+\tlocal-hook (local)\n+\tEOF\n+\tgit hook list --show-scope test-hook >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# without --show-scope the scope must not appear\n+\tgit hook list test-hook >actual &&\n+\ttest_grep ! \"(global)\" actual &&\n+\ttest_grep ! \"(local)\" actual\n+'\n+\n test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_when_finished \"rm -rf bad-hooks\" &&\n \tmkdir bad-hooks &&\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538241","messageId":"20260309005416.2760030-11-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH 10/10] hook: show disabled hooks in \"git hook list\"","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-09T00:54:16Z","receivedAt":"2026-03-09T00:55:07Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Disabled hooks were filtered out of the cache entirely, making them\ninvisible to \"git hook list\". Keep them in the cache with a new\n\"disabled\" flag which is propagated to the respective struct hook.\n\n\"git hook list\" now shows disabled hooks annotated with \"(disabled)\"\nin the config order. With --show-scope, it looks like:\n\n$ git hook list --show-scope pre-commit\nlinter (global)\nno-leaks (local, disabled)\nhook from hookdir\n\nA disabled hook without a command issues a warning instead of the\nfatal \"hook.X.command must be configured\" error. We could also throw\nan error, however it seemd a bit excessive to me in this case.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c  | 18 ++++++++++-------\n hook.c          | 54 +++++++++++++++++++++++++++++++++----------------\n hook.h          |  1 +\n t/t1800-hook.sh | 33 +++++++++++++++++++++++++++---\n 4 files changed, 79 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex c806640361..ff446948fa 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -72,16 +72,20 @@ static int list(int argc, const char **argv, const char *prefix,\n \t\tcase HOOK_TRADITIONAL:\n \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n \t\t\tbreak;\n-\t\tcase HOOK_CONFIGURED:\n-\t\t\tif (show_scope)\n-\t\t\t\tprintf(\"%s (%s)%c\",\n-\t\t\t\t       h->u.configured.friendly_name,\n-\t\t\t\t       config_scope_name(h->u.configured.scope),\n+\t\tcase HOOK_CONFIGURED: {\n+\t\t\tconst char *name = h->u.configured.friendly_name;\n+\t\t\tconst char *scope = show_scope ?\n+\t\t\t\tconfig_scope_name(h->u.configured.scope) : NULL;\n+\t\t\tif (scope)\n+\t\t\t\tprintf(\"%s (%s%s)%c\", name, scope,\n+\t\t\t\t       h->u.configured.disabled ? \", disabled\" : \"\",\n \t\t\t\t       line_terminator);\n+\t\t\telse if (h->u.configured.disabled)\n+\t\t\t\tprintf(\"%s (disabled)%c\", name, line_terminator);\n \t\t\telse\n-\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n-\t\t\t\t       line_terminator);\n+\t\t\t\tprintf(\"%s%c\", name, line_terminator);\n \t\t\tbreak;\n+\t\t}\n \t\tdefault:\n \t\t\tBUG(\"unknown hook kind\");\n \t\t}\ndiff --git a/hook.c b/hook.c\nindex 2c03baeaac..4f4f060156 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -119,6 +119,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n struct hook_config_cache_entry {\n \tchar *command;\n \tenum config_scope scope;\n+\tint disabled;\n };\n \n /*\n@@ -217,8 +218,10 @@ static int hook_config_lookup_all(const char *key, const char *value,\n  * every item's string is the hook's friendly-name and its util pointer is\n  * a hook_config_cache_entry. All strings are owned by the map.\n  *\n- * Disabled hooks and hooks missing a command are already filtered out at\n- * parse time, so callers can iterate the list directly.\n+ * Disabled hooks are kept in the cache with entry->disabled set, so that\n+ * \"git hook list\" can display them. Hooks missing a command are filtered\n+ * out at build time; if a disabled hook has no command it is silently\n+ * skipped rather than triggering a fatal error.\n  */\n void hook_cache_clear(struct hook_config_cache *cache)\n {\n@@ -268,21 +271,26 @@ static void build_hook_config_map(struct repository *r,\n \t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n-\t\t\t/* filter out disabled hooks */\n-\t\t\tif (unsorted_string_list_lookup(&cb_data.disabled_hooks,\n-\t\t\t\t\t\t\thname))\n-\t\t\t\tcontinue;\n+\t\t\tint is_disabled =\n+\t\t\t\t!!unsorted_string_list_lookup(\n+\t\t\t\t\t&cb_data.disabled_hooks, hname);\n \n \t\t\tcommand = strmap_get(&cb_data.commands, hname);\n-\t\t\tif (!command)\n-\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n-\t\t\t\t      \"'hook.%s.event' must be removed;\"\n-\t\t\t\t      \" aborting.\"), hname, hname);\n+\t\t\tif (!command) {\n+\t\t\t\tif (is_disabled)\n+\t\t\t\t\twarning(_(\"disabled hook '%s' has no \"\n+\t\t\t\t\t\t  \"command configured\"), hname);\n+\t\t\t\telse\n+\t\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n+\t\t\t\t\t      \"'hook.%s.event' must be removed;\"\n+\t\t\t\t\t      \" aborting.\"), hname, hname);\n+\t\t\t}\n \n \t\t\t/* util stores a cache entry; owned by the cache. */\n \t\t\tCALLOC_ARRAY(entry, 1);\n-\t\t\tentry->command = xstrdup(command);\n+\t\t\tentry->command = command ? xstrdup(command) : NULL;\n \t\t\tentry->scope = scope;\n+\t\t\tentry->disabled = is_disabled;\n \t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n@@ -362,8 +370,10 @@ static void list_hooks_add_configured(struct repository *r,\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n-\t\thook->u.configured.command = xstrdup(entry->command);\n+\t\thook->u.configured.command =\n+\t\t\tentry->command ? xstrdup(entry->command) : NULL;\n \t\thook->u.configured.scope = entry->scope;\n+\t\thook->u.configured.disabled = entry->disabled;\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\n@@ -401,7 +411,16 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n int hook_exists(struct repository *r, const char *name)\n {\n \tstruct string_list *hooks = list_hooks(r, name, NULL);\n-\tint exists = hooks->nr > 0;\n+\tint exists = 0;\n+\n+\tfor (size_t i = 0; i < hooks->nr; i++) {\n+\t\tstruct hook *h = hooks->items[i].util;\n+\t\tif (h->kind == HOOK_TRADITIONAL ||\n+\t\t    !h->u.configured.disabled) {\n+\t\t\texists = 1;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n \tstring_list_clear_func(hooks, hook_free);\n \tfree(hooks);\n \treturn exists;\n@@ -416,10 +435,11 @@ static int pick_next_hook(struct child_process *cp,\n \tstruct string_list *hook_list = hook_cb->hook_command_list;\n \tstruct hook *h;\n \n-\tif (hook_cb->hook_to_run_index >= hook_list->nr)\n-\t\treturn 0;\n-\n-\th = hook_list->items[hook_cb->hook_to_run_index++].util;\n+\tdo {\n+\t\tif (hook_cb->hook_to_run_index >= hook_list->nr)\n+\t\t\treturn 0;\n+\t\th = hook_list->items[hook_cb->hook_to_run_index++].util;\n+\t} while (h->kind == HOOK_CONFIGURED && h->u.configured.disabled);\n \n \tcp->no_stdin = 1;\n \tstrvec_pushv(&cp->env, hook_cb->options->env.v);\ndiff --git a/hook.h b/hook.h\nindex 0d711ed21a..0432df963f 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -31,6 +31,7 @@ struct hook {\n \t\t\tconst char *friendly_name;\n \t\t\tconst char *command;\n \t\t\tenum config_scope scope;\n+\t\t\tint disabled;\n \t\t} configured;\n \t} u;\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex aed07575e3..cbf7d2bf80 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -357,7 +357,15 @@ test_expect_success 'disabled hook is not run' '\n \ttest_must_be_empty actual\n '\n \n-test_expect_success 'disabled hook does not appear in git hook list' '\n+test_expect_success 'disabled hook with no command warns' '\n+\ttest_config hook.nocommand.event \"pre-commit\" &&\n+\ttest_config hook.nocommand.enabled false &&\n+\n+\tgit hook list pre-commit 2>actual &&\n+\ttest_grep \"disabled hook.*nocommand.*no command configured\" actual\n+'\n+\n+test_expect_success 'disabled hook appears as disabled in git hook list' '\n \ttest_config hook.active.event \"pre-commit\" &&\n \ttest_config hook.active.command \"echo active\" &&\n \ttest_config hook.inactive.event \"pre-commit\" &&\n@@ -365,8 +373,27 @@ test_expect_success 'disabled hook does not appear in git hook list' '\n \ttest_config hook.inactive.enabled false &&\n \n \tgit hook list pre-commit >actual &&\n-\ttest_grep \"active\" actual &&\n-\ttest_grep ! \"inactive\" actual\n+\ttest_grep \"^active$\" actual &&\n+\ttest_grep \"^inactive (disabled)$\" actual\n+'\n+\n+test_expect_success 'disabled hook shows scope with --show-scope' '\n+\ttest_config hook.myhook.event \"pre-commit\" &&\n+\ttest_config hook.myhook.command \"echo hi\" &&\n+\ttest_config hook.myhook.enabled false &&\n+\n+\tgit hook list --show-scope pre-commit >actual &&\n+\ttest_grep \"myhook (local, disabled)\" actual\n+'\n+\n+test_expect_success 'disabled configured hook is not reported as existing by hook_exists' '\n+\ttest_when_finished \"rm -f git-bugreport-hook-exists-test.txt\" &&\n+\ttest_config hook.linter.event \"pre-commit\" &&\n+\ttest_config hook.linter.command \"echo lint\" &&\n+\ttest_config hook.linter.enabled false &&\n+\n+\tgit bugreport -s hook-exists-test &&\n+\ttest_grep ! \"pre-commit\" git-bugreport-hook-exists-test.txt\n '\n \n test_expect_success 'globally disabled hook can be re-enabled locally' '\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"538247","messageId":"CAPig+cRGJVg+qo=UZA4syvTQ58G54tHo-pSZ_6dE6pU2e0agyw@mail.gmail.com","threadId":"65171","inReplyTo":"20260309005416.2760030-3-adrian.ratiu@collabora.com","subject":"Re: [PATCH 02/10] hook: fix minor style issues","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2026-03-09T02:12:12Z","receivedAt":"2026-03-09T02:12:24Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":null},"body":"On Sun, Mar 8, 2026 at 8:54 PM Adrian Ratiu <adrian.ratiu@collabora.com> wrote:\n> Fix some minor style nits pointed by Patrick and Junio:\n>   ...\n>   * Capitalization in error/warn messages.\n>   ...\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> @@ -51,7 +49,7 @@ static int list(int argc, const char **argv, const char *prefix,\n>         if (argc != 1)\n> -               usage_msg_opt(_(\"You must specify a hook event name to list.\"),\n> +               usage_msg_opt(_(\"you must specify a hook event name to list.\"),\n>                               builtin_hook_list_usage, list_options);\n\nYou will also want to drop the full-stop (\".\") from the message\naccording to style guidelines.\n"},{"id":"538248","messageId":"CAPig+cRngzGdd-ABWLSiypuJVQ-LVv-xqmn2h+c7qB8_OBpyRg@mail.gmail.com","threadId":"65171","inReplyTo":"20260309005416.2760030-6-adrian.ratiu@collabora.com","subject":"Re: [PATCH 05/10] hook: replace hook_list_clear() -> string_list_clear_func()","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2026-03-09T02:18:25Z","receivedAt":"2026-03-09T02:18:37Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":null},"body":"On Sun, Mar 8, 2026 at 8:55 PM Adrian Ratiu <adrian.ratiu@collabora.com> wrote:\n> Replace the custom function with string_list_clear_func() which\n> is a more common pattern for clearing a string_list.\n>\n> To be able to do this, rework hook_clear() into hook_free(), so\n> it can be passed to string_list_clear_func().\n>\n> A slight complication is the need to keep a copy of the internal\n> cb data free() pointer, however I think it's worth it since the\n> API becomes cleaner, e.g. no more calls with NULL function args\n> like hook_list_clear(hooks, NULL).\n>\n> Suggested-by: Patrick Steinhardt <ps@pks.im>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n> diff --git a/hook.h b/hook.h\n> @@ -186,10 +194,10 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n>  /**\n> + * Frees a struct hook stored as the util pointer of a string_list_item.\n> + * Suitable for use as a string_list_clear_func_t callback.\n>   */\n> +void hook_free(void *p, const char *str UNUSED);\n\nSee [*] regarding UNUSED in header file.\n\n[*]: https://lore.kernel.org/git/xmqqcy1g25fl.fsf@gitster.g/\n"},{"id":"538316","messageId":"xmqq1phs69qy.fsf@gitster.g","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH 00/10] config-hook cleanups and two small 'git hook list' features","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-09T20:14:29Z","receivedAt":"2026-03-09T20:14:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":null},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> As promised I've spun-off v3 of the config series [1] into its own standalone\n> patch series after v2 landed in next.\n>\n> This is mostly minor cleanups and refactorings + two minor feature additions\n> to `git hook list`, which resulted from the previous review discussions:\n>\n> 1. The ability to show the config scope (--show-scope).\n> 2. The ability to show which hooks are disabled.\n\nOK.\n\n> This is based on next because the config hooks support is only in next.\n\nNot advisable, as doing so would take your topic hostage of _all_\nother topics in 'next', and it will _never_ happen for all of them,\nincluding the merge commit that merged them into 'next', to be\nmerged to 'master'.\n\nAfter learning from the output of\n\n    $ git log --first-parent --oneline master..'seen^{/^### match next}' |\n      grep ar/\n\nthat ar/config-hooks and ar/run-command-hook-take-2 are the two\ntopic that may be relevant to the config-hook topic in 'next', and\nknowing that ar/config-hooks fully contains the other topic, I\ninstead did the following to prepare a base:\n\n    $ git checkout -b ar/config-hook-cleanups master\n    $ git merge ar/config-hooks\n\nand then applied these 10 patches.  That way, ar/config-hooks can\ngraduate in due course, and then this topic can follow, without\nwaiting for other random things in 'next'.\n\n> I have pushed the branch to Github [2] and provided a clean CI run [3].\n>\n> Big thank-you's to all who contributed to this up to now,\n> Adrian\n\nThanks.  Queued.\n"},{"id":"538317","messageId":"xmqqsea84uk5.fsf@gitster.g","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH 00/10] config-hook cleanups and two small 'git hook list' features","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-09T20:27:54Z","receivedAt":"2026-03-09T20:27:57Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":null},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Hello everyone,\n>\n> As promised I've spun-off v3 of the config series [1] into its own standalone\n> patch series after v2 landed in next.\n>\n> This is mostly minor cleanups and refactorings + two minor feature additions\n> to `git hook list`, which resulted from the previous review discussions:\n>\n> 1. The ability to show the config scope (--show-scope).\n> 2. The ability to show which hooks are disabled.\n\nThis is a very pleasant series to read. Thank you for spinning these\ncleanups and new features off into their own series. It makes the\nevolution of the hook-config work much easier to follow.\n\nThe overall progression from general cleanups to the more involved\ncache refactoring and finally the new features is logical and well-\nexecuted.\n\nI may have a few comments on the later patches, but the early parts\nlook already very promising.\n\n[PATCH 1/10] to [PATCH 4/10]\nThese look solid and correctly address the style and naming nits\nraised in previous rounds. Moving unsorted_string_list_remove() to\nstring-list.[ch] is a good call as it's a generally useful utility.\n\n[PATCH 5/10] hook: replace hook_list_clear() -> string_list_clear_func()\nAppreciative of this change; using the standard string_list API\nmakes the code more idiomatic. Stashing the data_free pointer in\nstruct hook is a clean way to handle the internal callback data.\n\nThanks.\n"},{"id":"538322","messageId":"xmqqtsuo3bru.fsf@gitster.g","threadId":"65171","inReplyTo":"20260309005416.2760030-9-adrian.ratiu@collabora.com","subject":"Re: [PATCH 08/10] hook: refactor hook_config_cache from strmap to named struct","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-09T21:59:01Z","receivedAt":"2026-03-09T21:59:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":null},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Replace the raw `struct strmap *hook_config_cache` in `struct repository`\n> with a `struct hook_config_cache` which wraps the strmap in a named field.\n>\n> Replace the bare `char *command` util pointer stored in each string_list\n> item with a heap-allocated `struct hook_config_cache_entry` that carries\n> that command string.\n>\n> This is just a refactoring with no behavior changes, to give the cache\n> struct room to grow so it can carry the additional hook metadata we'll\n> be adding in the following commits.\n>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>  hook.c       | 61 +++++++++++++++++++++++++++++++++-------------------\n>  hook.h       | 10 ++++++++-\n>  repository.h |  3 ++-\n>  3 files changed, 50 insertions(+), 24 deletions(-)\n\nHmph, so originally we pointed directly at a \"struct strmap\"\ninstance from inside a \"struct repository\", and the strmap mapped a\nhook's event name to the command to run, which is a raw \"char *\".\n\nIn order to future expand the attribute we keep track of for each\nhook beyond a simple string, we replace the \"char *\" with a pointer\nto \"struct hook_config_cache_entry\".  That part makes quite a lot of\nsense.\n\nHowever, I'm less convinced about the value of wrapping the `strmap`\nitself in a new `struct hook_config_cache`. Since the \"growth\" in\nlater patches happens entirely within the entry struct, this top-level\nwrapper feels like unnecessary boilerplate that adds an extra layer of\nindirection without a clear benefit. It might be better to keep the\ncache as a bare `strmap` and only introduce the entry struct.\n"},{"id":"538323","messageId":"xmqqpl5c3brr.fsf@gitster.g","threadId":"65171","inReplyTo":"20260309005416.2760030-10-adrian.ratiu@collabora.com","subject":"Re: [PATCH 09/10] hook: show config scope in git hook list","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-09T21:59:04Z","receivedAt":"2026-03-09T21:59:06Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":null},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> +list [-z] [--show-scope]::\n>  \tPrint a list of hooks which will be run on `<hook-name>` event. If no\n>  \thooks are configured for that event, print a warning and return 1.\n>  \tUse `-z` to terminate output lines with NUL instead of newlines.\n> ...\n>  \t\tcase HOOK_CONFIGURED:\n> -\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name, line_terminator);\n> +\t\t\tif (show_scope)\n> +\t\t\t\tprintf(\"%s (%s)%c\",\n> +\t\t\t\t       h->u.configured.friendly_name,\n> +\t\t\t\t       config_scope_name(h->u.configured.scope),\n> +\t\t\t\t       line_terminator);\n> +\t\t\telse\n> +\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n> +\t\t\t\t       line_terminator);\n>  \t\t\tbreak;\n\nEverything in this patch was as expected (the most important of\nwhich is where the data is kept, which is in the new structure\nhook_config_cache_entry that was introduced in the previous step for\nuse case like this), except for the above bit.\n\nI wonder if this already interacts well enough with \"-z\", or a minor\ntweak would make it better?  Wouldn't a machine consumer expect that\nfriendly name and cope be given as two separate and easily parseable\nfields in the same record?\n\n"},{"id":"538444","messageId":"87h5qng41o.fsf@gentoo.mail-host-address-is-not-set","threadId":"65171","inReplyTo":"xmqqtsuo3bru.fsf@gitster.g","subject":"Re: [PATCH 08/10] hook: refactor hook_config_cache from strmap to named struct","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-10T14:19:47Z","receivedAt":"2026-03-10T14:20:09Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Mon, 09 Mar 2026, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n>> Replace the raw `struct strmap *hook_config_cache` in `struct repository`\n>> with a `struct hook_config_cache` which wraps the strmap in a named field.\n>>\n>> Replace the bare `char *command` util pointer stored in each string_list\n>> item with a heap-allocated `struct hook_config_cache_entry` that carries\n>> that command string.\n>>\n>> This is just a refactoring with no behavior changes, to give the cache\n>> struct room to grow so it can carry the additional hook metadata we'll\n>> be adding in the following commits.\n>>\n>> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n>> ---\n>>  hook.c       | 61 +++++++++++++++++++++++++++++++++-------------------\n>>  hook.h       | 10 ++++++++-\n>>  repository.h |  3 ++-\n>>  3 files changed, 50 insertions(+), 24 deletions(-)\n>\n> Hmph, so originally we pointed directly at a \"struct strmap\"\n> instance from inside a \"struct repository\", and the strmap mapped a\n> hook's event name to the command to run, which is a raw \"char *\".\n>\n> In order to future expand the attribute we keep track of for each\n> hook beyond a simple string, we replace the \"char *\" with a pointer\n> to \"struct hook_config_cache_entry\".  That part makes quite a lot of\n> sense.\n>\n> However, I'm less convinced about the value of wrapping the `strmap`\n> itself in a new `struct hook_config_cache`. Since the \"growth\" in\n> later patches happens entirely within the entry struct, this top-level\n> wrapper feels like unnecessary boilerplate that adds an extra layer of\n> indirection without a clear benefit. It might be better to keep the\n> cache as a bare `strmap` and only introduce the entry struct.\n\nYes, I think this can work and is also simpler.\n\nWill do in v2, thanks for the suggestion!\n"},{"id":"538445","messageId":"87eclrg3zv.fsf@collabora.com","threadId":"65171","inReplyTo":"CAPig+cRngzGdd-ABWLSiypuJVQ-LVv-xqmn2h+c7qB8_OBpyRg@mail.gmail.com","subject":"Re: [PATCH 05/10] hook: replace hook_list_clear() -> string_list_clear_func()","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-10T14:20:52Z","receivedAt":"2026-03-10T14:21:24Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Sun, 08 Mar 2026, Eric Sunshine <sunshine@sunshineco.com> wrote:\n> On Sun, Mar 8, 2026 at 8:55 PM Adrian Ratiu <adrian.ratiu@collabora.com> wrote:\n>> Replace the custom function with string_list_clear_func() which\n>> is a more common pattern for clearing a string_list.\n>>\n>> To be able to do this, rework hook_clear() into hook_free(), so\n>> it can be passed to string_list_clear_func().\n>>\n>> A slight complication is the need to keep a copy of the internal\n>> cb data free() pointer, however I think it's worth it since the\n>> API becomes cleaner, e.g. no more calls with NULL function args\n>> like hook_list_clear(hooks, NULL).\n>>\n>> Suggested-by: Patrick Steinhardt <ps@pks.im>\n>> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n>> ---\n>> diff --git a/hook.h b/hook.h\n>> @@ -186,10 +194,10 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n>>  /**\n>> + * Frees a struct hook stored as the util pointer of a string_list_item.\n>> + * Suitable for use as a string_list_clear_func_t callback.\n>>   */\n>> +void hook_free(void *p, const char *str UNUSED);\n>\n> See [*] regarding UNUSED in header file.\n>\n> [*]: https://lore.kernel.org/git/xmqqcy1g25fl.fsf@gitster.g/\n\nThank you for pointing this out!\n\nI will fix it in v2 together with your other suggestion.\n"},{"id":"538453","messageId":"87a4wfg37h.fsf@collabora.com","threadId":"65171","inReplyTo":"xmqq1phs69qy.fsf@gitster.g","subject":"Re: [PATCH 00/10] config-hook cleanups and two small 'git hook list' features","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-10T14:37:54Z","receivedAt":"2026-03-10T14:38:12Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Mon, 09 Mar 2026, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n>> As promised I've spun-off v3 of the config series [1] into its own standalone\n>> patch series after v2 landed in next.\n>>\n>> This is mostly minor cleanups and refactorings + two minor feature additions\n>> to `git hook list`, which resulted from the previous review discussions:\n>>\n>> 1. The ability to show the config scope (--show-scope).\n>> 2. The ability to show which hooks are disabled.\n>\n> OK.\n>\n>> This is based on next because the config hooks support is only in next.\n>\n> Not advisable, as doing so would take your topic hostage of _all_\n> other topics in 'next', and it will _never_ happen for all of them,\n> including the merge commit that merged them into 'next', to be\n> merged to 'master'.\n>\n> After learning from the output of\n>\n>     $ git log --first-parent --oneline master..'seen^{/^### match next}' |\n>       grep ar/\n>\n> that ar/config-hooks and ar/run-command-hook-take-2 are the two\n> topic that may be relevant to the config-hook topic in 'next', and\n> knowing that ar/config-hooks fully contains the other topic, I\n> instead did the following to prepare a base:\n>\n>     $ git checkout -b ar/config-hook-cleanups master\n>     $ git merge ar/config-hooks\n>\n> and then applied these 10 patches.  That way, ar/config-hooks can\n> graduate in due course, and then this topic can follow, without\n> waiting for other random things in 'next'.\n\nI'll do something similar in my future series and stop basing patches on\ntop of next (I know it's the second time you're telling me to stop doing\nthis). Creating such a merge-base manually just didn't cross my mind. :)\n\nThanks for the tip.\n"},{"id":"538456","messageId":"878qbzg2ve.fsf@collabora.com","threadId":"65171","inReplyTo":"xmqqpl5c3brr.fsf@gitster.g","subject":"Re: [PATCH 09/10] hook: show config scope in git hook list","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-10T14:45:09Z","receivedAt":"2026-03-10T14:45:27Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Mon, 09 Mar 2026, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n>> +list [-z] [--show-scope]::\n>>  \tPrint a list of hooks which will be run on `<hook-name>` event. If no\n>>  \thooks are configured for that event, print a warning and return 1.\n>>  \tUse `-z` to terminate output lines with NUL instead of newlines.\n>> ...\n>>  \t\tcase HOOK_CONFIGURED:\n>> -\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name, line_terminator);\n>> +\t\t\tif (show_scope)\n>> +\t\t\t\tprintf(\"%s (%s)%c\",\n>> +\t\t\t\t       h->u.configured.friendly_name,\n>> +\t\t\t\t       config_scope_name(h->u.configured.scope),\n>> +\t\t\t\t       line_terminator);\n>> +\t\t\telse\n>> +\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n>> +\t\t\t\t       line_terminator);\n>>  \t\t\tbreak;\n>\n> Everything in this patch was as expected (the most important of\n> which is where the data is kept, which is in the new structure\n> hook_config_cache_entry that was introduced in the previous step for\n> use case like this), except for the above bit.\n>\n> I wonder if this already interacts well enough with \"-z\", or a minor\n> tweak would make it better?  Wouldn't a machine consumer expect that\n> friendly name and cope be given as two separate and easily parseable\n> fields in the same record?\n\nYes, out of all the logic in this series, these printf's are the ones\nI'm most unsure about.\n\nI'll try to come up with something better for machine parsing in v2.\n\nSuggestions are very much welcome btw. :)\n"},{"id":"538511","messageId":"abB3V4BcYDgMJo0x@szeder.dev","threadId":"65171","inReplyTo":"20260309005416.2760030-2-adrian.ratiu@collabora.com","subject":"Re: [PATCH 01/10] hook: move unsorted_string_list_remove() to string-list.[ch]","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-03-10T19:56:07Z","receivedAt":"2026-03-10T19:56:40Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":null},"body":"On Mon, Mar 09, 2026 at 02:54:07AM +0200, Adrian Ratiu wrote:\n> Move the convenience wrapper from hook to string-list since\n> it's a more suitable place. Add a doc comment to the header.\n\nunsorted_string_list_remove() in string-list has a 'free_util'\nparameter that didn't exist in its original version in 'hook.c', but\nit's not mentioned in the commit message.\nFurthermore, none of the function's callsites are adjusted to the new\nparameter, and the build fails with:\n\n  hook.c: In function ‘hook_config_lookup_all’:\n  hook.c:151:33: error: too few arguments to function ‘unsorted_string_list_remove’\n    151 |                                 unsorted_string_list_remove(e->value, hook_name);\n        |                                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n  In file included from hook.h:5,\n                   from hook.c:5:\n  string-list.h:273:6: note: declared here\n    273 | void unsorted_string_list_remove(struct string_list *list, const char *str,\n        |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n  hook.c:163:25: error: too few arguments to function ‘unsorted_string_list_remove’\n    163 |                         unsorted_string_list_remove(hooks, hook_name);\n        |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n  string-list.h:273:6: note: declared here\n    273 | void unsorted_string_list_remove(struct string_list *list, const char *str,\n        |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n  hook.c:180:25: error: too few arguments to function ‘unsorted_string_list_remove’\n    180 |                         unsorted_string_list_remove(&data->disabled_hooks,\n        |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n  string-list.h:273:6: note: declared here\n    273 | void unsorted_string_list_remove(struct string_list *list, const char *str,\n        |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n  make: *** [Makefile:2917: hook.o] Error 1\n\n\n> Suggested-by: Patrick Steinhardt <ps@pks.im>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>  hook.c        | 8 --------\n>  string-list.c | 9 +++++++++\n>  string-list.h | 8 ++++++++\n>  3 files changed, 17 insertions(+), 8 deletions(-)\n> \n> diff --git a/hook.c b/hook.c\n> index 2c8252b2c4..313a6b9937 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -110,14 +110,6 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n>  \tstring_list_append(hook_list, hook_path)->util = h;\n>  }\n>  \n> -static void unsorted_string_list_remove(struct string_list *list,\n> -\t\t\t\t\tconst char *str)\n> -{\n> -\tstruct string_list_item *item = unsorted_string_list_lookup(list, str);\n> -\tif (item)\n> -\t\tunsorted_string_list_delete_item(list, item - list->items, 0);\n> -}\n> -\n>  /*\n>   * Callback struct to collect all hook.* keys in a single config pass.\n>   * commands: friendly-name to command map.\n> diff --git a/string-list.c b/string-list.c\n> index fffa2ad4b6..d260b873c8 100644\n> --- a/string-list.c\n> +++ b/string-list.c\n> @@ -281,6 +281,15 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_\n>  \tlist->nr--;\n>  }\n>  \n> +void unsorted_string_list_remove(struct string_list *list, const char *str,\n> +\t\t\t\t int free_util)\n> +{\n> +\tstruct string_list_item *item = unsorted_string_list_lookup(list, str);\n> +\tif (item)\n> +\t\tunsorted_string_list_delete_item(list, item - list->items,\n> +\t\t\t\t\t\t free_util);\n> +}\n> +\n>  /*\n>   * append a substring [p..end] to list; return number of things it\n>   * appended to the list.\n> diff --git a/string-list.h b/string-list.h\n> index 3ad862a187..b86ee7c099 100644\n> --- a/string-list.h\n> +++ b/string-list.h\n> @@ -265,6 +265,14 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list,\n>   */\n>  void unsorted_string_list_delete_item(struct string_list *list, int i, int free_util);\n>  \n> +/**\n> + * Remove the first item matching `str` from an unsorted string_list.\n> + * No-op if `str` is not found. If `free_util` is non-zero, the `util`\n> + * pointer of the removed item is freed before deletion.\n> + */\n> +void unsorted_string_list_remove(struct string_list *list, const char *str,\n> +\t\t\t\t int free_util);\n> +\n>  /**\n>   * Split string into substrings on characters in `delim` and append the\n>   * substrings to `list`.  The input string is not modified.\n> -- \n> 2.52.0.732.gb351b5166d.dirty\n> \n"},{"id":"538573","messageId":"abFC3ToqvzCZ25h4@pks.im","threadId":"65171","inReplyTo":"20260309005416.2760030-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH 03/10] hook: rename cb_data_free/alloc -> hook_data_free/alloc","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-11T10:24:29Z","receivedAt":"2026-03-11T10:24:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Mon, Mar 09, 2026 at 02:54:09AM +0200, Adrian Ratiu wrote:\n> diff --git a/hook.h b/hook.h\n> index e949f5d488..e514c1b45b 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -43,8 +43,8 @@ struct hook {\n>  \tvoid *feed_pipe_cb_data;\n>  };\n>  \n> -typedef void (*cb_data_free_fn)(void *data);\n> -typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n> +typedef void (*hook_data_free_fn)(void *data);\n> +typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n>  \n>  struct run_hooks_opt\n>  {\n\nAh, this here is also a small style issue that you may want to fix up in\nthe preceding patch. For structures, the curly brace goes on the same\nline.\n\nPatrick\n"},{"id":"538574","messageId":"abFC4uLP-JwDoKde@pks.im","threadId":"65171","inReplyTo":"20260309005416.2760030-10-adrian.ratiu@collabora.com","subject":"Re: [PATCH 09/10] hook: show config scope in git hook list","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-11T10:24:34Z","receivedAt":"2026-03-11T10:24:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Mon, Mar 09, 2026 at 02:54:15AM +0200, Adrian Ratiu wrote:\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index 8fc647a4de..c806640361 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -70,7 +73,14 @@ static int list(int argc, const char **argv, const char *prefix,\n>  \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n>  \t\t\tbreak;\n>  \t\tcase HOOK_CONFIGURED:\n> -\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name, line_terminator);\n> +\t\t\tif (show_scope)\n> +\t\t\t\tprintf(\"%s (%s)%c\",\n> +\t\t\t\t       h->u.configured.friendly_name,\n> +\t\t\t\t       config_scope_name(h->u.configured.scope),\n> +\t\t\t\t       line_terminator);\n\nAre we sure that this is always unambiguous? Can the friendly name for\nexample contain a space itself, or is it possible that the scope gets\nextended eventually so that parsing becomes ambiguous?\n\nI'm not sure about this myself, but that may indicate that we should\nmaybe also separate the name and scope with a NUL byte.\n\n> diff --git a/hook.c b/hook.c\n> index 4fe50aa38c..2c03baeaac 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -172,7 +172,19 @@ static int hook_config_lookup_all(const char *key, const char *value,\n>  \n>  \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n>  \t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n> -\t\t\tstring_list_append(hooks, hook_name);\n> +\n> +\t\t\tif (!ctx->kvi)\n> +\t\t\t\tBUG(\"hook config callback called without key-value info\");\n> +\n> +\t\t\t/*\n> +\t\t\t * Stash the config scope in the util pointer for\n> +\t\t\t * later retrieval in build_hook_config_map(). This\n> +\t\t\t * intermediate struct is transient and never leaves\n> +\t\t\t * that function, so we pack the enum value into the\n> +\t\t\t * pointer rather than heap-allocating a wrapper.\n> +\t\t\t */\n> +\t\t\tstring_list_append(hooks, hook_name)->util =\n> +\t\t\t\t(void *)(uintptr_t)ctx->kvi->scope;\n>  \t\t}\n>  \t} else if (!strcmp(subkey, \"command\")) {\n>  \t\t/* Store command overwriting the old value */\n\nOkay. This is a bit ugly, but I guess it should work in practice? The\nalternative would be to allocate the scope and store the pointer here.\n\nPatrick\n"},{"id":"538575","messageId":"abFC6pEEmicjG6a9@pks.im","threadId":"65171","inReplyTo":"20260309005416.2760030-11-adrian.ratiu@collabora.com","subject":"Re: [PATCH 10/10] hook: show disabled hooks in \"git hook list\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-11T10:24:42Z","receivedAt":"2026-03-11T10:24:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Mon, Mar 09, 2026 at 02:54:16AM +0200, Adrian Ratiu wrote:\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index c806640361..ff446948fa 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -72,16 +72,20 @@ static int list(int argc, const char **argv, const char *prefix,\n>  \t\tcase HOOK_TRADITIONAL:\n>  \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n>  \t\t\tbreak;\n> -\t\tcase HOOK_CONFIGURED:\n> -\t\t\tif (show_scope)\n> -\t\t\t\tprintf(\"%s (%s)%c\",\n> -\t\t\t\t       h->u.configured.friendly_name,\n> -\t\t\t\t       config_scope_name(h->u.configured.scope),\n> +\t\tcase HOOK_CONFIGURED: {\n> +\t\t\tconst char *name = h->u.configured.friendly_name;\n> +\t\t\tconst char *scope = show_scope ?\n> +\t\t\t\tconfig_scope_name(h->u.configured.scope) : NULL;\n> +\t\t\tif (scope)\n> +\t\t\t\tprintf(\"%s (%s%s)%c\", name, scope,\n> +\t\t\t\t       h->u.configured.disabled ? \", disabled\" : \"\",\n>  \t\t\t\t       line_terminator);\n> +\t\t\telse if (h->u.configured.disabled)\n> +\t\t\t\tprintf(\"%s (disabled)%c\", name, line_terminator);\n>  \t\t\telse\n> -\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n> -\t\t\t\t       line_terminator);\n> +\t\t\t\tprintf(\"%s%c\", name, line_terminator);\n>  \t\t\tbreak;\n> +\t\t}\n>  \t\tdefault:\n>  \t\t\tBUG(\"unknown hook kind\");\n>  \t\t}\n\nHm. This starts to feel less and less like an interface that can easily\nbe parsed by a machine, even with \"-z\". I guess this partly comes from\nour insistence to reinvent the wheel in Git instead of just using\nsomething like JSON :/\n\n> diff --git a/hook.c b/hook.c\n> index 2c03baeaac..4f4f060156 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -119,6 +119,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n>  struct hook_config_cache_entry {\n>  \tchar *command;\n>  \tenum config_scope scope;\n> +\tint disabled;\n>  };\n>  \n>  /*\n\nIs there any reason this is an `int` and not a `bool`?\n\n> @@ -217,8 +218,10 @@ static int hook_config_lookup_all(const char *key, const char *value,\n>   * every item's string is the hook's friendly-name and its util pointer is\n>   * a hook_config_cache_entry. All strings are owned by the map.\n>   *\n> - * Disabled hooks and hooks missing a command are already filtered out at\n> - * parse time, so callers can iterate the list directly.\n> + * Disabled hooks are kept in the cache with entry->disabled set, so that\n> + * \"git hook list\" can display them. Hooks missing a command are filtered\n> + * out at build time; if a disabled hook has no command it is silently\n\nWhat exactly does \"build time\" refer to? To me this reads like invoking\nmake :)\n\n> + * skipped rather than triggering a fatal error.\n>   */\n>  void hook_cache_clear(struct hook_config_cache *cache)\n>  {\n> @@ -268,21 +271,26 @@ static void build_hook_config_map(struct repository *r,\n>  \t\t\tstruct hook_config_cache_entry *entry;\n>  \t\t\tchar *command;\n>  \n> -\t\t\t/* filter out disabled hooks */\n> -\t\t\tif (unsorted_string_list_lookup(&cb_data.disabled_hooks,\n> -\t\t\t\t\t\t\thname))\n> -\t\t\t\tcontinue;\n> +\t\t\tint is_disabled =\n> +\t\t\t\t!!unsorted_string_list_lookup(\n> +\t\t\t\t\t&cb_data.disabled_hooks, hname);\n>  \n>  \t\t\tcommand = strmap_get(&cb_data.commands, hname);\n> -\t\t\tif (!command)\n> -\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n> -\t\t\t\t      \"'hook.%s.event' must be removed;\"\n> -\t\t\t\t      \" aborting.\"), hname, hname);\n> +\t\t\tif (!command) {\n> +\t\t\t\tif (is_disabled)\n> +\t\t\t\t\twarning(_(\"disabled hook '%s' has no \"\n> +\t\t\t\t\t\t  \"command configured\"), hname);\n> +\t\t\t\telse\n> +\t\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n> +\t\t\t\t\t      \"'hook.%s.event' must be removed;\"\n> +\t\t\t\t\t      \" aborting.\"), hname, hname);\n> +\t\t\t}\n>  \n>  \t\t\t/* util stores a cache entry; owned by the cache. */\n>  \t\t\tCALLOC_ARRAY(entry, 1);\n> -\t\t\tentry->command = xstrdup(command);\n> +\t\t\tentry->command = command ? xstrdup(command) : NULL;\n\nYou can use `xstrdup_or_null()` here.\n\n>  \t\t\tentry->scope = scope;\n> +\t\t\tentry->disabled = is_disabled;\n>  \t\t\tstring_list_append(hooks, hname)->util = entry;\n>  \t\t}\n>  \n> @@ -401,7 +411,16 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n>  int hook_exists(struct repository *r, const char *name)\n>  {\n>  \tstruct string_list *hooks = list_hooks(r, name, NULL);\n> -\tint exists = hooks->nr > 0;\n> +\tint exists = 0;\n> +\n> +\tfor (size_t i = 0; i < hooks->nr; i++) {\n> +\t\tstruct hook *h = hooks->items[i].util;\n> +\t\tif (h->kind == HOOK_TRADITIONAL ||\n> +\t\t    !h->u.configured.disabled) {\n\nIs the first condition required? I would expect that `disabled` would\nalways be false for traditional hooks.\n\n> +\t\t\texists = 1;\n> +\t\t\tbreak;\n> +\t\t}\n> +\t}\n>  \tstring_list_clear_func(hooks, hook_free);\n>  \tfree(hooks);\n>  \treturn exists;\n> diff --git a/hook.h b/hook.h\n> index 0d711ed21a..0432df963f 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -31,6 +31,7 @@ struct hook {\n>  \t\t\tconst char *friendly_name;\n>  \t\t\tconst char *command;\n>  \t\t\tenum config_scope scope;\n> +\t\t\tint disabled;\n>  \t\t} configured;\n>  \t} u;\n>  \n\nSame question here regarding the type of the struct member.\n\nPatrick\n"},{"id":"538582","messageId":"87ms0eaait.fsf@gentoo.mail-host-address-is-not-set","threadId":"65171","inReplyTo":"abB3V4BcYDgMJo0x@szeder.dev","subject":"Re: [PATCH 01/10] hook: move unsorted_string_list_remove() to string-list.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-11T11:08:42Z","receivedAt":"2026-03-11T11:09:02Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Tue, 10 Mar 2026, SZEDER Gábor <szeder.dev@gmail.com> wrote:\n> On Mon, Mar 09, 2026 at 02:54:07AM +0200, Adrian Ratiu wrote:\n>> Move the convenience wrapper from hook to string-list since\n>> it's a more suitable place. Add a doc comment to the header.\n>\n> unsorted_string_list_remove() in string-list has a 'free_util'\n> parameter that didn't exist in its original version in 'hook.c', but\n> it's not mentioned in the commit message.\n> Furthermore, none of the function's callsites are adjusted to the new\n> parameter, and the build fails with:\n>\n>   hook.c: In function ‘hook_config_lookup_all’:\n>   hook.c:151:33: error: too few arguments to function ‘unsorted_string_list_remove’\n>     151 |                                 unsorted_string_list_remove(e->value, hook_name);\n>         |                                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n>   In file included from hook.h:5,\n>                    from hook.c:5:\n>   string-list.h:273:6: note: declared here\n>     273 | void unsorted_string_list_remove(struct string_list *list, const char *str,\n>         |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n>   hook.c:163:25: error: too few arguments to function ‘unsorted_string_list_remove’\n>     163 |                         unsorted_string_list_remove(hooks, hook_name);\n>         |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n>   string-list.h:273:6: note: declared here\n>     273 | void unsorted_string_list_remove(struct string_list *list, const char *str,\n>         |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n>   hook.c:180:25: error: too few arguments to function ‘unsorted_string_list_remove’\n>     180 |                         unsorted_string_list_remove(&data->disabled_hooks,\n>         |                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n>   string-list.h:273:6: note: declared here\n>     273 | void unsorted_string_list_remove(struct string_list *list, const char *str,\n>         |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~\n>   make: *** [Makefile:2917: hook.o] Error 1\n\nExcellent catch, thanks!\n\nI have been moving a lot of code around for this series and I ended up\nupdating the call sites in a later commit, causing this breakage.\n\nWill fix in v2 as well as document the introduction of the new parameter\nin the commit message.\n"},{"id":"538583","messageId":"87jyviaagp.fsf@collabora.com","threadId":"65171","inReplyTo":"abFC3ToqvzCZ25h4@pks.im","subject":"Re: [PATCH 03/10] hook: rename cb_data_free/alloc -> hook_data_free/alloc","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-11T11:09:58Z","receivedAt":"2026-03-11T11:10:16Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Wed, 11 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Mar 09, 2026 at 02:54:09AM +0200, Adrian Ratiu wrote:\n>> diff --git a/hook.h b/hook.h\n>> index e949f5d488..e514c1b45b 100644\n>> --- a/hook.h\n>> +++ b/hook.h\n>> @@ -43,8 +43,8 @@ struct hook {\n>>  \tvoid *feed_pipe_cb_data;\n>>  };\n>>  \n>> -typedef void (*cb_data_free_fn)(void *data);\n>> -typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n>> +typedef void (*hook_data_free_fn)(void *data);\n>> +typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n>>  \n>>  struct run_hooks_opt\n>>  {\n>\n> Ah, this here is also a small style issue that you may want to fix up in\n> the preceding patch. For structures, the curly brace goes on the same\n> line.\n\nAck, will fix this in v2 together with the rest of style issues pointed\nout. Thanks!\n"},{"id":"538587","messageId":"87h5qma8pe.fsf@collabora.com","threadId":"65171","inReplyTo":"abFC4uLP-JwDoKde@pks.im","subject":"Re: [PATCH 09/10] hook: show config scope in git hook list","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-11T11:47:57Z","receivedAt":"2026-03-11T11:48:16Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Wed, 11 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Mar 09, 2026 at 02:54:15AM +0200, Adrian Ratiu wrote:\n>> diff --git a/builtin/hook.c b/builtin/hook.c\n>> index 8fc647a4de..c806640361 100644\n>> --- a/builtin/hook.c\n>> +++ b/builtin/hook.c\n>> @@ -70,7 +73,14 @@ static int list(int argc, const char **argv, const char *prefix,\n>>  \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n>>  \t\t\tbreak;\n>>  \t\tcase HOOK_CONFIGURED:\n>> -\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name, line_terminator);\n>> +\t\t\tif (show_scope)\n>> +\t\t\t\tprintf(\"%s (%s)%c\",\n>> +\t\t\t\t       h->u.configured.friendly_name,\n>> +\t\t\t\t       config_scope_name(h->u.configured.scope),\n>> +\t\t\t\t       line_terminator);\n>\n> Are we sure that this is always unambiguous? Can the friendly name for\n> example contain a space itself, or is it possible that the scope gets\n> extended eventually so that parsing becomes ambiguous?\n\nIndeed, good catch, yes, friendly-name can contain a space and the scope\ncan also be extended, so this is not very parseable as Junio also\npointed out in the other message.\n\nWe need to come up with something better. :)\n\n> I'm not sure about this myself, but that may indicate that we should\n> maybe also separate the name and scope with a NUL byte.\n\nLooking closer at how git config --show-scope does it, I think we could\nmirror that, i.e. print the scope as a tab separated prefix.\n\nThat would also avoid an awkward intra-line NUL separator, since NUL is\nalready used for line termination with -z.\n\nI have no strong opinions on this btw and am very open to suggestions.\n\n>> +\t\t\tif (!ctx->kvi)\n>> +\t\t\t\tBUG(\"hook config callback called without key-value info\");\n>> +\n>> +\t\t\t/*\n>> +\t\t\t * Stash the config scope in the util pointer for\n>> +\t\t\t * later retrieval in build_hook_config_map(). This\n>> +\t\t\t * intermediate struct is transient and never leaves\n>> +\t\t\t * that function, so we pack the enum value into the\n>> +\t\t\t * pointer rather than heap-allocating a wrapper.\n>> +\t\t\t */\n>> +\t\t\tstring_list_append(hooks, hook_name)->util =\n>> +\t\t\t\t(void *)(uintptr_t)ctx->kvi->scope;\n>>  \t\t}\n>>  \t} else if (!strcmp(subkey, \"command\")) {\n>>  \t\t/* Store command overwriting the old value */\n>\n> Okay. This is a bit ugly, but I guess it should work in practice? The\n> alternative would be to allocate the scope and store the pointer here.\n\nYes, I just tried the simplest thing and it worked. If we need more than\njust the scope, then we could heap-allocate a struct wrapper and deal\nwith the associated memory management complexity.\n"},{"id":"538590","messageId":"87eclqa70x.fsf@collabora.com","threadId":"65171","inReplyTo":"abFC6pEEmicjG6a9@pks.im","subject":"Re: [PATCH 10/10] hook: show disabled hooks in \"git hook list\"","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-11T12:24:14Z","receivedAt":"2026-03-11T12:24:33Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Wed, 11 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Mar 09, 2026 at 02:54:16AM +0200, Adrian Ratiu wrote:\n>> diff --git a/builtin/hook.c b/builtin/hook.c\n>> index c806640361..ff446948fa 100644\n>> --- a/builtin/hook.c\n>> +++ b/builtin/hook.c\n>> @@ -72,16 +72,20 @@ static int list(int argc, const char **argv, const char *prefix,\n>>  \t\tcase HOOK_TRADITIONAL:\n>>  \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n>>  \t\t\tbreak;\n>> -\t\tcase HOOK_CONFIGURED:\n>> -\t\t\tif (show_scope)\n>> -\t\t\t\tprintf(\"%s (%s)%c\",\n>> -\t\t\t\t       h->u.configured.friendly_name,\n>> -\t\t\t\t       config_scope_name(h->u.configured.scope),\n>> +\t\tcase HOOK_CONFIGURED: {\n>> +\t\t\tconst char *name = h->u.configured.friendly_name;\n>> +\t\t\tconst char *scope = show_scope ?\n>> +\t\t\t\tconfig_scope_name(h->u.configured.scope) : NULL;\n>> +\t\t\tif (scope)\n>> +\t\t\t\tprintf(\"%s (%s%s)%c\", name, scope,\n>> +\t\t\t\t       h->u.configured.disabled ? \", disabled\" : \"\",\n>>  \t\t\t\t       line_terminator);\n>> +\t\t\telse if (h->u.configured.disabled)\n>> +\t\t\t\tprintf(\"%s (disabled)%c\", name, line_terminator);\n>>  \t\t\telse\n>> -\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n>> -\t\t\t\t       line_terminator);\n>> +\t\t\t\tprintf(\"%s%c\", name, line_terminator);\n>>  \t\t\tbreak;\n>> +\t\t}\n>>  \t\tdefault:\n>>  \t\t\tBUG(\"unknown hook kind\");\n>>  \t\t}\n>\n> Hm. This starts to feel less and less like an interface that can easily\n> be parsed by a machine, even with \"-z\". I guess this partly comes from\n> our insistence to reinvent the wheel in Git instead of just using\n> something like JSON :/\n\nYes, I agree, a structured output format like JSON would be ideal in\nthis case.\n\nPlease see my previous patch suggestion of mirroring the existing git\nconfig --show-scope by using tab separated prefixes. Maybe we could do\nthat here as well.\n\nSuggestions welcome.\n\n>> diff --git a/hook.c b/hook.c\n>> index 2c03baeaac..4f4f060156 100644\n>> --- a/hook.c\n>> +++ b/hook.c\n>> @@ -119,6 +119,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n>>  struct hook_config_cache_entry {\n>>  \tchar *command;\n>>  \tenum config_scope scope;\n>> +\tint disabled;\n>>  };\n>>  \n>>  /*\n>\n> Is there any reason this is an `int` and not a `bool`?\n\nNo, I'll change it to bool in v2, it was just an oversight on my part.\n\n>> @@ -217,8 +218,10 @@ static int hook_config_lookup_all(const char *key, const char *value,\n>>   * every item's string is the hook's friendly-name and its util pointer is\n>>   * a hook_config_cache_entry. All strings are owned by the map.\n>>   *\n>> - * Disabled hooks and hooks missing a command are already filtered out at\n>> - * parse time, so callers can iterate the list directly.\n>> + * Disabled hooks are kept in the cache with entry->disabled set, so that\n>> + * \"git hook list\" can display them. Hooks missing a command are filtered\n>> + * out at build time; if a disabled hook has no command it is silently\n>\n> What exactly does \"build time\" refer to? To me this reads like invoking\n> make :)\n\nThis is one of my pet peeves: I (mis)use \"build time\" a lot.\n\nbuild time in this case == cache construction time. :)\n\nThe comment is wrong anyway, because in the end I decided to issue a\nwarning (see code immediately below) instead of silently ignoring.\n\nI'll reword this in v2. Thanks!\n\n>> + * skipped rather than triggering a fatal error.\n>>   */\n>>  void hook_cache_clear(struct hook_config_cache *cache)\n>>  {\n>> @@ -268,21 +271,26 @@ static void build_hook_config_map(struct repository *r,\n>>  \t\t\tstruct hook_config_cache_entry *entry;\n>>  \t\t\tchar *command;\n>>  \n>> -\t\t\t/* filter out disabled hooks */\n>> -\t\t\tif (unsorted_string_list_lookup(&cb_data.disabled_hooks,\n>> -\t\t\t\t\t\t\thname))\n>> -\t\t\t\tcontinue;\n>> +\t\t\tint is_disabled =\n>> +\t\t\t\t!!unsorted_string_list_lookup(\n>> +\t\t\t\t\t&cb_data.disabled_hooks, hname);\n>>  \n>>  \t\t\tcommand = strmap_get(&cb_data.commands, hname);\n>> -\t\t\tif (!command)\n>> -\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n>> -\t\t\t\t      \"'hook.%s.event' must be removed;\"\n>> -\t\t\t\t      \" aborting.\"), hname, hname);\n>> +\t\t\tif (!command) {\n>> +\t\t\t\tif (is_disabled)\n>> +\t\t\t\t\twarning(_(\"disabled hook '%s' has no \"\n>> +\t\t\t\t\t\t  \"command configured\"), hname);\n>> +\t\t\t\telse\n>> +\t\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n>> +\t\t\t\t\t      \"'hook.%s.event' must be removed;\"\n>> +\t\t\t\t\t      \" aborting.\"), hname, hname);\n>> +\t\t\t}\n>>  \n>>  \t\t\t/* util stores a cache entry; owned by the cache. */\n>>  \t\t\tCALLOC_ARRAY(entry, 1);\n>> -\t\t\tentry->command = xstrdup(command);\n>> +\t\t\tentry->command = command ? xstrdup(command) : NULL;\n>\n> You can use `xstrdup_or_null()` here.\n\nAck, will do in v2.\n\n>>  \t\t\tentry->scope = scope;\n>> +\t\t\tentry->disabled = is_disabled;\n>>  \t\t\tstring_list_append(hooks, hname)->util = entry;\n>>  \t\t}\n>>  \n>> @@ -401,7 +411,16 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n>>  int hook_exists(struct repository *r, const char *name)\n>>  {\n>>  \tstruct string_list *hooks = list_hooks(r, name, NULL);\n>> -\tint exists = hooks->nr > 0;\n>> +\tint exists = 0;\n>> +\n>> +\tfor (size_t i = 0; i < hooks->nr; i++) {\n>> +\t\tstruct hook *h = hooks->items[i].util;\n>> +\t\tif (h->kind == HOOK_TRADITIONAL ||\n>> +\t\t    !h->u.configured.disabled) {\n>\n> Is the first condition required? I would expect that `disabled` would\n> always be false for traditional hooks.\n\nYes, it is required because !h->u.configured.disabled only applies to\nnon-traditional (configured) hooks.\n\nThe \"traditional\" member of the union doesn't even have a disabled\nfield, when h->kind == HOOK_TRADITIONAL => always exists = 1;\n\nBasically we're checking two different types here, if they exist and\nshort-circuiting in the traditional hook case.\n\nHope that explanation makes sense.\n\nI'll see if I can make this condition clearer in v2.\n\n\n>> +\t\t\texists = 1;\n>> +\t\t\tbreak;\n>> +\t\t}\n>> +\t}\n>>  \tstring_list_clear_func(hooks, hook_free);\n>>  \tfree(hooks);\n>>  \treturn exists;\n>> diff --git a/hook.h b/hook.h\n>> index 0d711ed21a..0432df963f 100644\n>> --- a/hook.h\n>> +++ b/hook.h\n>> @@ -31,6 +31,7 @@ struct hook {\n>>  \t\t\tconst char *friendly_name;\n>>  \t\t\tconst char *command;\n>>  \t\t\tenum config_scope scope;\n>> +\t\t\tint disabled;\n>>  \t\t} configured;\n>>  \t} u;\n>>  \n>\n> Same question here regarding the type of the struct member.\n\nYes, it can be a bool. Will do in v2.\n"},{"id":"538602","messageId":"abFz2sav7FLv8hOG@pks.im","threadId":"65171","inReplyTo":"87eclqa70x.fsf@collabora.com","subject":"Re: [PATCH 10/10] hook: show disabled hooks in \"git hook list\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-11T13:53:30Z","receivedAt":"2026-03-11T13:53:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Wed, Mar 11, 2026 at 02:24:14PM +0200, Adrian Ratiu wrote:\n> On Wed, 11 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> > On Mon, Mar 09, 2026 at 02:54:16AM +0200, Adrian Ratiu wrote:\n> >> diff --git a/builtin/hook.c b/builtin/hook.c\n> >> index c806640361..ff446948fa 100644\n> >> --- a/builtin/hook.c\n> >> +++ b/builtin/hook.c\n> >> @@ -72,16 +72,20 @@ static int list(int argc, const char **argv, const char *prefix,\n> >>  \t\tcase HOOK_TRADITIONAL:\n> >>  \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n> >>  \t\t\tbreak;\n> >> -\t\tcase HOOK_CONFIGURED:\n> >> -\t\t\tif (show_scope)\n> >> -\t\t\t\tprintf(\"%s (%s)%c\",\n> >> -\t\t\t\t       h->u.configured.friendly_name,\n> >> -\t\t\t\t       config_scope_name(h->u.configured.scope),\n> >> +\t\tcase HOOK_CONFIGURED: {\n> >> +\t\t\tconst char *name = h->u.configured.friendly_name;\n> >> +\t\t\tconst char *scope = show_scope ?\n> >> +\t\t\t\tconfig_scope_name(h->u.configured.scope) : NULL;\n> >> +\t\t\tif (scope)\n> >> +\t\t\t\tprintf(\"%s (%s%s)%c\", name, scope,\n> >> +\t\t\t\t       h->u.configured.disabled ? \", disabled\" : \"\",\n> >>  \t\t\t\t       line_terminator);\n> >> +\t\t\telse if (h->u.configured.disabled)\n> >> +\t\t\t\tprintf(\"%s (disabled)%c\", name, line_terminator);\n> >>  \t\t\telse\n> >> -\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n> >> -\t\t\t\t       line_terminator);\n> >> +\t\t\t\tprintf(\"%s%c\", name, line_terminator);\n> >>  \t\t\tbreak;\n> >> +\t\t}\n> >>  \t\tdefault:\n> >>  \t\t\tBUG(\"unknown hook kind\");\n> >>  \t\t}\n> >\n> > Hm. This starts to feel less and less like an interface that can easily\n> > be parsed by a machine, even with \"-z\". I guess this partly comes from\n> > our insistence to reinvent the wheel in Git instead of just using\n> > something like JSON :/\n> \n> Yes, I agree, a structured output format like JSON would be ideal in\n> this case.\n> \n> Please see my previous patch suggestion of mirroring the existing git\n> config --show-scope by using tab separated prefixes. Maybe we could do\n> that here as well.\n\nYeah, I think doing it similarly makes sense.\n\nPatrick\n"},{"id":"539529","messageId":"20260320115211.177351-1-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 00/10] config-hook cleanups and two small 'git hook list' features","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:01Z","receivedAt":"2026-03-20T11:53:01Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Hello everyone,\n\nv2 addresses all the feedback received in v1, many thanks to everyone\nwho contributed.\n\nThis series is just minor cleanups / refactorings + two minor feature additions\nto `git hook list`, which resulted from the previous series review discussions:\n\n1. The ability to show the config scope (--show-scope).\n2. The ability to show which hooks are disabled.\n\nThis is now based on the master branch.\n\nI have pushed the branch to Github [1] and provided a clean CI run [2] with\nthe exception of a known breakage for some MacOS builders (REG_ENHANCED).\n\nThanks again,\nAdrian\n\n1: https://github.com/10ne1/git/tree/dev/aratiu/config-cleanups-v2\n2: https://github.com/10ne1/git/actions/runs/23340298770\n\nChanges in v2:\n* Cleanly rebased on master, no conflicts (Adrian)\n* Fix first patch build break by updating call-sites in same commit (Szeder)\n* Drop UNUSED from function declaration in the header file (Eric)\n* Drop the new struct hook_config_cache because it's redundant (Junio)\n* git hook list now prints in tab separated output format similar\n  to git config --show-scope to improve machine parseability (Junio, Patrick)\n* Fix small style issues, comments, type, commit messages. (Eric, Patrick)\n\nRange-diff v1 -> v2:\n 1:  6f4ac0ddf8 !  1:  aeefa72f33 hook: move unsorted_string_list_remove() to string-list.[ch]\n    @@ Commit message\n         Move the convenience wrapper from hook to string-list since\n         it's a more suitable place. Add a doc comment to the header.\n     \n    +    Also add a free_util arg to make the function more generic\n    +    and make the API similar to other functions in string-list.h.\n    +    Update the existing call-sites.\n    +\n         Suggested-by: Patrick Steinhardt <ps@pks.im>\n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n    @@ hook.c: static void list_hooks_add_default(struct repository *r, const char *hoo\n      /*\n       * Callback struct to collect all hook.* keys in a single config pass.\n       * commands: friendly-name to command map.\n    +@@ hook.c: static int hook_config_lookup_all(const char *key, const char *value,\n    + \t\t\tstruct strmap_entry *e;\n    + \n    + \t\t\tstrmap_for_each_entry(&data->event_hooks, &iter, e)\n    +-\t\t\t\tunsorted_string_list_remove(e->value, hook_name);\n    ++\t\t\t\tunsorted_string_list_remove(e->value, hook_name, 0);\n    + \t\t} else {\n    + \t\t\tstruct string_list *hooks =\n    + \t\t\t\tstrmap_get(&data->event_hooks, value);\n    +@@ hook.c: static int hook_config_lookup_all(const char *key, const char *value,\n    + \t\t\t}\n    + \n    + \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n    +-\t\t\tunsorted_string_list_remove(hooks, hook_name);\n    ++\t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n    + \t\t\tstring_list_append(hooks, hook_name);\n    + \t\t}\n    + \t} else if (!strcmp(subkey, \"command\")) {\n    +@@ hook.c: static int hook_config_lookup_all(const char *key, const char *value,\n    + \t\t\tbreak;\n    + \t\tcase 1: /* enabled: undo a prior disabled entry */\n    + \t\t\tunsorted_string_list_remove(&data->disabled_hooks,\n    +-\t\t\t\t\t\t    hook_name);\n    ++\t\t\t\t\t\t    hook_name, 0);\n    + \t\t\tbreak;\n    + \t\tdefault:\n    + \t\t\tbreak; /* ignore unrecognised values */\n     \n      ## string-list.c ##\n     @@ string-list.c: void unsorted_string_list_delete_item(struct string_list *list, int i, int free_\n 2:  df876b8bc5 !  2:  90e821bdfa hook: fix minor style issues\n    @@ Metadata\n      ## Commit message ##\n         hook: fix minor style issues\n     \n    -    Fix some minor style nits pointed by Patrick and Junio:\n    +    Fix some minor style nits pointed by Patrick, Junio and Eric:\n           * Use CALLOC_ARRAY instead of xcalloc.\n           * Init struct members during declaration.\n           * Simplify if condition boolean logic.\n           * Missing curly braces in if/else stmts.\n           * Unnecessary header includes.\n    -      * Capitalization in error/warn messages.\n    +      * Capitalization and full-stop in error/warn messages.\n    +      * Curly brace on separate line when defining struct.\n           * Comment spelling: free'd -> freed.\n     \n         These contain no logic changes, the code behaves the same as before.\n     \n    +    Suggested-by: Eric Sunshine <sunshine@sunshineco.com>\n         Suggested-by: Junio C Hamano <gitster@pobox.com>\n         Suggested-by: Patrick Steinhardt <ps@pks.im>\n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix,\n      \t */\n      \tif (argc != 1)\n     -\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n    -+\t\tusage_msg_opt(_(\"you must specify a hook event name to list.\"),\n    ++\t\tusage_msg_opt(_(\"you must specify a hook event name to list\"),\n      \t\t\t      builtin_hook_list_usage, list_options);\n      \n      \thookname = argv[0];\n    @@ hook.c: int run_hooks_opt(struct repository *r, const char *hook_name,\n      \n      \tif (options->invoked_hook)\n     \n    + ## hook.h ##\n    +@@ hook.h: struct hook {\n    + typedef void (*cb_data_free_fn)(void *data);\n    + typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n    + \n    +-struct run_hooks_opt\n    +-{\n    ++struct run_hooks_opt {\n    + \t/* Environment vars to be set for each hook */\n    + \tstruct strvec env;\n    + \n    +\n      ## refs.c ##\n     @@ refs.c: static int transaction_hook_feed_stdin(int hook_stdin_fd, void *pp_cb, void *pp_\n      \n 3:  7cf6e32087 !  3:  dee1dd49a4 hook: rename cb_data_free/alloc -> hook_data_free/alloc\n    @@ hook.h: struct hook {\n     +typedef void (*hook_data_free_fn)(void *data);\n     +typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n      \n    - struct run_hooks_opt\n    - {\n    -@@ hook.h: struct run_hooks_opt\n    + struct run_hooks_opt {\n    + \t/* Environment vars to be set for each hook */\n    +@@ hook.h: struct run_hooks_opt {\n      \t *\n      \t * The `feed_pipe_ctx` pointer can be used to pass initialization data.\n      \t */\n 4:  06a9f3bfcb =  4:  86f61204b3 hook: detect & emit two more bugs\n 5:  47190d7a22 !  5:  30542de351 hook: replace hook_list_clear() -> string_list_clear_func()\n    @@ hook.c: static void list_hooks_add_default(struct repository *r, const char *hoo\n      \n      \th->kind = HOOK_TRADITIONAL;\n      \th->u.traditional.path = xstrdup(hook_path);\n    -@@ hook.c: static int hook_config_lookup_all(const char *key, const char *value,\n    - \t\t\tstruct strmap_entry *e;\n    - \n    - \t\t\tstrmap_for_each_entry(&data->event_hooks, &iter, e)\n    --\t\t\t\tunsorted_string_list_remove(e->value, hook_name);\n    -+\t\t\t\tunsorted_string_list_remove(e->value, hook_name, 0);\n    - \t\t} else {\n    - \t\t\tstruct string_list *hooks =\n    - \t\t\t\tstrmap_get(&data->event_hooks, value);\n    -@@ hook.c: static int hook_config_lookup_all(const char *key, const char *value,\n    - \t\t\t}\n    - \n    - \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n    --\t\t\tunsorted_string_list_remove(hooks, hook_name);\n    -+\t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n    - \t\t\tstring_list_append(hooks, hook_name);\n    - \t\t}\n    - \t} else if (!strcmp(subkey, \"command\")) {\n    -@@ hook.c: static int hook_config_lookup_all(const char *key, const char *value,\n    - \t\t\tbreak;\n    - \t\tcase 1: /* enabled: undo a prior disabled entry */\n    - \t\t\tunsorted_string_list_remove(&data->disabled_hooks,\n    --\t\t\t\t\t\t    hook_name);\n    -+\t\t\t\t\t\t    hook_name, 0);\n    - \t\t\tbreak;\n    - \t\tdefault:\n    - \t\t\tbreak; /* ignore unrecognised values */\n     @@ hook.c: static void list_hooks_add_configured(struct repository *r,\n      \t\tstruct hook *hook;\n      \t\tCALLOC_ARRAY(hook, 1);\n    @@ hook.h: struct hook {\n     +\thook_data_free_fn data_free;\n     +};\n      \n    - struct run_hooks_opt\n    - {\n    + struct run_hooks_opt {\n    + \t/* Environment vars to be set for each hook */\n     @@ hook.h: struct string_list *list_hooks(struct repository *r, const char *hookname,\n      \t\t\t       struct run_hooks_opt *options);\n      \n    @@ hook.h: struct string_list *list_hooks(struct repository *r, const char *hooknam\n     + * Suitable for use as a string_list_clear_func_t callback.\n       */\n     -void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free);\n    -+void hook_free(void *p, const char *str UNUSED);\n    ++void hook_free(void *p, const char *str);\n      \n      /**\n       * Frees the hook configuration cache stored in `struct repository`.\n 6:  469b8a7192 =  6:  4e1374b84e hook: make consistent use of friendly-name in docs\n 7:  dff30a5c06 =  7:  075f8202aa t1800: add test to verify hook execution ordering\n 8:  e53a000982 <  -:  ---------- hook: refactor hook_config_cache from strmap to named struct\n -:  ---------- >  8:  8f948bbbe7 hook: introduce hook_config_cache_entry for per-hook data\n 9:  7d7753b48a !  9:  dbf81604ed hook: show config scope in git hook list\n    @@ Commit message\n     \n         Without the flag the output is unchanged.\n     \n    +    The scope is printed as a tab-separated prefix (like \"git config --show-scope\"),\n    +    making it unambiguously machine-parseable even when the friendly name\n    +    contains spaces.\n    +\n         Example usage:\n         $ git hook list --show-scope pre-commit\n    -    linter (global)\n    -    no-leaks (local)\n    +    global  linter\n    +    local   no-leaks\n         hook from hookdir\n     \n         Traditional hooks from the hookdir are unaffected by --show-scope since\n    @@ Documentation/git-hook.adoc: OPTIONS\n      \tTerminate \"list\" output lines with NUL instead of newlines.\n      \n     +--show-scope::\n    -+\tFor \"list\"; print the config scope (e.g. `local`, `global`, `system`)\n    -+\tin parentheses after the friendly name of each configured hook, to show\n    -+\twhere it was defined. Traditional hooks from the hookdir are unaffected.\n    ++\tFor \"list\"; prefix each configured hook's friendly name with a\n    ++\ttab-separated config scope (e.g. `local`, `global`, `system`),\n    ++\tmirroring the output style of `git config --show-scope`. Traditional\n    ++\thooks from the hookdir are unaffected.\n     +\n      WRAPPERS\n      --------\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix,\n      \t\tcase HOOK_CONFIGURED:\n     -\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name, line_terminator);\n     +\t\t\tif (show_scope)\n    -+\t\t\t\tprintf(\"%s (%s)%c\",\n    -+\t\t\t\t       h->u.configured.friendly_name,\n    ++\t\t\t\tprintf(\"%s\\t%s%c\",\n     +\t\t\t\t       config_scope_name(h->u.configured.scope),\n    ++\t\t\t\t       h->u.configured.friendly_name,\n     +\t\t\t\t       line_terminator);\n     +\t\t\telse\n     +\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n    @@ hook.c: static int hook_config_lookup_all(const char *key, const char *value,\n      \t\t}\n      \t} else if (!strcmp(subkey, \"command\")) {\n      \t\t/* Store command overwriting the old value */\n    -@@ hook.c: static void build_hook_config_map(struct repository *r,\n    +@@ hook.c: static void build_hook_config_map(struct repository *r, struct strmap *cache)\n      \n      \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n      \t\t\tconst char *hname = hook_names->items[i].string;\n    @@ hook.c: static void build_hook_config_map(struct repository *r,\n      \t\t\tstruct hook_config_cache_entry *entry;\n      \t\t\tchar *command;\n      \n    -@@ hook.c: static void build_hook_config_map(struct repository *r,\n    +@@ hook.c: static void build_hook_config_map(struct repository *r, struct strmap *cache)\n      \t\t\t/* util stores a cache entry; owned by the cache. */\n      \t\t\tCALLOC_ARRAY(entry, 1);\n      \t\t\tentry->command = xstrdup(command);\n    @@ t/t1800-hook.sh: test_expect_success 'configured hooks run before hookdir hook'\n     +\ttest_config hook.local-hook.event test-hook --add &&\n     +\n     +\tcat >expected <<-\\EOF &&\n    -+\tglobal-hook (global)\n    -+\tlocal-hook (local)\n    ++\tglobal\tglobal-hook\n    ++\tlocal\tlocal-hook\n     +\tEOF\n     +\tgit hook list --show-scope test-hook >actual &&\n     +\ttest_cmp expected actual &&\n     +\n     +\t# without --show-scope the scope must not appear\n     +\tgit hook list test-hook >actual &&\n    -+\ttest_grep ! \"(global)\" actual &&\n    -+\ttest_grep ! \"(local)\" actual\n    ++\ttest_grep ! \"^global\t\" actual &&\n    ++\ttest_grep ! \"^local\t\" actual\n     +'\n     +\n      test_expect_success 'git hook run a hook with a bad shebang' '\n10:  dabb17aad6 ! 10:  2a67244b20 hook: show disabled hooks in \"git hook list\"\n    @@ Commit message\n         invisible to \"git hook list\". Keep them in the cache with a new\n         \"disabled\" flag which is propagated to the respective struct hook.\n     \n    -    \"git hook list\" now shows disabled hooks annotated with \"(disabled)\"\n    -    in the config order. With --show-scope, it looks like:\n    +    \"git hook list\" now shows disabled hooks as tab-separated columns,\n    +    with the status as a prefix before the name (like scope with\n    +    --show-scope). With --show-scope it looks like:\n     \n         $ git hook list --show-scope pre-commit\n    -    linter (global)\n    -    no-leaks (local, disabled)\n    +    global  linter\n    +    local   disabled        no-leaks\n         hook from hookdir\n     \n         A disabled hook without a command issues a warning instead of the\n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix,\n      \t\t\tbreak;\n     -\t\tcase HOOK_CONFIGURED:\n     -\t\t\tif (show_scope)\n    --\t\t\t\tprintf(\"%s (%s)%c\",\n    --\t\t\t\t       h->u.configured.friendly_name,\n    +-\t\t\t\tprintf(\"%s\\t%s%c\",\n     -\t\t\t\t       config_scope_name(h->u.configured.scope),\n    +-\t\t\t\t       h->u.configured.friendly_name,\n    +-\t\t\t\t       line_terminator);\n     +\t\tcase HOOK_CONFIGURED: {\n     +\t\t\tconst char *name = h->u.configured.friendly_name;\n     +\t\t\tconst char *scope = show_scope ?\n     +\t\t\t\tconfig_scope_name(h->u.configured.scope) : NULL;\n     +\t\t\tif (scope)\n    -+\t\t\t\tprintf(\"%s (%s%s)%c\", name, scope,\n    -+\t\t\t\t       h->u.configured.disabled ? \", disabled\" : \"\",\n    - \t\t\t\t       line_terminator);\n    -+\t\t\telse if (h->u.configured.disabled)\n    -+\t\t\t\tprintf(\"%s (disabled)%c\", name, line_terminator);\n    ++\t\t\t\tprintf(\"%s\\t%s%s%c\", scope,\n    ++\t\t\t\t       h->u.configured.disabled ? \"disabled\\t\" : \"\",\n    ++\t\t\t\t       name, line_terminator);\n      \t\t\telse\n     -\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n     -\t\t\t\t       line_terminator);\n    -+\t\t\t\tprintf(\"%s%c\", name, line_terminator);\n    ++\t\t\t\tprintf(\"%s%s%c\",\n    ++\t\t\t\t       h->u.configured.disabled ? \"disabled\\t\" : \"\",\n    ++\t\t\t\t       name, line_terminator);\n      \t\t\tbreak;\n     +\t\t}\n      \t\tdefault:\n    @@ hook.c: static void list_hooks_add_default(struct repository *r, const char *hoo\n      struct hook_config_cache_entry {\n      \tchar *command;\n      \tenum config_scope scope;\n    -+\tint disabled;\n    ++\tunsigned int disabled:1;\n      };\n      \n      /*\n     @@ hook.c: static int hook_config_lookup_all(const char *key, const char *value,\n       * every item's string is the hook's friendly-name and its util pointer is\n    -  * a hook_config_cache_entry. All strings are owned by the map.\n    +  * the corresponding command string. Both strings are owned by the map.\n       *\n     - * Disabled hooks and hooks missing a command are already filtered out at\n     - * parse time, so callers can iterate the list directly.\n     + * Disabled hooks are kept in the cache with entry->disabled set, so that\n    -+ * \"git hook list\" can display them. Hooks missing a command are filtered\n    -+ * out at build time; if a disabled hook has no command it is silently\n    -+ * skipped rather than triggering a fatal error.\n    ++ * \"git hook list\" can display them. A non-disabled hook missing a command\n    ++ * is fatal; a disabled hook missing a command emits a warning and is kept\n    ++ * in the cache with entry->command = NULL.\n       */\n    - void hook_cache_clear(struct hook_config_cache *cache)\n    + void hook_cache_clear(struct strmap *cache)\n      {\n    -@@ hook.c: static void build_hook_config_map(struct repository *r,\n    +@@ hook.c: static void build_hook_config_map(struct repository *r, struct strmap *cache)\n      \t\t\tstruct hook_config_cache_entry *entry;\n      \t\t\tchar *command;\n      \n    @@ hook.c: static void build_hook_config_map(struct repository *r,\n      \t\t\t/* util stores a cache entry; owned by the cache. */\n      \t\t\tCALLOC_ARRAY(entry, 1);\n     -\t\t\tentry->command = xstrdup(command);\n    -+\t\t\tentry->command = command ? xstrdup(command) : NULL;\n    ++\t\t\tentry->command = xstrdup_or_null(command);\n      \t\t\tentry->scope = scope;\n     +\t\t\tentry->disabled = is_disabled;\n      \t\t\tstring_list_append(hooks, hname)->util = entry;\n    @@ hook.h: struct hook {\n      \t\t\tconst char *friendly_name;\n      \t\t\tconst char *command;\n      \t\t\tenum config_scope scope;\n    -+\t\t\tint disabled;\n    ++\t\t\tunsigned int disabled:1;\n      \t\t} configured;\n      \t} u;\n      \n    @@ t/t1800-hook.sh: test_expect_success 'disabled hook does not appear in git hook\n     -\ttest_grep \"active\" actual &&\n     -\ttest_grep ! \"inactive\" actual\n     +\ttest_grep \"^active$\" actual &&\n    -+\ttest_grep \"^inactive (disabled)$\" actual\n    ++\ttest_grep \"^disabled\tinactive$\" actual\n     +'\n     +\n     +test_expect_success 'disabled hook shows scope with --show-scope' '\n    @@ t/t1800-hook.sh: test_expect_success 'disabled hook does not appear in git hook\n     +\ttest_config hook.myhook.enabled false &&\n     +\n     +\tgit hook list --show-scope pre-commit >actual &&\n    -+\ttest_grep \"myhook (local, disabled)\" actual\n    ++\ttest_grep \"^local\tdisabled\tmyhook$\" actual\n     +'\n     +\n     +test_expect_success 'disabled configured hook is not reported as existing by hook_exists' '\n\nAdrian Ratiu (10):\n  hook: move unsorted_string_list_remove() to string-list.[ch]\n  hook: fix minor style issues\n  hook: rename cb_data_free/alloc -> hook_data_free/alloc\n  hook: detect & emit two more bugs\n  hook: replace hook_list_clear() -> string_list_clear_func()\n  hook: make consistent use of friendly-name in docs\n  t1800: add test to verify hook execution ordering\n  hook: introduce hook_config_cache_entry for per-hook data\n  hook: show config scope in git hook list\n  hook: show disabled hooks in \"git hook list\"\n\n Documentation/config/hook.adoc |  30 +++---\n Documentation/git-hook.adoc    |  16 ++-\n builtin/hook.c                 |  28 +++--\n builtin/receive-pack.c         |  11 +-\n hook.c                         | 187 +++++++++++++++++++++------------\n hook.h                         |  32 ++++--\n refs.c                         |   3 +-\n string-list.c                  |   9 ++\n string-list.h                  |   8 ++\n t/t1800-hook.sh                |  83 ++++++++++++++-\n transport.c                    |   3 +-\n 11 files changed, 295 insertions(+), 115 deletions(-)\n\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539530","messageId":"20260320115211.177351-2-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 01/10] hook: move unsorted_string_list_remove() to string-list.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:02Z","receivedAt":"2026-03-20T11:53:04Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Move the convenience wrapper from hook to string-list since\nit's a more suitable place. Add a doc comment to the header.\n\nAlso add a free_util arg to make the function more generic\nand make the API similar to other functions in string-list.h.\nUpdate the existing call-sites.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c        | 14 +++-----------\n string-list.c |  9 +++++++++\n string-list.h |  8 ++++++++\n 3 files changed, 20 insertions(+), 11 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 2c8252b2c4..67cc9a66df 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -110,14 +110,6 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tstring_list_append(hook_list, hook_path)->util = h;\n }\n \n-static void unsorted_string_list_remove(struct string_list *list,\n-\t\t\t\t\tconst char *str)\n-{\n-\tstruct string_list_item *item = unsorted_string_list_lookup(list, str);\n-\tif (item)\n-\t\tunsorted_string_list_delete_item(list, item - list->items, 0);\n-}\n-\n /*\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\n@@ -156,7 +148,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\tstruct strmap_entry *e;\n \n \t\t\tstrmap_for_each_entry(&data->event_hooks, &iter, e)\n-\t\t\t\tunsorted_string_list_remove(e->value, hook_name);\n+\t\t\t\tunsorted_string_list_remove(e->value, hook_name, 0);\n \t\t} else {\n \t\t\tstruct string_list *hooks =\n \t\t\t\tstrmap_get(&data->event_hooks, value);\n@@ -168,7 +160,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\t}\n \n \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n-\t\t\tunsorted_string_list_remove(hooks, hook_name);\n+\t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n \t\t\tstring_list_append(hooks, hook_name);\n \t\t}\n \t} else if (!strcmp(subkey, \"command\")) {\n@@ -186,7 +178,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\tbreak;\n \t\tcase 1: /* enabled: undo a prior disabled entry */\n \t\t\tunsorted_string_list_remove(&data->disabled_hooks,\n-\t\t\t\t\t\t    hook_name);\n+\t\t\t\t\t\t    hook_name, 0);\n \t\t\tbreak;\n \t\tdefault:\n \t\t\tbreak; /* ignore unrecognised values */\ndiff --git a/string-list.c b/string-list.c\nindex fffa2ad4b6..d260b873c8 100644\n--- a/string-list.c\n+++ b/string-list.c\n@@ -281,6 +281,15 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_\n \tlist->nr--;\n }\n \n+void unsorted_string_list_remove(struct string_list *list, const char *str,\n+\t\t\t\t int free_util)\n+{\n+\tstruct string_list_item *item = unsorted_string_list_lookup(list, str);\n+\tif (item)\n+\t\tunsorted_string_list_delete_item(list, item - list->items,\n+\t\t\t\t\t\t free_util);\n+}\n+\n /*\n  * append a substring [p..end] to list; return number of things it\n  * appended to the list.\ndiff --git a/string-list.h b/string-list.h\nindex 3ad862a187..b86ee7c099 100644\n--- a/string-list.h\n+++ b/string-list.h\n@@ -265,6 +265,14 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list,\n  */\n void unsorted_string_list_delete_item(struct string_list *list, int i, int free_util);\n \n+/**\n+ * Remove the first item matching `str` from an unsorted string_list.\n+ * No-op if `str` is not found. If `free_util` is non-zero, the `util`\n+ * pointer of the removed item is freed before deletion.\n+ */\n+void unsorted_string_list_remove(struct string_list *list, const char *str,\n+\t\t\t\t int free_util);\n+\n /**\n  * Split string into substrings on characters in `delim` and append the\n  * substrings to `list`.  The input string is not modified.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539531","messageId":"20260320115211.177351-4-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 03/10] hook: rename cb_data_free/alloc -> hook_data_free/alloc","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:04Z","receivedAt":"2026-03-20T11:53:07Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Rename the hook callback function types to use the hook prefix.\n\nThis is a style fix with no logic changes.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c |  4 ++--\n hook.h | 10 +++++-----\n 2 files changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 349db729f6..afa8db21a0 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -52,7 +52,7 @@ const char *find_hook(struct repository *r, const char *name)\n \treturn path.buf;\n }\n \n-static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n+static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n {\n \tif (!h)\n \t\treturn;\n@@ -70,7 +70,7 @@ static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n \tfree(h);\n }\n \n-void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free)\n+void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free)\n {\n \tstruct string_list_item *item;\n \ndiff --git a/hook.h b/hook.h\nindex 40823ebde7..94649218a1 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -43,8 +43,8 @@ struct hook {\n \tvoid *feed_pipe_cb_data;\n };\n \n-typedef void (*cb_data_free_fn)(void *data);\n-typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n+typedef void (*hook_data_free_fn)(void *data);\n+typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n \n struct run_hooks_opt {\n \t/* Environment vars to be set for each hook */\n@@ -131,14 +131,14 @@ struct run_hooks_opt {\n \t *\n \t * The `feed_pipe_ctx` pointer can be used to pass initialization data.\n \t */\n-\tcb_data_alloc_fn feed_pipe_cb_data_alloc;\n+\thook_data_alloc_fn feed_pipe_cb_data_alloc;\n \n \t/**\n \t * Called to free the memory initialized by `feed_pipe_cb_data_alloc`.\n \t *\n \t * Must always be provided when `feed_pipe_cb_data_alloc` is provided.\n \t */\n-\tcb_data_free_fn feed_pipe_cb_data_free;\n+\thook_data_free_fn feed_pipe_cb_data_free;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -188,7 +188,7 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n  * Frees the memory allocated for the hook list, including the `struct hook`\n  * items and their internal state.\n  */\n-void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free);\n+void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free);\n \n /**\n  * Frees the hook configuration cache stored in `struct repository`.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539532","messageId":"20260320115211.177351-5-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 04/10] hook: detect & emit two more bugs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:05Z","receivedAt":"2026-03-20T11:53:10Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Trigger a bug when an unknown hook type is encountered while\nsetting up hook execution.\n\nAlso issue a bug if a configured hook is enabled without a cmd.\n\nMostly useful for defensive coding.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex afa8db21a0..6dfaa7e9b1 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -408,7 +408,11 @@ static int pick_next_hook(struct child_process *cp,\n \t} else if (h->kind == HOOK_CONFIGURED) {\n \t\t/* to enable oneliners, let config-specified hooks run in shell. */\n \t\tcp->use_shell = true;\n+\t\tif (!h->u.configured.command)\n+\t\t\tBUG(\"non-disabled HOOK_CONFIGURED hook has no command\");\n \t\tstrvec_push(&cp->args, h->u.configured.command);\n+\t} else {\n+\t\tBUG(\"unknown hook kind\");\n \t}\n \n \tif (!cp->args.nr)\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539533","messageId":"20260320115211.177351-6-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 05/10] hook: replace hook_list_clear() -> string_list_clear_func()","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:06Z","receivedAt":"2026-03-20T11:53:11Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Replace the custom function with string_list_clear_func() which\nis a more common pattern for clearing a string_list.\n\nTo be able to do this, rework hook_clear() into hook_free(), so\nit can be passed to string_list_clear_func().\n\nA slight complication is the need to keep a copy of the internal\ncb data free() pointer, however I think it's worth it since the\nAPI becomes cleaner, e.g. no more calls with NULL function args\nlike hook_list_clear(hooks, NULL).\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c |  2 +-\n hook.c         | 44 ++++++++++++++++++++++++++------------------\n hook.h         | 20 ++++++++++++++------\n 3 files changed, 41 insertions(+), 25 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex e641614b84..54b737990b 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -78,7 +78,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \t}\n \n cleanup:\n-\thook_list_clear(head, NULL);\n+\tstring_list_clear_func(head, hook_free);\n \tfree(head);\n \treturn ret;\n }\ndiff --git a/hook.c b/hook.c\nindex 6dfaa7e9b1..f6bb1999ae 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -52,8 +52,14 @@ const char *find_hook(struct repository *r, const char *name)\n \treturn path.buf;\n }\n \n-static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n+/*\n+ * Frees a struct hook stored as the util pointer of a string_list_item.\n+ * Suitable for use as a string_list_clear_func_t callback.\n+ */\n+void hook_free(void *p, const char *str UNUSED)\n {\n+\tstruct hook *h = p;\n+\n \tif (!h)\n \t\treturn;\n \n@@ -64,22 +70,12 @@ static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n \t\tfree((void *)h->u.configured.command);\n \t}\n \n-\tif (cb_data_free)\n-\t\tcb_data_free(h->feed_pipe_cb_data);\n+\tif (h->data_free && h->feed_pipe_cb_data)\n+\t\th->data_free(h->feed_pipe_cb_data);\n \n \tfree(h);\n }\n \n-void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free)\n-{\n-\tstruct string_list_item *item;\n-\n-\tfor_each_string_list_item(item, hooks)\n-\t\thook_clear(item->util, cb_data_free);\n-\n-\tstring_list_clear(hooks, 0);\n-}\n-\n /* Helper to detect and add default \"traditional\" hooks from the hookdir. */\n static void list_hooks_add_default(struct repository *r, const char *hookname,\n \t\t\t\t   struct string_list *hook_list,\n@@ -100,9 +96,15 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tif (options && options->dir)\n \t\thook_path = absolute_path(hook_path);\n \n-\t/* Setup per-hook internal state cb data */\n-\tif (options && options->feed_pipe_cb_data_alloc)\n+\t/*\n+\t * Setup per-hook internal state callback data.\n+\t * When provided, the alloc/free callbacks are always provided\n+\t * together, so use them to alloc/free the internal hook state.\n+\t */\n+\tif (options && options->feed_pipe_cb_data_alloc) {\n \t\th->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);\n+\t\th->data_free = options->feed_pipe_cb_data_free;\n+\t}\n \n \th->kind = HOOK_TRADITIONAL;\n \th->u.traditional.path = xstrdup(hook_path);\n@@ -315,10 +317,16 @@ static void list_hooks_add_configured(struct repository *r,\n \t\tstruct hook *hook;\n \t\tCALLOC_ARRAY(hook, 1);\n \n-\t\tif (options && options->feed_pipe_cb_data_alloc)\n+\t\t/*\n+\t\t * When provided, the alloc/free callbacks are always provided\n+\t\t * together, so use them to alloc/free the internal hook state.\n+\t\t */\n+\t\tif (options && options->feed_pipe_cb_data_alloc) {\n \t\t\thook->feed_pipe_cb_data =\n \t\t\t\toptions->feed_pipe_cb_data_alloc(\n \t\t\t\t\toptions->feed_pipe_ctx);\n+\t\t\thook->data_free = options->feed_pipe_cb_data_free;\n+\t\t}\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n@@ -361,7 +369,7 @@ int hook_exists(struct repository *r, const char *name)\n {\n \tstruct string_list *hooks = list_hooks(r, name, NULL);\n \tint exists = hooks->nr > 0;\n-\thook_list_clear(hooks, NULL);\n+\tstring_list_clear_func(hooks, hook_free);\n \tfree(hooks);\n \treturn exists;\n }\n@@ -515,7 +523,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,\n \trun_processes_parallel(&opts);\n \tret = cb_data.rc;\n cleanup:\n-\thook_list_clear(cb_data.hook_command_list, options->feed_pipe_cb_data_free);\n+\tstring_list_clear_func(cb_data.hook_command_list, hook_free);\n \tfree(cb_data.hook_command_list);\n \trun_hooks_opt_clear(options);\n \treturn ret;\ndiff --git a/hook.h b/hook.h\nindex 94649218a1..74f4701176 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -7,6 +7,9 @@\n \n struct repository;\n \n+typedef void (*hook_data_free_fn)(void *data);\n+typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n+\n /**\n  * Represents a hook command to be run.\n  * Hooks can be:\n@@ -41,10 +44,15 @@ struct hook {\n \t * Only useful when using `run_hooks_opt.feed_pipe`, otherwise ignore it.\n \t */\n \tvoid *feed_pipe_cb_data;\n-};\n \n-typedef void (*hook_data_free_fn)(void *data);\n-typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n+\t/**\n+\t * Callback to free `feed_pipe_cb_data`.\n+\t *\n+\t * It is called automatically and points to the `feed_pipe_cb_data_free`\n+\t * provided via the `run_hook_opt` parameter.\n+\t */\n+\thook_data_free_fn data_free;\n+};\n \n struct run_hooks_opt {\n \t/* Environment vars to be set for each hook */\n@@ -185,10 +193,10 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n \t\t\t       struct run_hooks_opt *options);\n \n /**\n- * Frees the memory allocated for the hook list, including the `struct hook`\n- * items and their internal state.\n+ * Frees a struct hook stored as the util pointer of a string_list_item.\n+ * Suitable for use as a string_list_clear_func_t callback.\n  */\n-void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free);\n+void hook_free(void *p, const char *str);\n \n /**\n  * Frees the hook configuration cache stored in `struct repository`.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539534","messageId":"20260320115211.177351-7-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 06/10] hook: make consistent use of friendly-name in docs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:07Z","receivedAt":"2026-03-20T11:53:13Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Both `name` and `friendly-name` is being used. Standardize on\n`friendly-name` for consistency since name is rather generic,\neven when used in the hooks namespace.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/hook.adoc | 30 +++++++++++++++---------------\n Documentation/git-hook.adoc    |  6 +++---\n hook.c                         |  2 +-\n hook.h                         |  2 +-\n 4 files changed, 20 insertions(+), 20 deletions(-)\n\ndiff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc\nindex 64e845a260..9e78f26439 100644\n--- a/Documentation/config/hook.adoc\n+++ b/Documentation/config/hook.adoc\n@@ -1,23 +1,23 @@\n-hook.<name>.command::\n-\tThe command to execute for `hook.<name>`. `<name>` is a unique\n-\t\"friendly\" name that identifies this hook. (The hook events that\n-\ttrigger the command are configured with `hook.<name>.event`.) The\n-\tvalue can be an executable path or a shell oneliner. If more than\n-\tone value is specified for the same `<name>`, only the last value\n-\tparsed is used. See linkgit:git-hook[1].\n+hook.<friendly-name>.command::\n+\tThe command to execute for `hook.<friendly-name>`. `<friendly-name>`\n+\tis a unique name that identifies this hook. The hook events that\n+\ttrigger the command are configured with `hook.<friendly-name>.event`.\n+\tThe value can be an executable path or a shell oneliner. If more than\n+\tone value is specified for the same `<friendly-name>`, only the last\n+\tvalue parsed is used. See linkgit:git-hook[1].\n \n-hook.<name>.event::\n-\tThe hook events that trigger `hook.<name>`. The value is the name\n-\tof a hook event, like \"pre-commit\" or \"update\". (See\n+hook.<friendly-name>.event::\n+\tThe hook events that trigger `hook.<friendly-name>`. The value is the\n+\tname of a hook event, like \"pre-commit\" or \"update\". (See\n \tlinkgit:githooks[5] for a complete list of hook events.) On the\n-\tspecified event, the associated `hook.<name>.command` is executed.\n-\tThis is a multi-valued key. To run `hook.<name>` on multiple\n+\tspecified event, the associated `hook.<friendly-name>.command` is executed.\n+\tThis is a multi-valued key. To run `hook.<friendly-name>` on multiple\n \tevents, specify the key more than once. An empty value resets\n \tthe list of events, clearing any previously defined events for\n-\t`hook.<name>`. See linkgit:git-hook[1].\n+\t`hook.<friendly-name>`. See linkgit:git-hook[1].\n \n-hook.<name>.enabled::\n-\tWhether the hook `hook.<name>` is enabled. Defaults to `true`.\n+hook.<friendly-name>.enabled::\n+\tWhether the hook `hook.<friendly-name>` is enabled. Defaults to `true`.\n \tSet to `false` to disable the hook without removing its\n \tconfiguration. This is particularly useful when a hook is defined\n \tin a system or global config file and needs to be disabled for a\ndiff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\nindex 12d2701b52..966388660a 100644\n--- a/Documentation/git-hook.adoc\n+++ b/Documentation/git-hook.adoc\n@@ -44,7 +44,7 @@ event`), and then `~/bin/spellchecker` will have a chance to check your commit\n message (during the `commit-msg` hook event).\n \n Commands are run in the order Git encounters their associated\n-`hook.<name>.event` configs during the configuration parse (see\n+`hook.<friendly-name>.event` configs during the configuration parse (see\n linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be\n added, only one `hook.linter.command` event is valid - Git uses \"last-one-wins\"\n to determine which command to run.\n@@ -76,10 +76,10 @@ first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It\n would evaluate the output of each when deciding whether to proceed with the\n commit.\n \n-For a full list of hook events which you can set your `hook.<name>.event` to,\n+For a full list of hook events which you can set your `hook.<friendly-name>.event` to,\n and how hooks are invoked during those events, see linkgit:githooks[5].\n \n-Git will ignore any `hook.<name>.event` that specifies an event it doesn't\n+Git will ignore any `hook.<friendly-name>.event` that specifies an event it doesn't\n recognize. This is intended so that tools which wrap Git can use the hook\n infrastructure to run their own hooks; see \"WRAPPERS\" for more guidance.\n \ndiff --git a/hook.c b/hook.c\nindex f6bb1999ae..7f89ae9cc2 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -116,7 +116,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\n  * event_hooks: event-name to list of friendly-names map.\n- * disabled_hooks: set of friendly-names with hook.name.enabled = false.\n+ * disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.\n  */\n struct hook_all_config_cb {\n \tstruct strmap commands;\ndiff --git a/hook.h b/hook.h\nindex 74f4701176..ad022821c1 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -14,7 +14,7 @@ typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n  * Represents a hook command to be run.\n  * Hooks can be:\n  * 1. \"traditional\" (found in the hooks directory)\n- * 2. \"configured\" (defined in Git's configuration via hook.<name>.event).\n+ * 2. \"configured\" (defined in Git's configuration via hook.<friendly-name>.event).\n  * The 'kind' field determines which part of the union 'u' is valid.\n  */\n struct hook {\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539535","messageId":"20260320115211.177351-8-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 07/10] t1800: add test to verify hook execution ordering","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:08Z","receivedAt":"2026-03-20T11:53:16Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"There is a documented expectation that configured hooks are\nrun before the hook from the hookdir. Add a test for it.\n\nWhile at it, I noticed that `git hook list -h` runs twice\nin the `git hook usage` test, so remove one invocation.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/t1800-hook.sh | 29 ++++++++++++++++++++++++++++-\n 1 file changed, 28 insertions(+), 1 deletion(-)\n\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 952bf97b86..7eee84fc39 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -25,7 +25,6 @@ test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook &&\n \ttest_expect_code 129 git hook run &&\n \ttest_expect_code 129 git hook run -h &&\n-\ttest_expect_code 129 git hook list -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n \ttest_expect_code 129 git hook list -h &&\n@@ -381,6 +380,34 @@ test_expect_success 'globally disabled hook can be re-enabled locally' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'configured hooks run before hookdir hook' '\n+\tsetup_hookdir &&\n+\ttest_config hook.first.event \"pre-commit\" &&\n+\ttest_config hook.first.command \"echo first\" &&\n+\ttest_config hook.second.event \"pre-commit\" &&\n+\ttest_config hook.second.command \"echo second\" &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tfirst\n+\tsecond\n+\thook from hookdir\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# \"Legacy Hook\" is the output of the hookdir pre-commit script\n+\t# written by setup_hookdir() above.\n+\tcat >expected <<-\\EOF &&\n+\tfirst\n+\tsecond\n+\t\"Legacy Hook\"\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_when_finished \"rm -rf bad-hooks\" &&\n \tmkdir bad-hooks &&\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539536","messageId":"20260320115211.177351-9-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 08/10] hook: introduce hook_config_cache_entry for per-hook data","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:09Z","receivedAt":"2026-03-20T11:53:19Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Replace the bare `char *command` util pointer stored in each string_list\nitem with a heap-allocated `struct hook_config_cache_entry` that carries\nthat command string.\n\nThis is just a refactoring with no behavior changes, to give the cache\nentry room to grow, so it can carry the additional hook metadata we'll\nbe adding in the following commits.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c | 28 ++++++++++++++++++++++------\n 1 file changed, 22 insertions(+), 6 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 7f89ae9cc2..49f1ebe17a 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -112,6 +112,15 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tstring_list_append(hook_list, hook_path)->util = h;\n }\n \n+/*\n+ * Cache entry stored as the .util pointer of string_list items inside the\n+ * hook config cache. For now carries only the command for the hook. Next\n+ * commits will add more data.\n+ */\n+struct hook_config_cache_entry {\n+\tchar *command;\n+};\n+\n /*\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\n@@ -206,7 +215,12 @@ void hook_cache_clear(struct strmap *cache)\n \n \tstrmap_for_each_entry(cache, &iter, e) {\n \t\tstruct string_list *hooks = e->value;\n-\t\tstring_list_clear(hooks, 1); /* free util (command) pointers */\n+\t\tfor (size_t i = 0; i < hooks->nr; i++) {\n+\t\t\tstruct hook_config_cache_entry *entry = hooks->items[i].util;\n+\t\t\tfree(entry->command);\n+\t\t\tfree(entry);\n+\t\t}\n+\t\tstring_list_clear(hooks, 0);\n \t\tfree(hooks);\n \t}\n \tstrmap_clear(cache, 0);\n@@ -236,6 +250,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \n \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n \t\t\tconst char *hname = hook_names->items[i].string;\n+\t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n \t\t\t/* filter out disabled hooks */\n@@ -249,9 +264,10 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t\t\t\t      \"'hook.%s.event' must be removed;\"\n \t\t\t\t      \" aborting.\"), hname, hname);\n \n-\t\t\t/* util stores the command; owned by the cache. */\n-\t\t\tstring_list_append(hooks, hname)->util =\n-\t\t\t\txstrdup(command);\n+\t\t\t/* util stores a cache entry; owned by the cache. */\n+\t\t\tCALLOC_ARRAY(entry, 1);\n+\t\t\tentry->command = xstrdup(command);\n+\t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n \t\tstrmap_put(cache, e->key, hooks);\n@@ -313,7 +329,7 @@ static void list_hooks_add_configured(struct repository *r,\n \t/* Iterate through configured hooks and initialize internal states */\n \tfor (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {\n \t\tconst char *friendly_name = configured_hooks->items[i].string;\n-\t\tconst char *command = configured_hooks->items[i].util;\n+\t\tstruct hook_config_cache_entry *entry = configured_hooks->items[i].util;\n \t\tstruct hook *hook;\n \t\tCALLOC_ARRAY(hook, 1);\n \n@@ -330,7 +346,7 @@ static void list_hooks_add_configured(struct repository *r,\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n-\t\thook->u.configured.command = xstrdup(command);\n+\t\thook->u.configured.command = xstrdup(entry->command);\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539537","messageId":"20260320115211.177351-3-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 02/10] hook: fix minor style issues","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:03Z","receivedAt":"2026-03-20T11:53:19Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Fix some minor style nits pointed by Patrick, Junio and Eric:\n  * Use CALLOC_ARRAY instead of xcalloc.\n  * Init struct members during declaration.\n  * Simplify if condition boolean logic.\n  * Missing curly braces in if/else stmts.\n  * Unnecessary header includes.\n  * Capitalization and full-stop in error/warn messages.\n  * Curly brace on separate line when defining struct.\n  * Comment spelling: free'd -> freed.\n\nThese contain no logic changes, the code behaves the same as before.\n\nSuggested-by: Eric Sunshine <sunshine@sunshineco.com>\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c         |  6 ++----\n builtin/receive-pack.c | 11 +++++++----\n hook.c                 | 25 +++++++++++++------------\n hook.h                 |  3 +--\n refs.c                 |  3 ++-\n t/t1800-hook.sh        |  2 +-\n transport.c            |  3 ++-\n 7 files changed, 28 insertions(+), 25 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 83020dfb4f..e641614b84 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -5,8 +5,6 @@\n #include \"gettext.h\"\n #include \"hook.h\"\n #include \"parse-options.h\"\n-#include \"strvec.h\"\n-#include \"abspath.h\"\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n@@ -51,7 +49,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \t * arguments later they probably should be caught by parse_options.\n \t */\n \tif (argc != 1)\n-\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\tusage_msg_opt(_(\"you must specify a hook event name to list\"),\n \t\t\t      builtin_hook_list_usage, list_options);\n \n \thookname = argv[0];\n@@ -59,7 +57,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \thead = list_hooks(repo, hookname, NULL);\n \n \tif (!head->nr) {\n-\t\twarning(_(\"No hooks found for event '%s'\"), hookname);\n+\t\twarning(_(\"no hooks found for event '%s'\"), hookname);\n \t\tret = 1; /* no hooks found */\n \t\tgoto cleanup;\n \t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex e34edff406..991d6ca7d5 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -904,7 +904,8 @@ static int feed_receive_hook_cb(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_\n static void *receive_hook_feed_state_alloc(void *feed_pipe_ctx)\n {\n \tstruct receive_hook_feed_state *init_state = feed_pipe_ctx;\n-\tstruct receive_hook_feed_state *data = xcalloc(1, sizeof(*data));\n+\tstruct receive_hook_feed_state *data;\n+\tCALLOC_ARRAY(data, 1);\n \tdata->report = init_state->report;\n \tdata->cmd = init_state->cmd;\n \tdata->skip_broken = init_state->skip_broken;\n@@ -928,7 +929,11 @@ static int run_receive_hook(struct command *commands,\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tstruct command *iter = commands;\n-\tstruct receive_hook_feed_state feed_init_state = { 0 };\n+\tstruct receive_hook_feed_state feed_init_state = {\n+\t\t.cmd = commands,\n+\t\t.skip_broken = skip_broken,\n+\t\t.buf = STRBUF_INIT,\n+\t};\n \tstruct async sideband_async;\n \tint sideband_async_started = 0;\n \tint saved_stderr = -1;\n@@ -961,8 +966,6 @@ static int run_receive_hook(struct command *commands,\n \tprepare_sideband_async(&sideband_async, &saved_stderr, &sideband_async_started);\n \n \t/* set up stdin callback */\n-\tfeed_init_state.cmd = commands;\n-\tfeed_init_state.skip_broken = skip_broken;\n \topt.feed_pipe_ctx = &feed_init_state;\n \topt.feed_pipe = feed_receive_hook_cb;\n \topt.feed_pipe_cb_data_alloc = receive_hook_feed_state_alloc;\ndiff --git a/hook.c b/hook.c\nindex 67cc9a66df..349db729f6 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -57,9 +57,9 @@ static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n \tif (!h)\n \t\treturn;\n \n-\tif (h->kind == HOOK_TRADITIONAL)\n+\tif (h->kind == HOOK_TRADITIONAL) {\n \t\tfree((void *)h->u.traditional.path);\n-\telse if (h->kind == HOOK_CONFIGURED) {\n+\t} else if (h->kind == HOOK_CONFIGURED) {\n \t\tfree((void *)h->u.configured.friendly_name);\n \t\tfree((void *)h->u.configured.command);\n \t}\n@@ -91,7 +91,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tif (!hook_path)\n \t\treturn;\n \n-\th = xcalloc(1, sizeof(struct hook));\n+\tCALLOC_ARRAY(h, 1);\n \n \t/*\n \t * If the hook is to run in a specific dir, a relative path can\n@@ -154,7 +154,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\t\tstrmap_get(&data->event_hooks, value);\n \n \t\t\tif (!hooks) {\n-\t\t\t\thooks = xcalloc(1, sizeof(*hooks));\n+\t\t\t\tCALLOC_ARRAY(hooks, 1);\n \t\t\t\tstring_list_init_dup(hooks);\n \t\t\t\tstrmap_put(&data->event_hooks, value, hooks);\n \t\t\t}\n@@ -227,7 +227,8 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t/* Construct the cache from parsed configs. */\n \tstrmap_for_each_entry(&cb_data.event_hooks, &iter, e) {\n \t\tstruct string_list *hook_names = e->value;\n-\t\tstruct string_list *hooks = xcalloc(1, sizeof(*hooks));\n+\t\tstruct string_list *hooks;\n+\t\tCALLOC_ARRAY(hooks, 1);\n \n \t\tstring_list_init_dup(hooks);\n \n@@ -281,7 +282,7 @@ static struct strmap *get_hook_config_cache(struct repository *r)\n \t\t * it just once on the first call.\n \t\t */\n \t\tif (!r->hook_config_cache) {\n-\t\t\tr->hook_config_cache = xcalloc(1, sizeof(*cache));\n+\t\t\tCALLOC_ARRAY(r->hook_config_cache, 1);\n \t\t\tstrmap_init(r->hook_config_cache);\n \t\t\tbuild_hook_config_map(r, r->hook_config_cache);\n \t\t}\n@@ -289,9 +290,9 @@ static struct strmap *get_hook_config_cache(struct repository *r)\n \t} else {\n \t\t/*\n \t\t * Out-of-repo calls (no gitdir) allocate and return a temporary\n-\t\t * map cache which gets free'd immediately by the caller.\n+\t\t * cache which gets freed immediately by the caller.\n \t\t */\n-\t\tcache = xcalloc(1, sizeof(*cache));\n+\t\tCALLOC_ARRAY(cache, 1);\n \t\tstrmap_init(cache);\n \t\tbuild_hook_config_map(r, cache);\n \t}\n@@ -311,7 +312,8 @@ static void list_hooks_add_configured(struct repository *r,\n \tfor (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {\n \t\tconst char *friendly_name = configured_hooks->items[i].string;\n \t\tconst char *command = configured_hooks->items[i].util;\n-\t\tstruct hook *hook = xcalloc(1, sizeof(struct hook));\n+\t\tstruct hook *hook;\n+\t\tCALLOC_ARRAY(hook, 1);\n \n \t\tif (options && options->feed_pipe_cb_data_alloc)\n \t\t\thook->feed_pipe_cb_data =\n@@ -343,7 +345,7 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n \tif (!hookname)\n \t\tBUG(\"null hookname was provided to hook_list()!\");\n \n-\thook_head = xmalloc(sizeof(struct string_list));\n+\tCALLOC_ARRAY(hook_head, 1);\n \tstring_list_init_dup(hook_head);\n \n \t/* Add hooks from the config, e.g. hook.myhook.event = pre-commit */\n@@ -493,8 +495,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,\n \t * Ensure cb_data copy and free functions are either provided together,\n \t * or neither one is provided.\n \t */\n-\tif ((options->feed_pipe_cb_data_alloc && !options->feed_pipe_cb_data_free) ||\n-\t    (!options->feed_pipe_cb_data_alloc && options->feed_pipe_cb_data_free))\n+\tif (!options->feed_pipe_cb_data_alloc != !options->feed_pipe_cb_data_free)\n \t\tBUG(\"feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together\");\n \n \tif (options->invoked_hook)\ndiff --git a/hook.h b/hook.h\nindex e949f5d488..40823ebde7 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -46,8 +46,7 @@ struct hook {\n typedef void (*cb_data_free_fn)(void *data);\n typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n \n-struct run_hooks_opt\n-{\n+struct run_hooks_opt {\n \t/* Environment vars to be set for each hook */\n \tstruct strvec env;\n \ndiff --git a/refs.c b/refs.c\nindex 6fb8f9d10c..33c7961802 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2591,7 +2591,8 @@ static int transaction_hook_feed_stdin(int hook_stdin_fd, void *pp_cb, void *pp_\n \n static void *transaction_feed_cb_data_alloc(void *feed_pipe_ctx UNUSED)\n {\n-\tstruct transaction_feed_cb_data *data = xmalloc(sizeof(*data));\n+\tstruct transaction_feed_cb_data *data;\n+\tCALLOC_ARRAY(data, 1);\n \tstrbuf_init(&data->buf, 0);\n \tdata->index = 0;\n \treturn data;\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex b1583e9ef9..952bf97b86 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -34,7 +34,7 @@ test_expect_success 'git hook usage' '\n \n test_expect_success 'git hook list: nonexistent hook' '\n \tcat >stderr.expect <<-\\EOF &&\n-\twarning: No hooks found for event '\\''test-hook'\\''\n+\twarning: no hooks found for event '\\''test-hook'\\''\n \tEOF\n \ttest_expect_code 1 git hook list test-hook 2>stderr.actual &&\n \ttest_cmp stderr.expect stderr.actual\ndiff --git a/transport.c b/transport.c\nindex 107f4fa5dc..56a4015389 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1360,7 +1360,8 @@ static int pre_push_hook_feed_stdin(int hook_stdin_fd, void *pp_cb UNUSED, void\n \n static void *pre_push_hook_data_alloc(void *feed_pipe_ctx)\n {\n-\tstruct feed_pre_push_hook_data *data = xmalloc(sizeof(*data));\n+\tstruct feed_pre_push_hook_data *data;\n+\tCALLOC_ARRAY(data, 1);\n \tstrbuf_init(&data->buf, 0);\n \tdata->refs = (struct ref *)feed_pipe_ctx;\n \treturn data;\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539538","messageId":"20260320115211.177351-10-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 09/10] hook: show config scope in git hook list","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:10Z","receivedAt":"2026-03-20T11:53:21Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Users running \"git hook list\" can see which hooks are configured but\nhave no way to tell at which config scope (local, global, system...)\neach hook was defined.\n\nStore the scope from ctx->kvi->scope in the single-pass config callback,\nthen carry it through the cache to the hook structs, so we can expose it\nto users via the \"git hook list --show-scope\" flag, which mirrors the\nexisting git config --show-scope convention.\n\nWithout the flag the output is unchanged.\n\nThe scope is printed as a tab-separated prefix (like \"git config --show-scope\"),\nmaking it unambiguously machine-parseable even when the friendly name\ncontains spaces.\n\nExample usage:\n$ git hook list --show-scope pre-commit\nglobal\tlinter\nlocal\tno-leaks\nhook from hookdir\n\nTraditional hooks from the hookdir are unaffected by --show-scope since\nthe config scope concept does not apply to them.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/git-hook.adoc | 10 ++++++++--\n builtin/hook.c              | 14 ++++++++++++--\n hook.c                      | 24 ++++++++++++++++++++----\n hook.h                      |  2 ++\n t/t1800-hook.sh             | 19 +++++++++++++++++++\n 5 files changed, 61 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\nindex 966388660a..e7d399ae57 100644\n--- a/Documentation/git-hook.adoc\n+++ b/Documentation/git-hook.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [verse]\n 'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\n-'git hook' list [-z] <hook-name>\n+'git hook' list [-z] [--show-scope] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -113,7 +113,7 @@ Any positional arguments to the hook should be passed after a\n mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See\n linkgit:githooks[5] for arguments hooks might expect (if any).\n \n-list [-z]::\n+list [-z] [--show-scope]::\n \tPrint a list of hooks which will be run on `<hook-name>` event. If no\n \thooks are configured for that event, print a warning and return 1.\n \tUse `-z` to terminate output lines with NUL instead of newlines.\n@@ -134,6 +134,12 @@ OPTIONS\n -z::\n \tTerminate \"list\" output lines with NUL instead of newlines.\n \n+--show-scope::\n+\tFor \"list\"; prefix each configured hook's friendly name with a\n+\ttab-separated config scope (e.g. `local`, `global`, `system`),\n+\tmirroring the output style of `git config --show-scope`. Traditional\n+\thooks from the hookdir are unaffected.\n+\n WRAPPERS\n --------\n \ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 54b737990b..4cc65a0dc5 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -9,7 +9,7 @@\n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n #define BUILTIN_HOOK_LIST_USAGE \\\n-\tN_(\"git hook list [-z] <hook-name>\")\n+\tN_(\"git hook list [-z] [--show-scope] <hook-name>\")\n \n static const char * const builtin_hook_usage[] = {\n \tBUILTIN_HOOK_RUN_USAGE,\n@@ -33,11 +33,14 @@ static int list(int argc, const char **argv, const char *prefix,\n \tstruct string_list_item *item;\n \tconst char *hookname = NULL;\n \tint line_terminator = '\\n';\n+\tint show_scope = 0;\n \tint ret = 0;\n \n \tstruct option list_options[] = {\n \t\tOPT_SET_INT('z', NULL, &line_terminator,\n \t\t\t    N_(\"use NUL as line terminator\"), '\\0'),\n+\t\tOPT_BOOL(0, \"show-scope\", &show_scope,\n+\t\t\t N_(\"show the config scope that defined each hook\")),\n \t\tOPT_END(),\n \t};\n \n@@ -70,7 +73,14 @@ static int list(int argc, const char **argv, const char *prefix,\n \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n \t\t\tbreak;\n \t\tcase HOOK_CONFIGURED:\n-\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name, line_terminator);\n+\t\t\tif (show_scope)\n+\t\t\t\tprintf(\"%s\\t%s%c\",\n+\t\t\t\t       config_scope_name(h->u.configured.scope),\n+\t\t\t\t       h->u.configured.friendly_name,\n+\t\t\t\t       line_terminator);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n+\t\t\t\t       line_terminator);\n \t\t\tbreak;\n \t\tdefault:\n \t\t\tBUG(\"unknown hook kind\");\ndiff --git a/hook.c b/hook.c\nindex 49f1ebe17a..aa08c38c27 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -114,11 +114,11 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \n /*\n  * Cache entry stored as the .util pointer of string_list items inside the\n- * hook config cache. For now carries only the command for the hook. Next\n- * commits will add more data.\n+ * hook config cache.\n  */\n struct hook_config_cache_entry {\n \tchar *command;\n+\tenum config_scope scope;\n };\n \n /*\n@@ -135,7 +135,7 @@ struct hook_all_config_cb {\n \n /* repo_config() callback that collects all hook.* configuration in one pass. */\n static int hook_config_lookup_all(const char *key, const char *value,\n-\t\t\t\t  const struct config_context *ctx UNUSED,\n+\t\t\t\t  const struct config_context *ctx,\n \t\t\t\t  void *cb_data)\n {\n \tstruct hook_all_config_cb *data = cb_data;\n@@ -172,7 +172,19 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \n \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n \t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n-\t\t\tstring_list_append(hooks, hook_name);\n+\n+\t\t\tif (!ctx->kvi)\n+\t\t\t\tBUG(\"hook config callback called without key-value info\");\n+\n+\t\t\t/*\n+\t\t\t * Stash the config scope in the util pointer for\n+\t\t\t * later retrieval in build_hook_config_map(). This\n+\t\t\t * intermediate struct is transient and never leaves\n+\t\t\t * that function, so we pack the enum value into the\n+\t\t\t * pointer rather than heap-allocating a wrapper.\n+\t\t\t */\n+\t\t\tstring_list_append(hooks, hook_name)->util =\n+\t\t\t\t(void *)(uintptr_t)ctx->kvi->scope;\n \t\t}\n \t} else if (!strcmp(subkey, \"command\")) {\n \t\t/* Store command overwriting the old value */\n@@ -250,6 +262,8 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \n \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n \t\t\tconst char *hname = hook_names->items[i].string;\n+\t\t\tenum config_scope scope =\n+\t\t\t\t(enum config_scope)(uintptr_t)hook_names->items[i].util;\n \t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n@@ -267,6 +281,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t\t\t/* util stores a cache entry; owned by the cache. */\n \t\t\tCALLOC_ARRAY(entry, 1);\n \t\t\tentry->command = xstrdup(command);\n+\t\t\tentry->scope = scope;\n \t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n@@ -347,6 +362,7 @@ static void list_hooks_add_configured(struct repository *r,\n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n \t\thook->u.configured.command = xstrdup(entry->command);\n+\t\thook->u.configured.scope = entry->scope;\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\ndiff --git a/hook.h b/hook.h\nindex ad022821c1..92e9faf9bb 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,5 +1,6 @@\n #ifndef HOOK_H\n #define HOOK_H\n+#include \"config.h\"\n #include \"strvec.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n@@ -29,6 +30,7 @@ struct hook {\n \t\tstruct {\n \t\t\tconst char *friendly_name;\n \t\t\tconst char *command;\n+\t\t\tenum config_scope scope;\n \t\t} configured;\n \t} u;\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 7eee84fc39..22cca15fda 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -408,6 +408,25 @@ test_expect_success 'configured hooks run before hookdir hook' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list --show-scope shows config scope' '\n+\ttest_config_global hook.global-hook.command \"echo global\" &&\n+\ttest_config_global hook.global-hook.event test-hook --add &&\n+\ttest_config hook.local-hook.command \"echo local\" &&\n+\ttest_config hook.local-hook.event test-hook --add &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tglobal\tglobal-hook\n+\tlocal\tlocal-hook\n+\tEOF\n+\tgit hook list --show-scope test-hook >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# without --show-scope the scope must not appear\n+\tgit hook list test-hook >actual &&\n+\ttest_grep ! \"^global\t\" actual &&\n+\ttest_grep ! \"^local\t\" actual\n+'\n+\n test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_when_finished \"rm -rf bad-hooks\" &&\n \tmkdir bad-hooks &&\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539539","messageId":"20260320115211.177351-11-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 10/10] hook: show disabled hooks in \"git hook list\"","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-20T11:52:11Z","receivedAt":"2026-03-20T11:53:23Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Disabled hooks were filtered out of the cache entirely, making them\ninvisible to \"git hook list\". Keep them in the cache with a new\n\"disabled\" flag which is propagated to the respective struct hook.\n\n\"git hook list\" now shows disabled hooks as tab-separated columns,\nwith the status as a prefix before the name (like scope with\n--show-scope). With --show-scope it looks like:\n\n$ git hook list --show-scope pre-commit\nglobal\tlinter\nlocal\tdisabled\tno-leaks\nhook from hookdir\n\nA disabled hook without a command issues a warning instead of the\nfatal \"hook.X.command must be configured\" error. We could also throw\nan error, however it seemd a bit excessive to me in this case.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c  | 20 ++++++++++--------\n hook.c          | 54 +++++++++++++++++++++++++++++++++----------------\n hook.h          |  1 +\n t/t1800-hook.sh | 33 +++++++++++++++++++++++++++---\n 4 files changed, 80 insertions(+), 28 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 4cc65a0dc5..f671e7f91a 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -72,16 +72,20 @@ static int list(int argc, const char **argv, const char *prefix,\n \t\tcase HOOK_TRADITIONAL:\n \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n \t\t\tbreak;\n-\t\tcase HOOK_CONFIGURED:\n-\t\t\tif (show_scope)\n-\t\t\t\tprintf(\"%s\\t%s%c\",\n-\t\t\t\t       config_scope_name(h->u.configured.scope),\n-\t\t\t\t       h->u.configured.friendly_name,\n-\t\t\t\t       line_terminator);\n+\t\tcase HOOK_CONFIGURED: {\n+\t\t\tconst char *name = h->u.configured.friendly_name;\n+\t\t\tconst char *scope = show_scope ?\n+\t\t\t\tconfig_scope_name(h->u.configured.scope) : NULL;\n+\t\t\tif (scope)\n+\t\t\t\tprintf(\"%s\\t%s%s%c\", scope,\n+\t\t\t\t       h->u.configured.disabled ? \"disabled\\t\" : \"\",\n+\t\t\t\t       name, line_terminator);\n \t\t\telse\n-\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n-\t\t\t\t       line_terminator);\n+\t\t\t\tprintf(\"%s%s%c\",\n+\t\t\t\t       h->u.configured.disabled ? \"disabled\\t\" : \"\",\n+\t\t\t\t       name, line_terminator);\n \t\t\tbreak;\n+\t\t}\n \t\tdefault:\n \t\t\tBUG(\"unknown hook kind\");\n \t\t}\ndiff --git a/hook.c b/hook.c\nindex aa08c38c27..0e09b9a2bb 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -119,6 +119,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n struct hook_config_cache_entry {\n \tchar *command;\n \tenum config_scope scope;\n+\tunsigned int disabled:1;\n };\n \n /*\n@@ -217,8 +218,10 @@ static int hook_config_lookup_all(const char *key, const char *value,\n  * every item's string is the hook's friendly-name and its util pointer is\n  * the corresponding command string. Both strings are owned by the map.\n  *\n- * Disabled hooks and hooks missing a command are already filtered out at\n- * parse time, so callers can iterate the list directly.\n+ * Disabled hooks are kept in the cache with entry->disabled set, so that\n+ * \"git hook list\" can display them. A non-disabled hook missing a command\n+ * is fatal; a disabled hook missing a command emits a warning and is kept\n+ * in the cache with entry->command = NULL.\n  */\n void hook_cache_clear(struct strmap *cache)\n {\n@@ -267,21 +270,26 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n-\t\t\t/* filter out disabled hooks */\n-\t\t\tif (unsorted_string_list_lookup(&cb_data.disabled_hooks,\n-\t\t\t\t\t\t\thname))\n-\t\t\t\tcontinue;\n+\t\t\tint is_disabled =\n+\t\t\t\t!!unsorted_string_list_lookup(\n+\t\t\t\t\t&cb_data.disabled_hooks, hname);\n \n \t\t\tcommand = strmap_get(&cb_data.commands, hname);\n-\t\t\tif (!command)\n-\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n-\t\t\t\t      \"'hook.%s.event' must be removed;\"\n-\t\t\t\t      \" aborting.\"), hname, hname);\n+\t\t\tif (!command) {\n+\t\t\t\tif (is_disabled)\n+\t\t\t\t\twarning(_(\"disabled hook '%s' has no \"\n+\t\t\t\t\t\t  \"command configured\"), hname);\n+\t\t\t\telse\n+\t\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n+\t\t\t\t\t      \"'hook.%s.event' must be removed;\"\n+\t\t\t\t\t      \" aborting.\"), hname, hname);\n+\t\t\t}\n \n \t\t\t/* util stores a cache entry; owned by the cache. */\n \t\t\tCALLOC_ARRAY(entry, 1);\n-\t\t\tentry->command = xstrdup(command);\n+\t\t\tentry->command = xstrdup_or_null(command);\n \t\t\tentry->scope = scope;\n+\t\t\tentry->disabled = is_disabled;\n \t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n@@ -361,8 +369,10 @@ static void list_hooks_add_configured(struct repository *r,\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n-\t\thook->u.configured.command = xstrdup(entry->command);\n+\t\thook->u.configured.command =\n+\t\t\tentry->command ? xstrdup(entry->command) : NULL;\n \t\thook->u.configured.scope = entry->scope;\n+\t\thook->u.configured.disabled = entry->disabled;\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\n@@ -400,7 +410,16 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n int hook_exists(struct repository *r, const char *name)\n {\n \tstruct string_list *hooks = list_hooks(r, name, NULL);\n-\tint exists = hooks->nr > 0;\n+\tint exists = 0;\n+\n+\tfor (size_t i = 0; i < hooks->nr; i++) {\n+\t\tstruct hook *h = hooks->items[i].util;\n+\t\tif (h->kind == HOOK_TRADITIONAL ||\n+\t\t    !h->u.configured.disabled) {\n+\t\t\texists = 1;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n \tstring_list_clear_func(hooks, hook_free);\n \tfree(hooks);\n \treturn exists;\n@@ -415,10 +434,11 @@ static int pick_next_hook(struct child_process *cp,\n \tstruct string_list *hook_list = hook_cb->hook_command_list;\n \tstruct hook *h;\n \n-\tif (hook_cb->hook_to_run_index >= hook_list->nr)\n-\t\treturn 0;\n-\n-\th = hook_list->items[hook_cb->hook_to_run_index++].util;\n+\tdo {\n+\t\tif (hook_cb->hook_to_run_index >= hook_list->nr)\n+\t\t\treturn 0;\n+\t\th = hook_list->items[hook_cb->hook_to_run_index++].util;\n+\t} while (h->kind == HOOK_CONFIGURED && h->u.configured.disabled);\n \n \tcp->no_stdin = 1;\n \tstrvec_pushv(&cp->env, hook_cb->options->env.v);\ndiff --git a/hook.h b/hook.h\nindex 92e9faf9bb..7c8c3d471e 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -31,6 +31,7 @@ struct hook {\n \t\t\tconst char *friendly_name;\n \t\t\tconst char *command;\n \t\t\tenum config_scope scope;\n+\t\t\tunsigned int disabled:1;\n \t\t} configured;\n \t} u;\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 22cca15fda..6f6fe88bea 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -357,7 +357,15 @@ test_expect_success 'disabled hook is not run' '\n \ttest_must_be_empty actual\n '\n \n-test_expect_success 'disabled hook does not appear in git hook list' '\n+test_expect_success 'disabled hook with no command warns' '\n+\ttest_config hook.nocommand.event \"pre-commit\" &&\n+\ttest_config hook.nocommand.enabled false &&\n+\n+\tgit hook list pre-commit 2>actual &&\n+\ttest_grep \"disabled hook.*nocommand.*no command configured\" actual\n+'\n+\n+test_expect_success 'disabled hook appears as disabled in git hook list' '\n \ttest_config hook.active.event \"pre-commit\" &&\n \ttest_config hook.active.command \"echo active\" &&\n \ttest_config hook.inactive.event \"pre-commit\" &&\n@@ -365,8 +373,27 @@ test_expect_success 'disabled hook does not appear in git hook list' '\n \ttest_config hook.inactive.enabled false &&\n \n \tgit hook list pre-commit >actual &&\n-\ttest_grep \"active\" actual &&\n-\ttest_grep ! \"inactive\" actual\n+\ttest_grep \"^active$\" actual &&\n+\ttest_grep \"^disabled\tinactive$\" actual\n+'\n+\n+test_expect_success 'disabled hook shows scope with --show-scope' '\n+\ttest_config hook.myhook.event \"pre-commit\" &&\n+\ttest_config hook.myhook.command \"echo hi\" &&\n+\ttest_config hook.myhook.enabled false &&\n+\n+\tgit hook list --show-scope pre-commit >actual &&\n+\ttest_grep \"^local\tdisabled\tmyhook$\" actual\n+'\n+\n+test_expect_success 'disabled configured hook is not reported as existing by hook_exists' '\n+\ttest_when_finished \"rm -f git-bugreport-hook-exists-test.txt\" &&\n+\ttest_config hook.linter.event \"pre-commit\" &&\n+\ttest_config hook.linter.command \"echo lint\" &&\n+\ttest_config hook.linter.enabled false &&\n+\n+\tgit bugreport -s hook-exists-test &&\n+\ttest_grep ! \"pre-commit\" git-bugreport-hook-exists-test.txt\n '\n \n test_expect_success 'globally disabled hook can be re-enabled locally' '\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539756","messageId":"xmqqmrzy5xwe.fsf@gitster.g","threadId":"65171","inReplyTo":"20260320115211.177351-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 00/10] config-hook cleanups and two small 'git hook list' features","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-23T16:11:13Z","receivedAt":"2026-03-23T16:11:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":null},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Hello everyone,\n>\n> v2 addresses all the feedback received in v1, many thanks to everyone\n> who contributed.\n>\n> This series is just minor cleanups / refactorings + two minor feature additions\n> to `git hook list`, which resulted from the previous series review discussions:\n>\n> 1. The ability to show the config scope (--show-scope).\n> 2. The ability to show which hooks are disabled.\n>\n> This is now based on the master branch.\n>\n> I have pushed the branch to Github [1] and provided a clean CI run [2] with\n> the exception of a known breakage for some MacOS builders (REG_ENHANCED).\n>\n> Thanks again,\n> Adrian\n>\n> 1: https://github.com/10ne1/git/tree/dev/aratiu/config-cleanups-v2\n> 2: https://github.com/10ne1/git/actions/runs/23340298770\n>\n> Changes in v2:\n> * Cleanly rebased on master, no conflicts (Adrian)\n> * Fix first patch build break by updating call-sites in same commit (Szeder)\n> * Drop UNUSED from function declaration in the header file (Eric)\n> * Drop the new struct hook_config_cache because it's redundant (Junio)\n> * git hook list now prints in tab separated output format similar\n>   to git config --show-scope to improve machine parseability (Junio, Patrick)\n> * Fix small style issues, comments, type, commit messages. (Eric, Patrick)\n\nThese came just before the weekend for many people, so let's hold to\nsee if we hear further comments for a few days and then merge it\ndown to 'next'.\n\nThanks, all.\n"},{"id":"539814","messageId":"acJNW0m2wHfRStqY@pks.im","threadId":"65171","inReplyTo":"20260320115211.177351-3-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 02/10] hook: fix minor style issues","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-24T08:37:47Z","receivedAt":"2026-03-24T08:37:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Fri, Mar 20, 2026 at 01:52:03PM +0200, Adrian Ratiu wrote:\n> Fix some minor style nits pointed by Patrick, Junio and Eric:\n\nTiny nit, not worth rerolling over: \"pointed out by\"\n\n> diff --git a/builtin/hook.c b/builtin/hook.c\n> index 83020dfb4f..e641614b84 100644\n> --- a/builtin/hook.c\n> +++ b/builtin/hook.c\n> @@ -5,8 +5,6 @@\n>  #include \"gettext.h\"\n>  #include \"hook.h\"\n>  #include \"parse-options.h\"\n> -#include \"strvec.h\"\n> -#include \"abspath.h\"\n\nAnother thing we could address while at it is to sort the headers\n(except \"builtin.h\" of course). Feel free to ignore though.\n\n> diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\n> index e34edff406..991d6ca7d5 100644\n> --- a/builtin/receive-pack.c\n> +++ b/builtin/receive-pack.c\n> @@ -904,7 +904,8 @@ static int feed_receive_hook_cb(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_\n>  static void *receive_hook_feed_state_alloc(void *feed_pipe_ctx)\n>  {\n>  \tstruct receive_hook_feed_state *init_state = feed_pipe_ctx;\n> -\tstruct receive_hook_feed_state *data = xcalloc(1, sizeof(*data));\n> +\tstruct receive_hook_feed_state *data;\n> +\tCALLOC_ARRAY(data, 1);\n\nI think it might help the reader to have an empty line between variables\nand logic.\n\n> @@ -928,7 +929,11 @@ static int run_receive_hook(struct command *commands,\n>  {\n>  \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>  \tstruct command *iter = commands;\n> -\tstruct receive_hook_feed_state feed_init_state = { 0 };\n> +\tstruct receive_hook_feed_state feed_init_state = {\n> +\t\t.cmd = commands,\n> +\t\t.skip_broken = skip_broken,\n> +\t\t.buf = STRBUF_INIT,\n> +\t};\n\nInteresting. The buffer here isn't only a style fix, but an actual bug\nfix, isn't it?\n\n> diff --git a/hook.c b/hook.c\n> index 67cc9a66df..349db729f6 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -227,7 +227,8 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n>  \t/* Construct the cache from parsed configs. */\n>  \tstrmap_for_each_entry(&cb_data.event_hooks, &iter, e) {\n>  \t\tstruct string_list *hook_names = e->value;\n> -\t\tstruct string_list *hooks = xcalloc(1, sizeof(*hooks));\n> +\t\tstruct string_list *hooks;\n> +\t\tCALLOC_ARRAY(hooks, 1);\n>  \n>  \t\tstring_list_init_dup(hooks);\n>  \n\nSame nit here: I'd move the empty line to come before `CALLOC_ARRAY()`.\n\n> @@ -311,7 +312,8 @@ static void list_hooks_add_configured(struct repository *r,\n>  \tfor (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {\n>  \t\tconst char *friendly_name = configured_hooks->items[i].string;\n>  \t\tconst char *command = configured_hooks->items[i].util;\n> -\t\tstruct hook *hook = xcalloc(1, sizeof(struct hook));\n> +\t\tstruct hook *hook;\n> +\t\tCALLOC_ARRAY(hook, 1);\n>  \n>  \t\tif (options && options->feed_pipe_cb_data_alloc)\n>  \t\t\thook->feed_pipe_cb_data =\n\nAnd here.\n\nNone of my nits are really important, so please feel free to address or\nignore them as you like.\n\nPatrick\n"},{"id":"539815","messageId":"acJNYPgOSO86hZYq@pks.im","threadId":"65171","inReplyTo":"20260320115211.177351-6-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 05/10] hook: replace hook_list_clear() -> string_list_clear_func()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-24T08:37:52Z","receivedAt":"2026-03-24T08:37:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Fri, Mar 20, 2026 at 01:52:06PM +0200, Adrian Ratiu wrote:\n> diff --git a/hook.c b/hook.c\n> index 6dfaa7e9b1..f6bb1999ae 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -52,8 +52,14 @@ const char *find_hook(struct repository *r, const char *name)\n>  \treturn path.buf;\n>  }\n>  \n> -static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n> +/*\n> + * Frees a struct hook stored as the util pointer of a string_list_item.\n> + * Suitable for use as a string_list_clear_func_t callback.\n> + */\n\nThis comment should probably live in the header. I also wonder whether\nthis wrapper isn't a bit too specific to freeing hooks with a string\nlist. Maybe it would be preferable to expose a \"proper\" `hook_free()`\nfunction that only takes a hook, and then provide a small wrapper\nfunction for freeing in the string list?\n\nIf so it feels like we're going a bit full circle though. Maybe the\noriginal code wasn't all that bad in the first place?\n\nPatrick\n"},{"id":"539816","messageId":"acJNZ6Tu71YSL57o@pks.im","threadId":"65171","inReplyTo":"20260320115211.177351-10-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 09/10] hook: show config scope in git hook list","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-24T08:37:59Z","receivedAt":"2026-03-24T08:38:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Fri, Mar 20, 2026 at 01:52:10PM +0200, Adrian Ratiu wrote:\n> diff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\n> index 966388660a..e7d399ae57 100644\n> --- a/Documentation/git-hook.adoc\n> +++ b/Documentation/git-hook.adoc\n> @@ -9,7 +9,7 @@ SYNOPSIS\n>  --------\n>  [verse]\n>  'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\n> -'git hook' list [-z] <hook-name>\n> +'git hook' list [-z] [--show-scope] <hook-name>\n>  \n>  DESCRIPTION\n>  -----------\n\nTaking a random patch that relates to the git-hook(1) command. I was\nwondering whether we want to introduce another change here that will\ncause git-hook(1) to bail out when given an unknown hook name.\n\nI know that we explicitly want to allow having custom hook events, but I\nwould argue that 99% of all invocations will use any of Git's own hook\nevents. And given that it's really easy to misspell the \"prereceive\"\nhook (which really is \"pre-receive\") I think it would be nice if we told\nthe user that it's an unknown hook instead of silently doing nothing.\n\nTo cover the original use case we could then add something like\n\"--allow-unknown-hook-name\" to make the caller explicitly accept non-Git\nhook events.\n\n> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n> index 7eee84fc39..22cca15fda 100755\n> --- a/t/t1800-hook.sh\n> +++ b/t/t1800-hook.sh\n> @@ -408,6 +408,25 @@ test_expect_success 'configured hooks run before hookdir hook' '\n>  \ttest_cmp expected actual\n>  '\n>  \n> +test_expect_success 'git hook list --show-scope shows config scope' '\n> +\ttest_config_global hook.global-hook.command \"echo global\" &&\n> +\ttest_config_global hook.global-hook.event test-hook --add &&\n> +\ttest_config hook.local-hook.command \"echo local\" &&\n> +\ttest_config hook.local-hook.event test-hook --add &&\n> +\n> +\tcat >expected <<-\\EOF &&\n> +\tglobal\tglobal-hook\n> +\tlocal\tlocal-hook\n> +\tEOF\n> +\tgit hook list --show-scope test-hook >actual &&\n> +\ttest_cmp expected actual &&\n> +\n> +\t# without --show-scope the scope must not appear\n> +\tgit hook list test-hook >actual &&\n> +\ttest_grep ! \"^global\t\" actual &&\n> +\ttest_grep ! \"^local\t\" actual\n> +'\n\nDo we also want to add a hook discovered via \".git/hooks\" to show how it\ninteracts with the new flag?\n\nPatrick\n"},{"id":"539817","messageId":"acJNblQ8WRGIri0V@pks.im","threadId":"65171","inReplyTo":"xmqqmrzy5xwe.fsf@gitster.g","subject":"Re: [PATCH v2 00/10] config-hook cleanups and two small 'git hook list' features","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-24T08:38:06Z","receivedAt":"2026-03-24T08:38:12Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Mon, Mar 23, 2026 at 09:11:13AM -0700, Junio C Hamano wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n> \n> > Hello everyone,\n> >\n> > v2 addresses all the feedback received in v1, many thanks to everyone\n> > who contributed.\n> >\n> > This series is just minor cleanups / refactorings + two minor feature additions\n> > to `git hook list`, which resulted from the previous series review discussions:\n> >\n> > 1. The ability to show the config scope (--show-scope).\n> > 2. The ability to show which hooks are disabled.\n> >\n> > This is now based on the master branch.\n> >\n> > I have pushed the branch to Github [1] and provided a clean CI run [2] with\n> > the exception of a known breakage for some MacOS builders (REG_ENHANCED).\n> >\n> > Thanks again,\n> > Adrian\n> >\n> > 1: https://github.com/10ne1/git/tree/dev/aratiu/config-cleanups-v2\n> > 2: https://github.com/10ne1/git/actions/runs/23340298770\n> >\n> > Changes in v2:\n> > * Cleanly rebased on master, no conflicts (Adrian)\n> > * Fix first patch build break by updating call-sites in same commit (Szeder)\n> > * Drop UNUSED from function declaration in the header file (Eric)\n> > * Drop the new struct hook_config_cache because it's redundant (Junio)\n> > * git hook list now prints in tab separated output format similar\n> >   to git config --show-scope to improve machine parseability (Junio, Patrick)\n> > * Fix small style issues, comments, type, commit messages. (Eric, Patrick)\n> \n> These came just before the weekend for many people, so let's hold to\n> see if we hear further comments for a few days and then merge it\n> down to 'next'.\n\nSorry, I've been a bit behind on the mailing list recently. I've got a\nfew further comments that might warrant a v3, but I think we're overall\nclose.\n\nThanks!\n\nPatrick\n"},{"id":"539818","messageId":"acJNdKyz-ISsq4fJ@pks.im","threadId":"65171","inReplyTo":"20260320115211.177351-11-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 10/10] hook: show disabled hooks in \"git hook list\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-24T08:38:12Z","receivedAt":"2026-03-24T08:38:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Fri, Mar 20, 2026 at 01:52:11PM +0200, Adrian Ratiu wrote:\n> diff --git a/hook.c b/hook.c\n> index aa08c38c27..0e09b9a2bb 100644\n> --- a/hook.c\n> +++ b/hook.c\n> @@ -119,6 +119,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n>  struct hook_config_cache_entry {\n>  \tchar *command;\n>  \tenum config_scope scope;\n> +\tunsigned int disabled:1;\n>  };\n>  \n>  /*\n\nCurious, this is now a single-bit int. I still would have expected a\nproper bool here :)\n\n> diff --git a/hook.h b/hook.h\n> index 92e9faf9bb..7c8c3d471e 100644\n> --- a/hook.h\n> +++ b/hook.h\n> @@ -31,6 +31,7 @@ struct hook {\n>  \t\t\tconst char *friendly_name;\n>  \t\t\tconst char *command;\n>  \t\t\tenum config_scope scope;\n> +\t\t\tunsigned int disabled:1;\n>  \t\t} configured;\n>  \t} u;\n\nSame here, I would expect a proper bool.\n\nPatrick\n"},{"id":"539854","messageId":"xmqqtsu519yt.fsf@gitster.g","threadId":"65171","inReplyTo":"acJNdKyz-ISsq4fJ@pks.im","subject":"Re: [PATCH v2 10/10] hook: show disabled hooks in \"git hook list\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-24T16:14:02Z","receivedAt":"2026-03-24T16:14:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":null},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Fri, Mar 20, 2026 at 01:52:11PM +0200, Adrian Ratiu wrote:\n>> diff --git a/hook.c b/hook.c\n>> index aa08c38c27..0e09b9a2bb 100644\n>> --- a/hook.c\n>> +++ b/hook.c\n>> @@ -119,6 +119,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n>>  struct hook_config_cache_entry {\n>>  \tchar *command;\n>>  \tenum config_scope scope;\n>> +\tunsigned int disabled:1;\n>>  };\n>>  \n>>  /*\n>\n> Curious, this is now a single-bit int. I still would have expected a\n> proper bool here :)\n>\n>> diff --git a/hook.h b/hook.h\n>> index 92e9faf9bb..7c8c3d471e 100644\n>> --- a/hook.h\n>> +++ b/hook.h\n>> @@ -31,6 +31,7 @@ struct hook {\n>>  \t\t\tconst char *friendly_name;\n>>  \t\t\tconst char *command;\n>>  \t\t\tenum config_scope scope;\n>> +\t\t\tunsigned int disabled:1;\n>>  \t\t} configured;\n>>  \t} u;\n>\n> Same here, I would expect a proper bool.\n>\n> Patrick\n\nI do not know what an improper bool is, though ;-).\n"},{"id":"539868","messageId":"87mrzxcb04.fsf@gentoo.mail-host-address-is-not-set","threadId":"65171","inReplyTo":"acJNblQ8WRGIri0V@pks.im","subject":"Re: [PATCH v2 00/10] config-hook cleanups and two small 'git hook list' features","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-24T18:56:11Z","receivedAt":"2026-03-24T18:56:31Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Tue, 24 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Mar 23, 2026 at 09:11:13AM -0700, Junio C Hamano wrote:\n>> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>> \n>> > Hello everyone,\n>> >\n>> > v2 addresses all the feedback received in v1, many thanks to everyone\n>> > who contributed.\n>> >\n>> > This series is just minor cleanups / refactorings + two minor feature additions\n>> > to `git hook list`, which resulted from the previous series review discussions:\n>> >\n>> > 1. The ability to show the config scope (--show-scope).\n>> > 2. The ability to show which hooks are disabled.\n>> >\n>> > This is now based on the master branch.\n>> >\n>> > I have pushed the branch to Github [1] and provided a clean CI run [2] with\n>> > the exception of a known breakage for some MacOS builders (REG_ENHANCED).\n>> >\n>> > Thanks again,\n>> > Adrian\n>> >\n>> > 1: https://github.com/10ne1/git/tree/dev/aratiu/config-cleanups-v2\n>> > 2: https://github.com/10ne1/git/actions/runs/23340298770\n>> >\n>> > Changes in v2:\n>> > * Cleanly rebased on master, no conflicts (Adrian)\n>> > * Fix first patch build break by updating call-sites in same commit (Szeder)\n>> > * Drop UNUSED from function declaration in the header file (Eric)\n>> > * Drop the new struct hook_config_cache because it's redundant (Junio)\n>> > * git hook list now prints in tab separated output format similar\n>> >   to git config --show-scope to improve machine parseability (Junio, Patrick)\n>> > * Fix small style issues, comments, type, commit messages. (Eric, Patrick)\n>> \n>> These came just before the weekend for many people, so let's hold to\n>> see if we hear further comments for a few days and then merge it\n>> down to 'next'.\n>\n> Sorry, I've been a bit behind on the mailing list recently. I've got a\n> few further comments that might warrant a v3, but I think we're overall\n> close.\n\nNo worries, I appreciate all your reviewes and feedback!\n\nI'll give it 1-2 more days in case other people have more feedback then\nsend a v3 which addresses everything you pointed out.\n"},{"id":"539873","messageId":"87h5q5c9xc.fsf@collabora.com","threadId":"65171","inReplyTo":"acJNW0m2wHfRStqY@pks.im","subject":"Re: [PATCH v2 02/10] hook: fix minor style issues","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-24T19:19:27Z","receivedAt":"2026-03-24T19:19:49Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Tue, 24 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Fri, Mar 20, 2026 at 01:52:03PM +0200, Adrian Ratiu wrote:\n>> diff --git a/builtin/hook.c b/builtin/hook.c\n>> index 83020dfb4f..e641614b84 100644\n>> --- a/builtin/hook.c\n>> +++ b/builtin/hook.c\n>> @@ -5,8 +5,6 @@\n>>  #include \"gettext.h\"\n>>  #include \"hook.h\"\n>>  #include \"parse-options.h\"\n>> -#include \"strvec.h\"\n>> -#include \"abspath.h\"\n>pp\n> Another thing we could address while at it is to sort the headers\n> (except \"builtin.h\" of course). Feel free to ignore though.\n>\n\nI'll fix all the nits you pointed out in v3, no worries. :)\n\n<snip>\n\n>> @@ -928,7 +929,11 @@ static int run_receive_hook(struct command *commands,\n>>  {\n>>  \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n>>  \tstruct command *iter = commands;\n>> -\tstruct receive_hook_feed_state feed_init_state = { 0 };\n>> +\tstruct receive_hook_feed_state feed_init_state = {\n>> +\t\t.cmd = commands,\n>> +\t\t.skip_broken = skip_broken,\n>> +\t\t.buf = STRBUF_INIT,\n>> +\t};\n>\n> Interesting. The buffer here isn't only a style fix, but an actual bug\n> fix, isn't it?\n\nIn theory yes, it's a bug. I'll fix it in a separate commit.\n\nIn practice there is no difference because this is passed to\nreceive_hook_feed_state_alloc() which creates \"copies\" for each hook and\nproperly initializes each copy with strbuf_init(&data->buf, 0);\n\n(The differnce is between .buf being NULL vs pointing to a static array\nstrbuf_slopbuf with one element containing NULL )\n\nI'll fix it anyway and explain in a separate commit to avoid confusion.\n\nThanks for spotting this!\n"},{"id":"539874","messageId":"87ecl9c9qk.fsf@collabora.com","threadId":"65171","inReplyTo":"acJNdKyz-ISsq4fJ@pks.im","subject":"Re: [PATCH v2 10/10] hook: show disabled hooks in \"git hook list\"","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-24T19:23:31Z","receivedAt":"2026-03-24T19:23:49Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Tue, 24 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Fri, Mar 20, 2026 at 01:52:11PM +0200, Adrian Ratiu wrote:\n>> diff --git a/hook.c b/hook.c\n>> index aa08c38c27..0e09b9a2bb 100644\n>> --- a/hook.c\n>> +++ b/hook.c\n>> @@ -119,6 +119,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n>>  struct hook_config_cache_entry {\n>>  \tchar *command;\n>>  \tenum config_scope scope;\n>> +\tunsigned int disabled:1;\n>>  };\n>>  \n>>  /*\n>\n> Curious, this is now a single-bit int. I still would have expected a\n> proper bool here :)\n\nYes, I did it this way because the parallel series also adds another\n1-bit int after this one. :) \n\nI'll make both these a proper bool as you requested in the other series\nas well. I don't have any preference and am ok with bool as well. :)\n"},{"id":"539889","messageId":"87bjgcdfim.fsf@collabora.com","threadId":"65171","inReplyTo":"acJNYPgOSO86hZYq@pks.im","subject":"Re: [PATCH v2 05/10] hook: replace hook_list_clear() -> string_list_clear_func()","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-24T22:33:21Z","receivedAt":"2026-03-24T22:33:40Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Tue, 24 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Fri, Mar 20, 2026 at 01:52:06PM +0200, Adrian Ratiu wrote:\n>> diff --git a/hook.c b/hook.c\n>> index 6dfaa7e9b1..f6bb1999ae 100644\n>> --- a/hook.c\n>> +++ b/hook.c\n>> @@ -52,8 +52,14 @@ const char *find_hook(struct repository *r, const char *name)\n>>  \treturn path.buf;\n>>  }\n>>  \n>> -static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n>> +/*\n>> + * Frees a struct hook stored as the util pointer of a string_list_item.\n>> + * Suitable for use as a string_list_clear_func_t callback.\n>> + */\n>\n> This comment should probably live in the header. I also wonder whether\n> this wrapper isn't a bit too specific to freeing hooks with a string\n> list. Maybe it would be preferable to expose a \"proper\" `hook_free()`\n> function that only takes a hook, and then provide a small wrapper\n> function for freeing in the string list?\n>\n> If so it feels like we're going a bit full circle though. Maybe the\n> original code wasn't all that bad in the first place?\n\nI prefer this new design (suggested by you), because:\n\n1. We use the generic string_list_clear_func() API.\n\n2. Each struct hook owns its data_free callback, which means that callers\ndon't need to keep track of internal state (e.g. when to pass NULL to\nskip cleanup).\n\n3. The hook API itself is cleaner for hook.[ch] users, because it's\nalways string_list_clear_func(head, hook_free); regardless of context.\n\nSo if it's ok with you, let's use your new design. :)\n\nI'll move the comment to the header (it's already there, I just forgot\nto remove the duplicated comment in hook.c above the function\ndefinition).\n"},{"id":"539893","messageId":"acNx6pDjOMQaMsBr@pks.im","threadId":"65171","inReplyTo":"87bjgcdfim.fsf@collabora.com","subject":"Re: [PATCH v2 05/10] hook: replace hook_list_clear() -> string_list_clear_func()","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-25T05:26:02Z","receivedAt":"2026-03-25T05:26:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Wed, Mar 25, 2026 at 12:33:21AM +0200, Adrian Ratiu wrote:\n> On Tue, 24 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> > On Fri, Mar 20, 2026 at 01:52:06PM +0200, Adrian Ratiu wrote:\n> >> diff --git a/hook.c b/hook.c\n> >> index 6dfaa7e9b1..f6bb1999ae 100644\n> >> --- a/hook.c\n> >> +++ b/hook.c\n> >> @@ -52,8 +52,14 @@ const char *find_hook(struct repository *r, const char *name)\n> >>  \treturn path.buf;\n> >>  }\n> >>  \n> >> -static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n> >> +/*\n> >> + * Frees a struct hook stored as the util pointer of a string_list_item.\n> >> + * Suitable for use as a string_list_clear_func_t callback.\n> >> + */\n> >\n> > This comment should probably live in the header. I also wonder whether\n> > this wrapper isn't a bit too specific to freeing hooks with a string\n> > list. Maybe it would be preferable to expose a \"proper\" `hook_free()`\n> > function that only takes a hook, and then provide a small wrapper\n> > function for freeing in the string list?\n> >\n> > If so it feels like we're going a bit full circle though. Maybe the\n> > original code wasn't all that bad in the first place?\n> \n> I prefer this new design (suggested by you), because:\n> \n> 1. We use the generic string_list_clear_func() API.\n> \n> 2. Each struct hook owns its data_free callback, which means that callers\n> don't need to keep track of internal state (e.g. when to pass NULL to\n> skip cleanup).\n> \n> 3. The hook API itself is cleaner for hook.[ch] users, because it's\n> always string_list_clear_func(head, hook_free); regardless of context.\n> \n> So if it's ok with you, let's use your new design. :)\n> \n> I'll move the comment to the header (it's already there, I just forgot\n> to remove the duplicated comment in hook.c above the function\n> definition).\n\nFine with me, thanks!\n\nPatrick\n"},{"id":"539929","messageId":"87v7ekrvvr.fsf@gentoo.mail-host-address-is-not-set","threadId":"65171","inReplyTo":"acJNZ6Tu71YSL57o@pks.im","subject":"Re: [PATCH v2 09/10] hook: show config scope in git hook list","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T11:28:24Z","receivedAt":"2026-03-25T11:28:44Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Tue, 24 Mar 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Fri, Mar 20, 2026 at 01:52:10PM +0200, Adrian Ratiu wrote:\n>> diff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\n>> index 966388660a..e7d399ae57 100644\n>> --- a/Documentation/git-hook.adoc\n>> +++ b/Documentation/git-hook.adoc\n>> @@ -9,7 +9,7 @@ SYNOPSIS\n>>  --------\n>>  [verse]\n>>  'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\n>> -'git hook' list [-z] <hook-name>\n>> +'git hook' list [-z] [--show-scope] <hook-name>\n>>  \n>>  DESCRIPTION\n>>  -----------\n>\n> Taking a random patch that relates to the git-hook(1) command. I was\n> wondering whether we want to introduce another change here that will\n> cause git-hook(1) to bail out when given an unknown hook name.\n>\n> I know that we explicitly want to allow having custom hook events, but I\n> would argue that 99% of all invocations will use any of Git's own hook\n> events. And given that it's really easy to misspell the \"prereceive\"\n> hook (which really is \"pre-receive\") I think it would be nice if we told\n> the user that it's an unknown hook instead of silently doing nothing.\n>\n> To cover the original use case we could then add something like\n> \"--allow-unknown-hook-name\" to make the caller explicitly accept non-Git\n> hook events.\n\nI think this makes sense.\n\nI just wrote and tested the implementation and it works nicely.\n\nI'll include it in v3.\n\n>> diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\n>> index 7eee84fc39..22cca15fda 100755\n>> --- a/t/t1800-hook.sh\n>> +++ b/t/t1800-hook.sh\n>> @@ -408,6 +408,25 @@ test_expect_success 'configured hooks run before hookdir hook' '\n>>  \ttest_cmp expected actual\n>>  '\n>>  \n>> +test_expect_success 'git hook list --show-scope shows config scope' '\n>> +\ttest_config_global hook.global-hook.command \"echo global\" &&\n>> +\ttest_config_global hook.global-hook.event test-hook --add &&\n>> +\ttest_config hook.local-hook.command \"echo local\" &&\n>> +\ttest_config hook.local-hook.event test-hook --add &&\n>> +\n>> +\tcat >expected <<-\\EOF &&\n>> +\tglobal\tglobal-hook\n>> +\tlocal\tlocal-hook\n>> +\tEOF\n>> +\tgit hook list --show-scope test-hook >actual &&\n>> +\ttest_cmp expected actual &&\n>> +\n>> +\t# without --show-scope the scope must not appear\n>> +\tgit hook list test-hook >actual &&\n>> +\ttest_grep ! \"^global\t\" actual &&\n>> +\ttest_grep ! \"^local\t\" actual\n>> +'\n>\n> Do we also want to add a hook discovered via \".git/hooks\" to show how it\n> interacts with the new flag?\n\nYes, good catch, I'll add it in v3.\n"},{"id":"539990","messageId":"20260325195503.1139418-1-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260309005416.2760030-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 00/12] config-hook cleanups and three small git-hook features","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:51Z","receivedAt":"2026-03-25T19:55:53Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Hello everyone,\n\nv3 addresses all the feedback and requests received in v2, many thanks to all\nwho contributed.\n\nLet's please stop adding features since this is getting rather big again. :)\nNew features can be added in subsequent patches.\n\nThis series is mostly for minor cleanups, bug fixes and refactorings + three\nminor feature additions to git-hook, which resulted from review discussions:\n\n1. The ability to show the config scope (--show-scope).\n2. The ability to show which hooks are disabled.\n3. The ability reject unknown hook names with \"--allow-unknown-hook-name\" as\n   an escape hatch.\n\nThe series is based on the master branch.\n\nBranch pushed GitHub: [1]\nSuccessful CI run: [2]\n\nThanks again,\nAdrian\n\n1: https://github.com/10ne1/git/tree/dev/aratiu/config-cleanups-v3\n2: https://github.com/10ne1/git/actions/runs/23540818495\n\nChanges in v3:\n* New commit: properly initialize strbuf in receive-pack.c (Patrick)\n* New commit: add a check which prevents unknown hooks with git-hook(1) (Patrick)\n* Removed duplicated function doc comment between .h and .c files (Patrick)\n* Extended `git hook list` test to also include a hook from the hookdir (Patrick)\n* Converted unsigned int disabled:1 to proper bool (Patrick)\n* Minor commit rewording, header sorting, blank line fixes (Patrick)\n\nRange-diff v2 -> v3:\n 1:  aeefa72f33 =  1:  db8b7b0552 hook: move unsorted_string_list_remove() to string-list.[ch]\n -:  ---------- >  2:  02854ecc8b builtin/receive-pack: properly init receive_hook strbuf\n 2:  90e821bdfa !  3:  14dcedcd9b hook: fix minor style issues\n    @@ Metadata\n      ## Commit message ##\n         hook: fix minor style issues\n     \n    -    Fix some minor style nits pointed by Patrick, Junio and Eric:\n    +    Fix some minor style nits pointed out by Patrick, Junio and Eric:\n           * Use CALLOC_ARRAY instead of xcalloc.\n           * Init struct members during declaration.\n           * Simplify if condition boolean logic.\n    @@ Commit message\n           * Capitalization and full-stop in error/warn messages.\n           * Curly brace on separate line when defining struct.\n           * Comment spelling: free'd -> freed.\n    +      * Sort the included headers.\n    +      * Blank line fixes to improve readability.\n     \n         These contain no logic changes, the code behaves the same as before.\n     \n    @@ builtin/hook.c: static int list(int argc, const char **argv, const char *prefix,\n      \t}\n     \n      ## builtin/receive-pack.c ##\n    +@@\n    + \n    + #include \"builtin.h\"\n    + #include \"abspath.h\"\n    +-\n    ++#include \"commit.h\"\n    ++#include \"commit-reach.h\"\n    + #include \"config.h\"\n    ++#include \"connect.h\"\n    ++#include \"connected.h\"\n    + #include \"environment.h\"\n    ++#include \"exec-cmd.h\"\n    ++#include \"fsck.h\"\n    + #include \"gettext.h\"\n    ++#include \"gpg-interface.h\"\n    + #include \"hex.h\"\n    +-#include \"lockfile.h\"\n    +-#include \"pack.h\"\n    +-#include \"refs.h\"\n    +-#include \"pkt-line.h\"\n    +-#include \"sideband.h\"\n    +-#include \"run-command.h\"\n    + #include \"hook.h\"\n    +-#include \"exec-cmd.h\"\n    +-#include \"commit.h\"\n    ++#include \"lockfile.h\"\n    + #include \"object.h\"\n    +-#include \"remote.h\"\n    +-#include \"connect.h\"\n    +-#include \"string-list.h\"\n    +-#include \"oid-array.h\"\n    +-#include \"connected.h\"\n    +-#include \"strvec.h\"\n    +-#include \"version.h\"\n    +-#include \"gpg-interface.h\"\n    +-#include \"sigchain.h\"\n    +-#include \"fsck.h\"\n    +-#include \"tmp-objdir.h\"\n    +-#include \"oidset.h\"\n    +-#include \"packfile.h\"\n    + #include \"object-file.h\"\n    + #include \"object-name.h\"\n    + #include \"odb.h\"\n    ++#include \"oid-array.h\"\n    ++#include \"oidset.h\"\n    ++#include \"pack.h\"\n    ++#include \"packfile.h\"\n    ++#include \"parse-options.h\"\n    ++#include \"pkt-line.h\"\n    + #include \"protocol.h\"\n    +-#include \"commit-reach.h\"\n    ++#include \"refs.h\"\n    ++#include \"remote.h\"\n    ++#include \"run-command.h\"\n    + #include \"server-info.h\"\n    ++#include \"setup.h\"\n    ++#include \"shallow.h\"\n    ++#include \"sideband.h\"\n    ++#include \"sigchain.h\"\n    ++#include \"string-list.h\"\n    ++#include \"strvec.h\"\n    ++#include \"tmp-objdir.h\"\n    + #include \"trace.h\"\n    + #include \"trace2.h\"\n    ++#include \"version.h\"\n    + #include \"worktree.h\"\n    +-#include \"shallow.h\"\n    +-#include \"setup.h\"\n    +-#include \"parse-options.h\"\n    + \n    + static const char * const receive_pack_usage[] = {\n    + \tN_(\"git receive-pack <git-dir>\"),\n     @@ builtin/receive-pack.c: static int feed_receive_hook_cb(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_\n      static void *receive_hook_feed_state_alloc(void *feed_pipe_ctx)\n      {\n      \tstruct receive_hook_feed_state *init_state = feed_pipe_ctx;\n     -\tstruct receive_hook_feed_state *data = xcalloc(1, sizeof(*data));\n     +\tstruct receive_hook_feed_state *data;\n    ++\n     +\tCALLOC_ARRAY(data, 1);\n      \tdata->report = init_state->report;\n      \tdata->cmd = init_state->cmd;\n      \tdata->skip_broken = init_state->skip_broken;\n    + \tstrbuf_init(&data->buf, 0);\n    ++\n    + \treturn data;\n    + }\n    + \n     @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n      {\n      \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n    @@ builtin/receive-pack.c: static int run_receive_hook(struct command *commands,\n      \t/* set up stdin callback */\n     -\tfeed_init_state.cmd = commands;\n     -\tfeed_init_state.skip_broken = skip_broken;\n    +-\tstrbuf_init(&feed_init_state.buf, 0);\n      \topt.feed_pipe_ctx = &feed_init_state;\n      \topt.feed_pipe = feed_receive_hook_cb;\n      \topt.feed_pipe_cb_data_alloc = receive_hook_feed_state_alloc;\n     \n      ## hook.c ##\n    +@@\n    + #include \"git-compat-util.h\"\n    + #include \"abspath.h\"\n    + #include \"advice.h\"\n    ++#include \"config.h\"\n    ++#include \"environment.h\"\n    + #include \"gettext.h\"\n    + #include \"hook.h\"\n    +-#include \"path.h\"\n    + #include \"parse.h\"\n    ++#include \"path.h\"\n    + #include \"run-command.h\"\n    +-#include \"config.h\"\n    ++#include \"setup.h\"\n    + #include \"strbuf.h\"\n    + #include \"strmap.h\"\n    +-#include \"environment.h\"\n    +-#include \"setup.h\"\n    + \n    + const char *find_hook(struct repository *r, const char *name)\n    + {\n     @@ hook.c: static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n      \tif (!h)\n      \t\treturn;\n    @@ hook.c: static void build_hook_config_map(struct repository *r, struct strmap *c\n      \t\tstruct string_list *hook_names = e->value;\n     -\t\tstruct string_list *hooks = xcalloc(1, sizeof(*hooks));\n     +\t\tstruct string_list *hooks;\n    -+\t\tCALLOC_ARRAY(hooks, 1);\n      \n    ++\t\tCALLOC_ARRAY(hooks, 1);\n      \t\tstring_list_init_dup(hooks);\n      \n    + \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n     @@ hook.c: static struct strmap *get_hook_config_cache(struct repository *r)\n      \t\t * it just once on the first call.\n      \t\t */\n    @@ hook.c: static void list_hooks_add_configured(struct repository *r,\n      \t\tconst char *command = configured_hooks->items[i].util;\n     -\t\tstruct hook *hook = xcalloc(1, sizeof(struct hook));\n     +\t\tstruct hook *hook;\n    ++\n     +\t\tCALLOC_ARRAY(hook, 1);\n      \n      \t\tif (options && options->feed_pipe_cb_data_alloc)\n    @@ hook.c: int run_hooks_opt(struct repository *r, const char *hook_name,\n      \tif (options->invoked_hook)\n     \n      ## hook.h ##\n    +@@\n    + #ifndef HOOK_H\n    + #define HOOK_H\n    +-#include \"strvec.h\"\n    + #include \"run-command.h\"\n    + #include \"string-list.h\"\n    + #include \"strmap.h\"\n    ++#include \"strvec.h\"\n    + \n    + struct repository;\n    + \n     @@ hook.h: struct hook {\n      typedef void (*cb_data_free_fn)(void *data);\n      typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n 3:  dee1dd49a4 =  4:  bd49f58486 hook: rename cb_data_free/alloc -> hook_data_free/alloc\n 4:  86f61204b3 =  5:  9502a98b09 hook: detect & emit two more bugs\n 5:  30542de351 !  6:  b623e682c7 hook: replace hook_list_clear() -> string_list_clear_func()\n    @@ Commit message\n         API becomes cleaner, e.g. no more calls with NULL function args\n         like hook_list_clear(hooks, NULL).\n     \n    +    In other words, the callers don't need to keep track of hook\n    +    internal state to determine when cleanup is necessary or not\n    +    (pass NULL) because each `struct hook` now owns its data_free\n    +    callback.\n    +\n         Suggested-by: Patrick Steinhardt <ps@pks.im>\n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n    @@ hook.c: const char *find_hook(struct repository *r, const char *name)\n      }\n      \n     -static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n    -+/*\n    -+ * Frees a struct hook stored as the util pointer of a string_list_item.\n    -+ * Suitable for use as a string_list_clear_func_t callback.\n    -+ */\n     +void hook_free(void *p, const char *str UNUSED)\n      {\n     +\tstruct hook *h = p;\n    @@ hook.c: static void list_hooks_add_default(struct repository *r, const char *hoo\n      \th->kind = HOOK_TRADITIONAL;\n      \th->u.traditional.path = xstrdup(hook_path);\n     @@ hook.c: static void list_hooks_add_configured(struct repository *r,\n    - \t\tstruct hook *hook;\n    + \n      \t\tCALLOC_ARRAY(hook, 1);\n      \n     -\t\tif (options && options->feed_pipe_cb_data_alloc)\n 6:  4e1374b84e =  7:  315b62b24e hook: make consistent use of friendly-name in docs\n 7:  075f8202aa =  8:  830cea298b t1800: add test to verify hook execution ordering\n 8:  8f948bbbe7 !  9:  4f4a720cb8 hook: introduce hook_config_cache_entry for per-hook data\n    @@ hook.c: static void list_hooks_add_configured(struct repository *r,\n     -\t\tconst char *command = configured_hooks->items[i].util;\n     +\t\tstruct hook_config_cache_entry *entry = configured_hooks->items[i].util;\n      \t\tstruct hook *hook;\n    - \t\tCALLOC_ARRAY(hook, 1);\n      \n    + \t\tCALLOC_ARRAY(hook, 1);\n     @@ hook.c: static void list_hooks_add_configured(struct repository *r,\n      \n      \t\thook->kind = HOOK_CONFIGURED;\n 9:  dbf81604ed ! 10:  164e3df981 hook: show config scope in git hook list\n    @@ hook.h\n      #ifndef HOOK_H\n      #define HOOK_H\n     +#include \"config.h\"\n    - #include \"strvec.h\"\n      #include \"run-command.h\"\n      #include \"string-list.h\"\n    + #include \"strmap.h\"\n     @@ hook.h: struct hook {\n      \t\tstruct {\n      \t\t\tconst char *friendly_name;\n    @@ t/t1800-hook.sh: test_expect_success 'configured hooks run before hookdir hook'\n      '\n      \n     +test_expect_success 'git hook list --show-scope shows config scope' '\n    ++\tsetup_hookdir &&\n     +\ttest_config_global hook.global-hook.command \"echo global\" &&\n    -+\ttest_config_global hook.global-hook.event test-hook --add &&\n    ++\ttest_config_global hook.global-hook.event pre-commit --add &&\n     +\ttest_config hook.local-hook.command \"echo local\" &&\n    -+\ttest_config hook.local-hook.event test-hook --add &&\n    ++\ttest_config hook.local-hook.event pre-commit --add &&\n     +\n     +\tcat >expected <<-\\EOF &&\n     +\tglobal\tglobal-hook\n     +\tlocal\tlocal-hook\n    ++\thook from hookdir\n     +\tEOF\n    -+\tgit hook list --show-scope test-hook >actual &&\n    ++\tgit hook list --show-scope pre-commit >actual &&\n     +\ttest_cmp expected actual &&\n     +\n     +\t# without --show-scope the scope must not appear\n    -+\tgit hook list test-hook >actual &&\n    ++\tgit hook list pre-commit >actual &&\n     +\ttest_grep ! \"^global\t\" actual &&\n     +\ttest_grep ! \"^local\t\" actual\n     +'\n10:  2a67244b20 ! 11:  ab9cd3ec68 hook: show disabled hooks in \"git hook list\"\n    @@ hook.c: static void list_hooks_add_default(struct repository *r, const char *hoo\n      struct hook_config_cache_entry {\n      \tchar *command;\n      \tenum config_scope scope;\n    -+\tunsigned int disabled:1;\n    ++\tbool disabled;\n      };\n      \n      /*\n    @@ hook.c: static void build_hook_config_map(struct repository *r, struct strmap *c\n     -\t\t\tif (unsorted_string_list_lookup(&cb_data.disabled_hooks,\n     -\t\t\t\t\t\t\thname))\n     -\t\t\t\tcontinue;\n    -+\t\t\tint is_disabled =\n    ++\t\t\tbool is_disabled =\n     +\t\t\t\t!!unsorted_string_list_lookup(\n     +\t\t\t\t\t&cb_data.disabled_hooks, hname);\n      \n    @@ hook.h: struct hook {\n      \t\t\tconst char *friendly_name;\n      \t\t\tconst char *command;\n      \t\t\tenum config_scope scope;\n    -+\t\t\tunsigned int disabled:1;\n    ++\t\t\tbool disabled;\n      \t\t} configured;\n      \t} u;\n      \n -:  ---------- > 12:  dce86488bc hook: reject unknown hook names in git-hook(1)\n\nAdrian Ratiu (12):\n  hook: move unsorted_string_list_remove() to string-list.[ch]\n  builtin/receive-pack: properly init receive_hook strbuf\n  hook: fix minor style issues\n  hook: rename cb_data_free/alloc -> hook_data_free/alloc\n  hook: detect & emit two more bugs\n  hook: replace hook_list_clear() -> string_list_clear_func()\n  hook: make consistent use of friendly-name in docs\n  t1800: add test to verify hook execution ordering\n  hook: introduce hook_config_cache_entry for per-hook data\n  hook: show config scope in git hook list\n  hook: show disabled hooks in \"git hook list\"\n  hook: reject unknown hook names in git-hook(1)\n\n Documentation/config/hook.adoc |  30 +++---\n Documentation/git-hook.adoc    |  27 +++--\n Makefile                       |   1 +\n builtin/hook.c                 |  61 +++++++++--\n builtin/receive-pack.c         |  64 +++++------\n hook.c                         | 192 +++++++++++++++++++++------------\n hook.h                         |  34 +++---\n refs.c                         |   3 +-\n string-list.c                  |   9 ++\n string-list.h                  |   8 ++\n t/t1800-hook.sh                | 167 ++++++++++++++++++++++------\n transport.c                    |   3 +-\n 12 files changed, 424 insertions(+), 175 deletions(-)\n\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539991","messageId":"20260325195503.1139418-2-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 01/12] hook: move unsorted_string_list_remove() to string-list.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:52Z","receivedAt":"2026-03-25T19:55:55Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Move the convenience wrapper from hook to string-list since\nit's a more suitable place. Add a doc comment to the header.\n\nAlso add a free_util arg to make the function more generic\nand make the API similar to other functions in string-list.h.\nUpdate the existing call-sites.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c        | 14 +++-----------\n string-list.c |  9 +++++++++\n string-list.h |  8 ++++++++\n 3 files changed, 20 insertions(+), 11 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 2c8252b2c4..67cc9a66df 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -110,14 +110,6 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tstring_list_append(hook_list, hook_path)->util = h;\n }\n \n-static void unsorted_string_list_remove(struct string_list *list,\n-\t\t\t\t\tconst char *str)\n-{\n-\tstruct string_list_item *item = unsorted_string_list_lookup(list, str);\n-\tif (item)\n-\t\tunsorted_string_list_delete_item(list, item - list->items, 0);\n-}\n-\n /*\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\n@@ -156,7 +148,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\tstruct strmap_entry *e;\n \n \t\t\tstrmap_for_each_entry(&data->event_hooks, &iter, e)\n-\t\t\t\tunsorted_string_list_remove(e->value, hook_name);\n+\t\t\t\tunsorted_string_list_remove(e->value, hook_name, 0);\n \t\t} else {\n \t\t\tstruct string_list *hooks =\n \t\t\t\tstrmap_get(&data->event_hooks, value);\n@@ -168,7 +160,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\t}\n \n \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n-\t\t\tunsorted_string_list_remove(hooks, hook_name);\n+\t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n \t\t\tstring_list_append(hooks, hook_name);\n \t\t}\n \t} else if (!strcmp(subkey, \"command\")) {\n@@ -186,7 +178,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\tbreak;\n \t\tcase 1: /* enabled: undo a prior disabled entry */\n \t\t\tunsorted_string_list_remove(&data->disabled_hooks,\n-\t\t\t\t\t\t    hook_name);\n+\t\t\t\t\t\t    hook_name, 0);\n \t\t\tbreak;\n \t\tdefault:\n \t\t\tbreak; /* ignore unrecognised values */\ndiff --git a/string-list.c b/string-list.c\nindex fffa2ad4b6..d260b873c8 100644\n--- a/string-list.c\n+++ b/string-list.c\n@@ -281,6 +281,15 @@ void unsorted_string_list_delete_item(struct string_list *list, int i, int free_\n \tlist->nr--;\n }\n \n+void unsorted_string_list_remove(struct string_list *list, const char *str,\n+\t\t\t\t int free_util)\n+{\n+\tstruct string_list_item *item = unsorted_string_list_lookup(list, str);\n+\tif (item)\n+\t\tunsorted_string_list_delete_item(list, item - list->items,\n+\t\t\t\t\t\t free_util);\n+}\n+\n /*\n  * append a substring [p..end] to list; return number of things it\n  * appended to the list.\ndiff --git a/string-list.h b/string-list.h\nindex 3ad862a187..b86ee7c099 100644\n--- a/string-list.h\n+++ b/string-list.h\n@@ -265,6 +265,14 @@ struct string_list_item *unsorted_string_list_lookup(struct string_list *list,\n  */\n void unsorted_string_list_delete_item(struct string_list *list, int i, int free_util);\n \n+/**\n+ * Remove the first item matching `str` from an unsorted string_list.\n+ * No-op if `str` is not found. If `free_util` is non-zero, the `util`\n+ * pointer of the removed item is freed before deletion.\n+ */\n+void unsorted_string_list_remove(struct string_list *list, const char *str,\n+\t\t\t\t int free_util);\n+\n /**\n  * Split string into substrings on characters in `delim` and append the\n  * substrings to `list`.  The input string is not modified.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539992","messageId":"20260325195503.1139418-3-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 02/12] builtin/receive-pack: properly init receive_hook strbuf","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:53Z","receivedAt":"2026-03-25T19:55:56Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"The run_receive_hook() stack-allocated `struct receive_hook_feed_state`\nis a template with initial values for child states allocated on the heap\nfor each hook process, by calling receive_hook_feed_state_alloc() when\nspinning up each hook child.\n\nAll these values are already initialized to zero, however I forgot to\nproperly initialize the strbuf, which I left NULL.\n\nThis is more of a code cleanup because in practice it has no effect,\nthe states used by the children are always initialized, however it's\ngood to fix in case someone ends up accidentally dereferencing the NULL\npointer in the future.\n\nReported-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/receive-pack.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex e34edff406..a1ffe4570f 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -963,6 +963,7 @@ static int run_receive_hook(struct command *commands,\n \t/* set up stdin callback */\n \tfeed_init_state.cmd = commands;\n \tfeed_init_state.skip_broken = skip_broken;\n+\tstrbuf_init(&feed_init_state.buf, 0);\n \topt.feed_pipe_ctx = &feed_init_state;\n \topt.feed_pipe = feed_receive_hook_cb;\n \topt.feed_pipe_cb_data_alloc = receive_hook_feed_state_alloc;\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539993","messageId":"20260325195503.1139418-5-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 04/12] hook: rename cb_data_free/alloc -> hook_data_free/alloc","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:55Z","receivedAt":"2026-03-25T19:55:59Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Rename the hook callback function types to use the hook prefix.\n\nThis is a style fix with no logic changes.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c |  4 ++--\n hook.h | 10 +++++-----\n 2 files changed, 7 insertions(+), 7 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex 935237fc1d..4a0db5cfeb 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -52,7 +52,7 @@ const char *find_hook(struct repository *r, const char *name)\n \treturn path.buf;\n }\n \n-static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n+static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n {\n \tif (!h)\n \t\treturn;\n@@ -70,7 +70,7 @@ static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n \tfree(h);\n }\n \n-void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free)\n+void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free)\n {\n \tstruct string_list_item *item;\n \ndiff --git a/hook.h b/hook.h\nindex 1c447cbb6b..965794a5b8 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -43,8 +43,8 @@ struct hook {\n \tvoid *feed_pipe_cb_data;\n };\n \n-typedef void (*cb_data_free_fn)(void *data);\n-typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n+typedef void (*hook_data_free_fn)(void *data);\n+typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n \n struct run_hooks_opt {\n \t/* Environment vars to be set for each hook */\n@@ -131,14 +131,14 @@ struct run_hooks_opt {\n \t *\n \t * The `feed_pipe_ctx` pointer can be used to pass initialization data.\n \t */\n-\tcb_data_alloc_fn feed_pipe_cb_data_alloc;\n+\thook_data_alloc_fn feed_pipe_cb_data_alloc;\n \n \t/**\n \t * Called to free the memory initialized by `feed_pipe_cb_data_alloc`.\n \t *\n \t * Must always be provided when `feed_pipe_cb_data_alloc` is provided.\n \t */\n-\tcb_data_free_fn feed_pipe_cb_data_free;\n+\thook_data_free_fn feed_pipe_cb_data_free;\n };\n \n #define RUN_HOOKS_OPT_INIT { \\\n@@ -188,7 +188,7 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n  * Frees the memory allocated for the hook list, including the `struct hook`\n  * items and their internal state.\n  */\n-void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free);\n+void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free);\n \n /**\n  * Frees the hook configuration cache stored in `struct repository`.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539994","messageId":"20260325195503.1139418-4-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 03/12] hook: fix minor style issues","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:54Z","receivedAt":"2026-03-25T19:56:00Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Fix some minor style nits pointed out by Patrick, Junio and Eric:\n  * Use CALLOC_ARRAY instead of xcalloc.\n  * Init struct members during declaration.\n  * Simplify if condition boolean logic.\n  * Missing curly braces in if/else stmts.\n  * Unnecessary header includes.\n  * Capitalization and full-stop in error/warn messages.\n  * Curly brace on separate line when defining struct.\n  * Comment spelling: free'd -> freed.\n  * Sort the included headers.\n  * Blank line fixes to improve readability.\n\nThese contain no logic changes, the code behaves the same as before.\n\nSuggested-by: Eric Sunshine <sunshine@sunshineco.com>\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c         |  6 ++--\n builtin/receive-pack.c | 65 ++++++++++++++++++++++--------------------\n hook.c                 | 34 +++++++++++-----------\n hook.h                 |  5 ++--\n refs.c                 |  3 +-\n t/t1800-hook.sh        |  2 +-\n transport.c            |  3 +-\n 7 files changed, 61 insertions(+), 57 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 83020dfb4f..e641614b84 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -5,8 +5,6 @@\n #include \"gettext.h\"\n #include \"hook.h\"\n #include \"parse-options.h\"\n-#include \"strvec.h\"\n-#include \"abspath.h\"\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n@@ -51,7 +49,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \t * arguments later they probably should be caught by parse_options.\n \t */\n \tif (argc != 1)\n-\t\tusage_msg_opt(_(\"You must specify a hook event name to list.\"),\n+\t\tusage_msg_opt(_(\"you must specify a hook event name to list\"),\n \t\t\t      builtin_hook_list_usage, list_options);\n \n \thookname = argv[0];\n@@ -59,7 +57,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \thead = list_hooks(repo, hookname, NULL);\n \n \tif (!head->nr) {\n-\t\twarning(_(\"No hooks found for event '%s'\"), hookname);\n+\t\twarning(_(\"no hooks found for event '%s'\"), hookname);\n \t\tret = 1; /* no hooks found */\n \t\tgoto cleanup;\n \t}\ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex a1ffe4570f..cb3656a034 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -3,46 +3,45 @@\n \n #include \"builtin.h\"\n #include \"abspath.h\"\n-\n+#include \"commit.h\"\n+#include \"commit-reach.h\"\n #include \"config.h\"\n+#include \"connect.h\"\n+#include \"connected.h\"\n #include \"environment.h\"\n+#include \"exec-cmd.h\"\n+#include \"fsck.h\"\n #include \"gettext.h\"\n+#include \"gpg-interface.h\"\n #include \"hex.h\"\n-#include \"lockfile.h\"\n-#include \"pack.h\"\n-#include \"refs.h\"\n-#include \"pkt-line.h\"\n-#include \"sideband.h\"\n-#include \"run-command.h\"\n #include \"hook.h\"\n-#include \"exec-cmd.h\"\n-#include \"commit.h\"\n+#include \"lockfile.h\"\n #include \"object.h\"\n-#include \"remote.h\"\n-#include \"connect.h\"\n-#include \"string-list.h\"\n-#include \"oid-array.h\"\n-#include \"connected.h\"\n-#include \"strvec.h\"\n-#include \"version.h\"\n-#include \"gpg-interface.h\"\n-#include \"sigchain.h\"\n-#include \"fsck.h\"\n-#include \"tmp-objdir.h\"\n-#include \"oidset.h\"\n-#include \"packfile.h\"\n #include \"object-file.h\"\n #include \"object-name.h\"\n #include \"odb.h\"\n+#include \"oid-array.h\"\n+#include \"oidset.h\"\n+#include \"pack.h\"\n+#include \"packfile.h\"\n+#include \"parse-options.h\"\n+#include \"pkt-line.h\"\n #include \"protocol.h\"\n-#include \"commit-reach.h\"\n+#include \"refs.h\"\n+#include \"remote.h\"\n+#include \"run-command.h\"\n #include \"server-info.h\"\n+#include \"setup.h\"\n+#include \"shallow.h\"\n+#include \"sideband.h\"\n+#include \"sigchain.h\"\n+#include \"string-list.h\"\n+#include \"strvec.h\"\n+#include \"tmp-objdir.h\"\n #include \"trace.h\"\n #include \"trace2.h\"\n+#include \"version.h\"\n #include \"worktree.h\"\n-#include \"shallow.h\"\n-#include \"setup.h\"\n-#include \"parse-options.h\"\n \n static const char * const receive_pack_usage[] = {\n \tN_(\"git receive-pack <git-dir>\"),\n@@ -904,11 +903,14 @@ static int feed_receive_hook_cb(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_\n static void *receive_hook_feed_state_alloc(void *feed_pipe_ctx)\n {\n \tstruct receive_hook_feed_state *init_state = feed_pipe_ctx;\n-\tstruct receive_hook_feed_state *data = xcalloc(1, sizeof(*data));\n+\tstruct receive_hook_feed_state *data;\n+\n+\tCALLOC_ARRAY(data, 1);\n \tdata->report = init_state->report;\n \tdata->cmd = init_state->cmd;\n \tdata->skip_broken = init_state->skip_broken;\n \tstrbuf_init(&data->buf, 0);\n+\n \treturn data;\n }\n \n@@ -928,7 +930,11 @@ static int run_receive_hook(struct command *commands,\n {\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tstruct command *iter = commands;\n-\tstruct receive_hook_feed_state feed_init_state = { 0 };\n+\tstruct receive_hook_feed_state feed_init_state = {\n+\t\t.cmd = commands,\n+\t\t.skip_broken = skip_broken,\n+\t\t.buf = STRBUF_INIT,\n+\t};\n \tstruct async sideband_async;\n \tint sideband_async_started = 0;\n \tint saved_stderr = -1;\n@@ -961,9 +967,6 @@ static int run_receive_hook(struct command *commands,\n \tprepare_sideband_async(&sideband_async, &saved_stderr, &sideband_async_started);\n \n \t/* set up stdin callback */\n-\tfeed_init_state.cmd = commands;\n-\tfeed_init_state.skip_broken = skip_broken;\n-\tstrbuf_init(&feed_init_state.buf, 0);\n \topt.feed_pipe_ctx = &feed_init_state;\n \topt.feed_pipe = feed_receive_hook_cb;\n \topt.feed_pipe_cb_data_alloc = receive_hook_feed_state_alloc;\ndiff --git a/hook.c b/hook.c\nindex 67cc9a66df..935237fc1d 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -1,16 +1,16 @@\n #include \"git-compat-util.h\"\n #include \"abspath.h\"\n #include \"advice.h\"\n+#include \"config.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n #include \"hook.h\"\n-#include \"path.h\"\n #include \"parse.h\"\n+#include \"path.h\"\n #include \"run-command.h\"\n-#include \"config.h\"\n+#include \"setup.h\"\n #include \"strbuf.h\"\n #include \"strmap.h\"\n-#include \"environment.h\"\n-#include \"setup.h\"\n \n const char *find_hook(struct repository *r, const char *name)\n {\n@@ -57,9 +57,9 @@ static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)\n \tif (!h)\n \t\treturn;\n \n-\tif (h->kind == HOOK_TRADITIONAL)\n+\tif (h->kind == HOOK_TRADITIONAL) {\n \t\tfree((void *)h->u.traditional.path);\n-\telse if (h->kind == HOOK_CONFIGURED) {\n+\t} else if (h->kind == HOOK_CONFIGURED) {\n \t\tfree((void *)h->u.configured.friendly_name);\n \t\tfree((void *)h->u.configured.command);\n \t}\n@@ -91,7 +91,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tif (!hook_path)\n \t\treturn;\n \n-\th = xcalloc(1, sizeof(struct hook));\n+\tCALLOC_ARRAY(h, 1);\n \n \t/*\n \t * If the hook is to run in a specific dir, a relative path can\n@@ -154,7 +154,7 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \t\t\t\tstrmap_get(&data->event_hooks, value);\n \n \t\t\tif (!hooks) {\n-\t\t\t\thooks = xcalloc(1, sizeof(*hooks));\n+\t\t\t\tCALLOC_ARRAY(hooks, 1);\n \t\t\t\tstring_list_init_dup(hooks);\n \t\t\t\tstrmap_put(&data->event_hooks, value, hooks);\n \t\t\t}\n@@ -227,8 +227,9 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t/* Construct the cache from parsed configs. */\n \tstrmap_for_each_entry(&cb_data.event_hooks, &iter, e) {\n \t\tstruct string_list *hook_names = e->value;\n-\t\tstruct string_list *hooks = xcalloc(1, sizeof(*hooks));\n+\t\tstruct string_list *hooks;\n \n+\t\tCALLOC_ARRAY(hooks, 1);\n \t\tstring_list_init_dup(hooks);\n \n \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n@@ -281,7 +282,7 @@ static struct strmap *get_hook_config_cache(struct repository *r)\n \t\t * it just once on the first call.\n \t\t */\n \t\tif (!r->hook_config_cache) {\n-\t\t\tr->hook_config_cache = xcalloc(1, sizeof(*cache));\n+\t\t\tCALLOC_ARRAY(r->hook_config_cache, 1);\n \t\t\tstrmap_init(r->hook_config_cache);\n \t\t\tbuild_hook_config_map(r, r->hook_config_cache);\n \t\t}\n@@ -289,9 +290,9 @@ static struct strmap *get_hook_config_cache(struct repository *r)\n \t} else {\n \t\t/*\n \t\t * Out-of-repo calls (no gitdir) allocate and return a temporary\n-\t\t * map cache which gets free'd immediately by the caller.\n+\t\t * cache which gets freed immediately by the caller.\n \t\t */\n-\t\tcache = xcalloc(1, sizeof(*cache));\n+\t\tCALLOC_ARRAY(cache, 1);\n \t\tstrmap_init(cache);\n \t\tbuild_hook_config_map(r, cache);\n \t}\n@@ -311,7 +312,9 @@ static void list_hooks_add_configured(struct repository *r,\n \tfor (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {\n \t\tconst char *friendly_name = configured_hooks->items[i].string;\n \t\tconst char *command = configured_hooks->items[i].util;\n-\t\tstruct hook *hook = xcalloc(1, sizeof(struct hook));\n+\t\tstruct hook *hook;\n+\n+\t\tCALLOC_ARRAY(hook, 1);\n \n \t\tif (options && options->feed_pipe_cb_data_alloc)\n \t\t\thook->feed_pipe_cb_data =\n@@ -343,7 +346,7 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n \tif (!hookname)\n \t\tBUG(\"null hookname was provided to hook_list()!\");\n \n-\thook_head = xmalloc(sizeof(struct string_list));\n+\tCALLOC_ARRAY(hook_head, 1);\n \tstring_list_init_dup(hook_head);\n \n \t/* Add hooks from the config, e.g. hook.myhook.event = pre-commit */\n@@ -493,8 +496,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,\n \t * Ensure cb_data copy and free functions are either provided together,\n \t * or neither one is provided.\n \t */\n-\tif ((options->feed_pipe_cb_data_alloc && !options->feed_pipe_cb_data_free) ||\n-\t    (!options->feed_pipe_cb_data_alloc && options->feed_pipe_cb_data_free))\n+\tif (!options->feed_pipe_cb_data_alloc != !options->feed_pipe_cb_data_free)\n \t\tBUG(\"feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together\");\n \n \tif (options->invoked_hook)\ndiff --git a/hook.h b/hook.h\nindex e949f5d488..1c447cbb6b 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,9 +1,9 @@\n #ifndef HOOK_H\n #define HOOK_H\n-#include \"strvec.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n #include \"strmap.h\"\n+#include \"strvec.h\"\n \n struct repository;\n \n@@ -46,8 +46,7 @@ struct hook {\n typedef void (*cb_data_free_fn)(void *data);\n typedef void *(*cb_data_alloc_fn)(void *init_ctx);\n \n-struct run_hooks_opt\n-{\n+struct run_hooks_opt {\n \t/* Environment vars to be set for each hook */\n \tstruct strvec env;\n \ndiff --git a/refs.c b/refs.c\nindex 6fb8f9d10c..33c7961802 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2591,7 +2591,8 @@ static int transaction_hook_feed_stdin(int hook_stdin_fd, void *pp_cb, void *pp_\n \n static void *transaction_feed_cb_data_alloc(void *feed_pipe_ctx UNUSED)\n {\n-\tstruct transaction_feed_cb_data *data = xmalloc(sizeof(*data));\n+\tstruct transaction_feed_cb_data *data;\n+\tCALLOC_ARRAY(data, 1);\n \tstrbuf_init(&data->buf, 0);\n \tdata->index = 0;\n \treturn data;\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex b1583e9ef9..952bf97b86 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -34,7 +34,7 @@ test_expect_success 'git hook usage' '\n \n test_expect_success 'git hook list: nonexistent hook' '\n \tcat >stderr.expect <<-\\EOF &&\n-\twarning: No hooks found for event '\\''test-hook'\\''\n+\twarning: no hooks found for event '\\''test-hook'\\''\n \tEOF\n \ttest_expect_code 1 git hook list test-hook 2>stderr.actual &&\n \ttest_cmp stderr.expect stderr.actual\ndiff --git a/transport.c b/transport.c\nindex cb1befba8c..e53936d87b 100644\n--- a/transport.c\n+++ b/transport.c\n@@ -1360,7 +1360,8 @@ static int pre_push_hook_feed_stdin(int hook_stdin_fd, void *pp_cb UNUSED, void\n \n static void *pre_push_hook_data_alloc(void *feed_pipe_ctx)\n {\n-\tstruct feed_pre_push_hook_data *data = xmalloc(sizeof(*data));\n+\tstruct feed_pre_push_hook_data *data;\n+\tCALLOC_ARRAY(data, 1);\n \tstrbuf_init(&data->buf, 0);\n \tdata->refs = (struct ref *)feed_pipe_ctx;\n \treturn data;\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539995","messageId":"20260325195503.1139418-6-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 05/12] hook: detect & emit two more bugs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:56Z","receivedAt":"2026-03-25T19:56:02Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Trigger a bug when an unknown hook type is encountered while\nsetting up hook execution.\n\nAlso issue a bug if a configured hook is enabled without a cmd.\n\nMostly useful for defensive coding.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/hook.c b/hook.c\nindex 4a0db5cfeb..b0226ed716 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -409,7 +409,11 @@ static int pick_next_hook(struct child_process *cp,\n \t} else if (h->kind == HOOK_CONFIGURED) {\n \t\t/* to enable oneliners, let config-specified hooks run in shell. */\n \t\tcp->use_shell = true;\n+\t\tif (!h->u.configured.command)\n+\t\t\tBUG(\"non-disabled HOOK_CONFIGURED hook has no command\");\n \t\tstrvec_push(&cp->args, h->u.configured.command);\n+\t} else {\n+\t\tBUG(\"unknown hook kind\");\n \t}\n \n \tif (!cp->args.nr)\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539996","messageId":"20260325195503.1139418-7-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 06/12] hook: replace hook_list_clear() -> string_list_clear_func()","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:57Z","receivedAt":"2026-03-25T19:56:03Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Replace the custom function with string_list_clear_func() which\nis a more common pattern for clearing a string_list.\n\nTo be able to do this, rework hook_clear() into hook_free(), so\nit can be passed to string_list_clear_func().\n\nA slight complication is the need to keep a copy of the internal\ncb data free() pointer, however I think it's worth it since the\nAPI becomes cleaner, e.g. no more calls with NULL function args\nlike hook_list_clear(hooks, NULL).\n\nIn other words, the callers don't need to keep track of hook\ninternal state to determine when cleanup is necessary or not\n(pass NULL) because each `struct hook` now owns its data_free\ncallback.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c |  2 +-\n hook.c         | 40 ++++++++++++++++++++++------------------\n hook.h         | 20 ++++++++++++++------\n 3 files changed, 37 insertions(+), 25 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex e641614b84..54b737990b 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -78,7 +78,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \t}\n \n cleanup:\n-\thook_list_clear(head, NULL);\n+\tstring_list_clear_func(head, hook_free);\n \tfree(head);\n \treturn ret;\n }\ndiff --git a/hook.c b/hook.c\nindex b0226ed716..021110f216 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -52,8 +52,10 @@ const char *find_hook(struct repository *r, const char *name)\n \treturn path.buf;\n }\n \n-static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n+void hook_free(void *p, const char *str UNUSED)\n {\n+\tstruct hook *h = p;\n+\n \tif (!h)\n \t\treturn;\n \n@@ -64,22 +66,12 @@ static void hook_clear(struct hook *h, hook_data_free_fn cb_data_free)\n \t\tfree((void *)h->u.configured.command);\n \t}\n \n-\tif (cb_data_free)\n-\t\tcb_data_free(h->feed_pipe_cb_data);\n+\tif (h->data_free && h->feed_pipe_cb_data)\n+\t\th->data_free(h->feed_pipe_cb_data);\n \n \tfree(h);\n }\n \n-void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free)\n-{\n-\tstruct string_list_item *item;\n-\n-\tfor_each_string_list_item(item, hooks)\n-\t\thook_clear(item->util, cb_data_free);\n-\n-\tstring_list_clear(hooks, 0);\n-}\n-\n /* Helper to detect and add default \"traditional\" hooks from the hookdir. */\n static void list_hooks_add_default(struct repository *r, const char *hookname,\n \t\t\t\t   struct string_list *hook_list,\n@@ -100,9 +92,15 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tif (options && options->dir)\n \t\thook_path = absolute_path(hook_path);\n \n-\t/* Setup per-hook internal state cb data */\n-\tif (options && options->feed_pipe_cb_data_alloc)\n+\t/*\n+\t * Setup per-hook internal state callback data.\n+\t * When provided, the alloc/free callbacks are always provided\n+\t * together, so use them to alloc/free the internal hook state.\n+\t */\n+\tif (options && options->feed_pipe_cb_data_alloc) {\n \t\th->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);\n+\t\th->data_free = options->feed_pipe_cb_data_free;\n+\t}\n \n \th->kind = HOOK_TRADITIONAL;\n \th->u.traditional.path = xstrdup(hook_path);\n@@ -316,10 +314,16 @@ static void list_hooks_add_configured(struct repository *r,\n \n \t\tCALLOC_ARRAY(hook, 1);\n \n-\t\tif (options && options->feed_pipe_cb_data_alloc)\n+\t\t/*\n+\t\t * When provided, the alloc/free callbacks are always provided\n+\t\t * together, so use them to alloc/free the internal hook state.\n+\t\t */\n+\t\tif (options && options->feed_pipe_cb_data_alloc) {\n \t\t\thook->feed_pipe_cb_data =\n \t\t\t\toptions->feed_pipe_cb_data_alloc(\n \t\t\t\t\toptions->feed_pipe_ctx);\n+\t\t\thook->data_free = options->feed_pipe_cb_data_free;\n+\t\t}\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n@@ -362,7 +366,7 @@ int hook_exists(struct repository *r, const char *name)\n {\n \tstruct string_list *hooks = list_hooks(r, name, NULL);\n \tint exists = hooks->nr > 0;\n-\thook_list_clear(hooks, NULL);\n+\tstring_list_clear_func(hooks, hook_free);\n \tfree(hooks);\n \treturn exists;\n }\n@@ -516,7 +520,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,\n \trun_processes_parallel(&opts);\n \tret = cb_data.rc;\n cleanup:\n-\thook_list_clear(cb_data.hook_command_list, options->feed_pipe_cb_data_free);\n+\tstring_list_clear_func(cb_data.hook_command_list, hook_free);\n \tfree(cb_data.hook_command_list);\n \trun_hooks_opt_clear(options);\n \treturn ret;\ndiff --git a/hook.h b/hook.h\nindex 965794a5b8..a56ac20ccf 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -7,6 +7,9 @@\n \n struct repository;\n \n+typedef void (*hook_data_free_fn)(void *data);\n+typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n+\n /**\n  * Represents a hook command to be run.\n  * Hooks can be:\n@@ -41,10 +44,15 @@ struct hook {\n \t * Only useful when using `run_hooks_opt.feed_pipe`, otherwise ignore it.\n \t */\n \tvoid *feed_pipe_cb_data;\n-};\n \n-typedef void (*hook_data_free_fn)(void *data);\n-typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n+\t/**\n+\t * Callback to free `feed_pipe_cb_data`.\n+\t *\n+\t * It is called automatically and points to the `feed_pipe_cb_data_free`\n+\t * provided via the `run_hook_opt` parameter.\n+\t */\n+\thook_data_free_fn data_free;\n+};\n \n struct run_hooks_opt {\n \t/* Environment vars to be set for each hook */\n@@ -185,10 +193,10 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n \t\t\t       struct run_hooks_opt *options);\n \n /**\n- * Frees the memory allocated for the hook list, including the `struct hook`\n- * items and their internal state.\n+ * Frees a struct hook stored as the util pointer of a string_list_item.\n+ * Suitable for use as a string_list_clear_func_t callback.\n  */\n-void hook_list_clear(struct string_list *hooks, hook_data_free_fn cb_data_free);\n+void hook_free(void *p, const char *str);\n \n /**\n  * Frees the hook configuration cache stored in `struct repository`.\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539997","messageId":"20260325195503.1139418-8-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 07/12] hook: make consistent use of friendly-name in docs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:58Z","receivedAt":"2026-03-25T19:56:06Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Both `name` and `friendly-name` is being used. Standardize on\n`friendly-name` for consistency since name is rather generic,\neven when used in the hooks namespace.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/hook.adoc | 30 +++++++++++++++---------------\n Documentation/git-hook.adoc    |  6 +++---\n hook.c                         |  2 +-\n hook.h                         |  2 +-\n 4 files changed, 20 insertions(+), 20 deletions(-)\n\ndiff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc\nindex 64e845a260..9e78f26439 100644\n--- a/Documentation/config/hook.adoc\n+++ b/Documentation/config/hook.adoc\n@@ -1,23 +1,23 @@\n-hook.<name>.command::\n-\tThe command to execute for `hook.<name>`. `<name>` is a unique\n-\t\"friendly\" name that identifies this hook. (The hook events that\n-\ttrigger the command are configured with `hook.<name>.event`.) The\n-\tvalue can be an executable path or a shell oneliner. If more than\n-\tone value is specified for the same `<name>`, only the last value\n-\tparsed is used. See linkgit:git-hook[1].\n+hook.<friendly-name>.command::\n+\tThe command to execute for `hook.<friendly-name>`. `<friendly-name>`\n+\tis a unique name that identifies this hook. The hook events that\n+\ttrigger the command are configured with `hook.<friendly-name>.event`.\n+\tThe value can be an executable path or a shell oneliner. If more than\n+\tone value is specified for the same `<friendly-name>`, only the last\n+\tvalue parsed is used. See linkgit:git-hook[1].\n \n-hook.<name>.event::\n-\tThe hook events that trigger `hook.<name>`. The value is the name\n-\tof a hook event, like \"pre-commit\" or \"update\". (See\n+hook.<friendly-name>.event::\n+\tThe hook events that trigger `hook.<friendly-name>`. The value is the\n+\tname of a hook event, like \"pre-commit\" or \"update\". (See\n \tlinkgit:githooks[5] for a complete list of hook events.) On the\n-\tspecified event, the associated `hook.<name>.command` is executed.\n-\tThis is a multi-valued key. To run `hook.<name>` on multiple\n+\tspecified event, the associated `hook.<friendly-name>.command` is executed.\n+\tThis is a multi-valued key. To run `hook.<friendly-name>` on multiple\n \tevents, specify the key more than once. An empty value resets\n \tthe list of events, clearing any previously defined events for\n-\t`hook.<name>`. See linkgit:git-hook[1].\n+\t`hook.<friendly-name>`. See linkgit:git-hook[1].\n \n-hook.<name>.enabled::\n-\tWhether the hook `hook.<name>` is enabled. Defaults to `true`.\n+hook.<friendly-name>.enabled::\n+\tWhether the hook `hook.<friendly-name>` is enabled. Defaults to `true`.\n \tSet to `false` to disable the hook without removing its\n \tconfiguration. This is particularly useful when a hook is defined\n \tin a system or global config file and needs to be disabled for a\ndiff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\nindex 12d2701b52..966388660a 100644\n--- a/Documentation/git-hook.adoc\n+++ b/Documentation/git-hook.adoc\n@@ -44,7 +44,7 @@ event`), and then `~/bin/spellchecker` will have a chance to check your commit\n message (during the `commit-msg` hook event).\n \n Commands are run in the order Git encounters their associated\n-`hook.<name>.event` configs during the configuration parse (see\n+`hook.<friendly-name>.event` configs during the configuration parse (see\n linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be\n added, only one `hook.linter.command` event is valid - Git uses \"last-one-wins\"\n to determine which command to run.\n@@ -76,10 +76,10 @@ first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It\n would evaluate the output of each when deciding whether to proceed with the\n commit.\n \n-For a full list of hook events which you can set your `hook.<name>.event` to,\n+For a full list of hook events which you can set your `hook.<friendly-name>.event` to,\n and how hooks are invoked during those events, see linkgit:githooks[5].\n \n-Git will ignore any `hook.<name>.event` that specifies an event it doesn't\n+Git will ignore any `hook.<friendly-name>.event` that specifies an event it doesn't\n recognize. This is intended so that tools which wrap Git can use the hook\n infrastructure to run their own hooks; see \"WRAPPERS\" for more guidance.\n \ndiff --git a/hook.c b/hook.c\nindex 021110f216..dc0c3de667 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -112,7 +112,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\n  * event_hooks: event-name to list of friendly-names map.\n- * disabled_hooks: set of friendly-names with hook.name.enabled = false.\n+ * disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.\n  */\n struct hook_all_config_cb {\n \tstruct strmap commands;\ndiff --git a/hook.h b/hook.h\nindex a56ac20ccf..d2cf59e649 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -14,7 +14,7 @@ typedef void *(*hook_data_alloc_fn)(void *init_ctx);\n  * Represents a hook command to be run.\n  * Hooks can be:\n  * 1. \"traditional\" (found in the hooks directory)\n- * 2. \"configured\" (defined in Git's configuration via hook.<name>.event).\n+ * 2. \"configured\" (defined in Git's configuration via hook.<friendly-name>.event).\n  * The 'kind' field determines which part of the union 'u' is valid.\n  */\n struct hook {\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539998","messageId":"20260325195503.1139418-9-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 08/12] t1800: add test to verify hook execution ordering","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:54:59Z","receivedAt":"2026-03-25T19:56:07Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"There is a documented expectation that configured hooks are\nrun before the hook from the hookdir. Add a test for it.\n\nWhile at it, I noticed that `git hook list -h` runs twice\nin the `git hook usage` test, so remove one invocation.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/t1800-hook.sh | 29 ++++++++++++++++++++++++++++-\n 1 file changed, 28 insertions(+), 1 deletion(-)\n\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 952bf97b86..7eee84fc39 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -25,7 +25,6 @@ test_expect_success 'git hook usage' '\n \ttest_expect_code 129 git hook &&\n \ttest_expect_code 129 git hook run &&\n \ttest_expect_code 129 git hook run -h &&\n-\ttest_expect_code 129 git hook list -h &&\n \ttest_expect_code 129 git hook run --unknown 2>err &&\n \ttest_expect_code 129 git hook list &&\n \ttest_expect_code 129 git hook list -h &&\n@@ -381,6 +380,34 @@ test_expect_success 'globally disabled hook can be re-enabled locally' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'configured hooks run before hookdir hook' '\n+\tsetup_hookdir &&\n+\ttest_config hook.first.event \"pre-commit\" &&\n+\ttest_config hook.first.command \"echo first\" &&\n+\ttest_config hook.second.event \"pre-commit\" &&\n+\ttest_config hook.second.command \"echo second\" &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tfirst\n+\tsecond\n+\thook from hookdir\n+\tEOF\n+\n+\tgit hook list pre-commit >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# \"Legacy Hook\" is the output of the hookdir pre-commit script\n+\t# written by setup_hookdir() above.\n+\tcat >expected <<-\\EOF &&\n+\tfirst\n+\tsecond\n+\t\"Legacy Hook\"\n+\tEOF\n+\n+\tgit hook run pre-commit 2>actual &&\n+\ttest_cmp expected actual\n+'\n+\n test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_when_finished \"rm -rf bad-hooks\" &&\n \tmkdir bad-hooks &&\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"539999","messageId":"20260325195503.1139418-10-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 09/12] hook: introduce hook_config_cache_entry for per-hook data","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:55:00Z","receivedAt":"2026-03-25T19:56:10Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Replace the bare `char *command` util pointer stored in each string_list\nitem with a heap-allocated `struct hook_config_cache_entry` that carries\nthat command string.\n\nThis is just a refactoring with no behavior changes, to give the cache\nentry room to grow, so it can carry the additional hook metadata we'll\nbe adding in the following commits.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n hook.c | 28 ++++++++++++++++++++++------\n 1 file changed, 22 insertions(+), 6 deletions(-)\n\ndiff --git a/hook.c b/hook.c\nindex dc0c3de667..54f99f4989 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -108,6 +108,15 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \tstring_list_append(hook_list, hook_path)->util = h;\n }\n \n+/*\n+ * Cache entry stored as the .util pointer of string_list items inside the\n+ * hook config cache. For now carries only the command for the hook. Next\n+ * commits will add more data.\n+ */\n+struct hook_config_cache_entry {\n+\tchar *command;\n+};\n+\n /*\n  * Callback struct to collect all hook.* keys in a single config pass.\n  * commands: friendly-name to command map.\n@@ -202,7 +211,12 @@ void hook_cache_clear(struct strmap *cache)\n \n \tstrmap_for_each_entry(cache, &iter, e) {\n \t\tstruct string_list *hooks = e->value;\n-\t\tstring_list_clear(hooks, 1); /* free util (command) pointers */\n+\t\tfor (size_t i = 0; i < hooks->nr; i++) {\n+\t\t\tstruct hook_config_cache_entry *entry = hooks->items[i].util;\n+\t\t\tfree(entry->command);\n+\t\t\tfree(entry);\n+\t\t}\n+\t\tstring_list_clear(hooks, 0);\n \t\tfree(hooks);\n \t}\n \tstrmap_clear(cache, 0);\n@@ -232,6 +246,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \n \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n \t\t\tconst char *hname = hook_names->items[i].string;\n+\t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n \t\t\t/* filter out disabled hooks */\n@@ -245,9 +260,10 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t\t\t\t      \"'hook.%s.event' must be removed;\"\n \t\t\t\t      \" aborting.\"), hname, hname);\n \n-\t\t\t/* util stores the command; owned by the cache. */\n-\t\t\tstring_list_append(hooks, hname)->util =\n-\t\t\t\txstrdup(command);\n+\t\t\t/* util stores a cache entry; owned by the cache. */\n+\t\t\tCALLOC_ARRAY(entry, 1);\n+\t\t\tentry->command = xstrdup(command);\n+\t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n \t\tstrmap_put(cache, e->key, hooks);\n@@ -309,7 +325,7 @@ static void list_hooks_add_configured(struct repository *r,\n \t/* Iterate through configured hooks and initialize internal states */\n \tfor (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {\n \t\tconst char *friendly_name = configured_hooks->items[i].string;\n-\t\tconst char *command = configured_hooks->items[i].util;\n+\t\tstruct hook_config_cache_entry *entry = configured_hooks->items[i].util;\n \t\tstruct hook *hook;\n \n \t\tCALLOC_ARRAY(hook, 1);\n@@ -327,7 +343,7 @@ static void list_hooks_add_configured(struct repository *r,\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n-\t\thook->u.configured.command = xstrdup(command);\n+\t\thook->u.configured.command = xstrdup(entry->command);\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"540000","messageId":"20260325195503.1139418-11-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 10/12] hook: show config scope in git hook list","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:55:01Z","receivedAt":"2026-03-25T19:56:11Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Users running \"git hook list\" can see which hooks are configured but\nhave no way to tell at which config scope (local, global, system...)\neach hook was defined.\n\nStore the scope from ctx->kvi->scope in the single-pass config callback,\nthen carry it through the cache to the hook structs, so we can expose it\nto users via the \"git hook list --show-scope\" flag, which mirrors the\nexisting git config --show-scope convention.\n\nWithout the flag the output is unchanged.\n\nThe scope is printed as a tab-separated prefix (like \"git config --show-scope\"),\nmaking it unambiguously machine-parseable even when the friendly name\ncontains spaces.\n\nExample usage:\n$ git hook list --show-scope pre-commit\nglobal\tlinter\nlocal\tno-leaks\nhook from hookdir\n\nTraditional hooks from the hookdir are unaffected by --show-scope since\nthe config scope concept does not apply to them.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/git-hook.adoc | 10 ++++++++--\n builtin/hook.c              | 14 ++++++++++++--\n hook.c                      | 24 ++++++++++++++++++++----\n hook.h                      |  2 ++\n t/t1800-hook.sh             | 21 +++++++++++++++++++++\n 5 files changed, 63 insertions(+), 8 deletions(-)\n\ndiff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\nindex 966388660a..e7d399ae57 100644\n--- a/Documentation/git-hook.adoc\n+++ b/Documentation/git-hook.adoc\n@@ -9,7 +9,7 @@ SYNOPSIS\n --------\n [verse]\n 'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\n-'git hook' list [-z] <hook-name>\n+'git hook' list [-z] [--show-scope] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -113,7 +113,7 @@ Any positional arguments to the hook should be passed after a\n mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See\n linkgit:githooks[5] for arguments hooks might expect (if any).\n \n-list [-z]::\n+list [-z] [--show-scope]::\n \tPrint a list of hooks which will be run on `<hook-name>` event. If no\n \thooks are configured for that event, print a warning and return 1.\n \tUse `-z` to terminate output lines with NUL instead of newlines.\n@@ -134,6 +134,12 @@ OPTIONS\n -z::\n \tTerminate \"list\" output lines with NUL instead of newlines.\n \n+--show-scope::\n+\tFor \"list\"; prefix each configured hook's friendly name with a\n+\ttab-separated config scope (e.g. `local`, `global`, `system`),\n+\tmirroring the output style of `git config --show-scope`. Traditional\n+\thooks from the hookdir are unaffected.\n+\n WRAPPERS\n --------\n \ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 54b737990b..4cc65a0dc5 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -9,7 +9,7 @@\n #define BUILTIN_HOOK_RUN_USAGE \\\n \tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n #define BUILTIN_HOOK_LIST_USAGE \\\n-\tN_(\"git hook list [-z] <hook-name>\")\n+\tN_(\"git hook list [-z] [--show-scope] <hook-name>\")\n \n static const char * const builtin_hook_usage[] = {\n \tBUILTIN_HOOK_RUN_USAGE,\n@@ -33,11 +33,14 @@ static int list(int argc, const char **argv, const char *prefix,\n \tstruct string_list_item *item;\n \tconst char *hookname = NULL;\n \tint line_terminator = '\\n';\n+\tint show_scope = 0;\n \tint ret = 0;\n \n \tstruct option list_options[] = {\n \t\tOPT_SET_INT('z', NULL, &line_terminator,\n \t\t\t    N_(\"use NUL as line terminator\"), '\\0'),\n+\t\tOPT_BOOL(0, \"show-scope\", &show_scope,\n+\t\t\t N_(\"show the config scope that defined each hook\")),\n \t\tOPT_END(),\n \t};\n \n@@ -70,7 +73,14 @@ static int list(int argc, const char **argv, const char *prefix,\n \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n \t\t\tbreak;\n \t\tcase HOOK_CONFIGURED:\n-\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name, line_terminator);\n+\t\t\tif (show_scope)\n+\t\t\t\tprintf(\"%s\\t%s%c\",\n+\t\t\t\t       config_scope_name(h->u.configured.scope),\n+\t\t\t\t       h->u.configured.friendly_name,\n+\t\t\t\t       line_terminator);\n+\t\t\telse\n+\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n+\t\t\t\t       line_terminator);\n \t\t\tbreak;\n \t\tdefault:\n \t\t\tBUG(\"unknown hook kind\");\ndiff --git a/hook.c b/hook.c\nindex 54f99f4989..74f5a1df35 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -110,11 +110,11 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n \n /*\n  * Cache entry stored as the .util pointer of string_list items inside the\n- * hook config cache. For now carries only the command for the hook. Next\n- * commits will add more data.\n+ * hook config cache.\n  */\n struct hook_config_cache_entry {\n \tchar *command;\n+\tenum config_scope scope;\n };\n \n /*\n@@ -131,7 +131,7 @@ struct hook_all_config_cb {\n \n /* repo_config() callback that collects all hook.* configuration in one pass. */\n static int hook_config_lookup_all(const char *key, const char *value,\n-\t\t\t\t  const struct config_context *ctx UNUSED,\n+\t\t\t\t  const struct config_context *ctx,\n \t\t\t\t  void *cb_data)\n {\n \tstruct hook_all_config_cb *data = cb_data;\n@@ -168,7 +168,19 @@ static int hook_config_lookup_all(const char *key, const char *value,\n \n \t\t\t/* Re-insert if necessary to preserve last-seen order. */\n \t\t\tunsorted_string_list_remove(hooks, hook_name, 0);\n-\t\t\tstring_list_append(hooks, hook_name);\n+\n+\t\t\tif (!ctx->kvi)\n+\t\t\t\tBUG(\"hook config callback called without key-value info\");\n+\n+\t\t\t/*\n+\t\t\t * Stash the config scope in the util pointer for\n+\t\t\t * later retrieval in build_hook_config_map(). This\n+\t\t\t * intermediate struct is transient and never leaves\n+\t\t\t * that function, so we pack the enum value into the\n+\t\t\t * pointer rather than heap-allocating a wrapper.\n+\t\t\t */\n+\t\t\tstring_list_append(hooks, hook_name)->util =\n+\t\t\t\t(void *)(uintptr_t)ctx->kvi->scope;\n \t\t}\n \t} else if (!strcmp(subkey, \"command\")) {\n \t\t/* Store command overwriting the old value */\n@@ -246,6 +258,8 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \n \t\tfor (size_t i = 0; i < hook_names->nr; i++) {\n \t\t\tconst char *hname = hook_names->items[i].string;\n+\t\t\tenum config_scope scope =\n+\t\t\t\t(enum config_scope)(uintptr_t)hook_names->items[i].util;\n \t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n@@ -263,6 +277,7 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t\t\t/* util stores a cache entry; owned by the cache. */\n \t\t\tCALLOC_ARRAY(entry, 1);\n \t\t\tentry->command = xstrdup(command);\n+\t\t\tentry->scope = scope;\n \t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n@@ -344,6 +359,7 @@ static void list_hooks_add_configured(struct repository *r,\n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n \t\thook->u.configured.command = xstrdup(entry->command);\n+\t\thook->u.configured.scope = entry->scope;\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\ndiff --git a/hook.h b/hook.h\nindex d2cf59e649..a0432e8307 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -1,5 +1,6 @@\n #ifndef HOOK_H\n #define HOOK_H\n+#include \"config.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n #include \"strmap.h\"\n@@ -29,6 +30,7 @@ struct hook {\n \t\tstruct {\n \t\t\tconst char *friendly_name;\n \t\t\tconst char *command;\n+\t\t\tenum config_scope scope;\n \t\t} configured;\n \t} u;\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 7eee84fc39..6fc6603da8 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -408,6 +408,27 @@ test_expect_success 'configured hooks run before hookdir hook' '\n \ttest_cmp expected actual\n '\n \n+test_expect_success 'git hook list --show-scope shows config scope' '\n+\tsetup_hookdir &&\n+\ttest_config_global hook.global-hook.command \"echo global\" &&\n+\ttest_config_global hook.global-hook.event pre-commit --add &&\n+\ttest_config hook.local-hook.command \"echo local\" &&\n+\ttest_config hook.local-hook.event pre-commit --add &&\n+\n+\tcat >expected <<-\\EOF &&\n+\tglobal\tglobal-hook\n+\tlocal\tlocal-hook\n+\thook from hookdir\n+\tEOF\n+\tgit hook list --show-scope pre-commit >actual &&\n+\ttest_cmp expected actual &&\n+\n+\t# without --show-scope the scope must not appear\n+\tgit hook list pre-commit >actual &&\n+\ttest_grep ! \"^global\t\" actual &&\n+\ttest_grep ! \"^local\t\" actual\n+'\n+\n test_expect_success 'git hook run a hook with a bad shebang' '\n \ttest_when_finished \"rm -rf bad-hooks\" &&\n \tmkdir bad-hooks &&\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"540001","messageId":"20260325195503.1139418-12-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 11/12] hook: show disabled hooks in \"git hook list\"","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:55:02Z","receivedAt":"2026-03-25T19:56:15Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Disabled hooks were filtered out of the cache entirely, making them\ninvisible to \"git hook list\". Keep them in the cache with a new\n\"disabled\" flag which is propagated to the respective struct hook.\n\n\"git hook list\" now shows disabled hooks as tab-separated columns,\nwith the status as a prefix before the name (like scope with\n--show-scope). With --show-scope it looks like:\n\n$ git hook list --show-scope pre-commit\nglobal\tlinter\nlocal\tdisabled\tno-leaks\nhook from hookdir\n\nA disabled hook without a command issues a warning instead of the\nfatal \"hook.X.command must be configured\" error. We could also throw\nan error, however it seemd a bit excessive to me in this case.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/hook.c  | 20 ++++++++++--------\n hook.c          | 54 +++++++++++++++++++++++++++++++++----------------\n hook.h          |  1 +\n t/t1800-hook.sh | 33 +++++++++++++++++++++++++++---\n 4 files changed, 80 insertions(+), 28 deletions(-)\n\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex 4cc65a0dc5..f671e7f91a 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -72,16 +72,20 @@ static int list(int argc, const char **argv, const char *prefix,\n \t\tcase HOOK_TRADITIONAL:\n \t\t\tprintf(\"%s%c\", _(\"hook from hookdir\"), line_terminator);\n \t\t\tbreak;\n-\t\tcase HOOK_CONFIGURED:\n-\t\t\tif (show_scope)\n-\t\t\t\tprintf(\"%s\\t%s%c\",\n-\t\t\t\t       config_scope_name(h->u.configured.scope),\n-\t\t\t\t       h->u.configured.friendly_name,\n-\t\t\t\t       line_terminator);\n+\t\tcase HOOK_CONFIGURED: {\n+\t\t\tconst char *name = h->u.configured.friendly_name;\n+\t\t\tconst char *scope = show_scope ?\n+\t\t\t\tconfig_scope_name(h->u.configured.scope) : NULL;\n+\t\t\tif (scope)\n+\t\t\t\tprintf(\"%s\\t%s%s%c\", scope,\n+\t\t\t\t       h->u.configured.disabled ? \"disabled\\t\" : \"\",\n+\t\t\t\t       name, line_terminator);\n \t\t\telse\n-\t\t\t\tprintf(\"%s%c\", h->u.configured.friendly_name,\n-\t\t\t\t       line_terminator);\n+\t\t\t\tprintf(\"%s%s%c\",\n+\t\t\t\t       h->u.configured.disabled ? \"disabled\\t\" : \"\",\n+\t\t\t\t       name, line_terminator);\n \t\t\tbreak;\n+\t\t}\n \t\tdefault:\n \t\t\tBUG(\"unknown hook kind\");\n \t\t}\ndiff --git a/hook.c b/hook.c\nindex 74f5a1df35..cc23276d27 100644\n--- a/hook.c\n+++ b/hook.c\n@@ -115,6 +115,7 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,\n struct hook_config_cache_entry {\n \tchar *command;\n \tenum config_scope scope;\n+\tbool disabled;\n };\n \n /*\n@@ -213,8 +214,10 @@ static int hook_config_lookup_all(const char *key, const char *value,\n  * every item's string is the hook's friendly-name and its util pointer is\n  * the corresponding command string. Both strings are owned by the map.\n  *\n- * Disabled hooks and hooks missing a command are already filtered out at\n- * parse time, so callers can iterate the list directly.\n+ * Disabled hooks are kept in the cache with entry->disabled set, so that\n+ * \"git hook list\" can display them. A non-disabled hook missing a command\n+ * is fatal; a disabled hook missing a command emits a warning and is kept\n+ * in the cache with entry->command = NULL.\n  */\n void hook_cache_clear(struct strmap *cache)\n {\n@@ -263,21 +266,26 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)\n \t\t\tstruct hook_config_cache_entry *entry;\n \t\t\tchar *command;\n \n-\t\t\t/* filter out disabled hooks */\n-\t\t\tif (unsorted_string_list_lookup(&cb_data.disabled_hooks,\n-\t\t\t\t\t\t\thname))\n-\t\t\t\tcontinue;\n+\t\t\tbool is_disabled =\n+\t\t\t\t!!unsorted_string_list_lookup(\n+\t\t\t\t\t&cb_data.disabled_hooks, hname);\n \n \t\t\tcommand = strmap_get(&cb_data.commands, hname);\n-\t\t\tif (!command)\n-\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n-\t\t\t\t      \"'hook.%s.event' must be removed;\"\n-\t\t\t\t      \" aborting.\"), hname, hname);\n+\t\t\tif (!command) {\n+\t\t\t\tif (is_disabled)\n+\t\t\t\t\twarning(_(\"disabled hook '%s' has no \"\n+\t\t\t\t\t\t  \"command configured\"), hname);\n+\t\t\t\telse\n+\t\t\t\t\tdie(_(\"'hook.%s.command' must be configured or \"\n+\t\t\t\t\t      \"'hook.%s.event' must be removed;\"\n+\t\t\t\t\t      \" aborting.\"), hname, hname);\n+\t\t\t}\n \n \t\t\t/* util stores a cache entry; owned by the cache. */\n \t\t\tCALLOC_ARRAY(entry, 1);\n-\t\t\tentry->command = xstrdup(command);\n+\t\t\tentry->command = xstrdup_or_null(command);\n \t\t\tentry->scope = scope;\n+\t\t\tentry->disabled = is_disabled;\n \t\t\tstring_list_append(hooks, hname)->util = entry;\n \t\t}\n \n@@ -358,8 +366,10 @@ static void list_hooks_add_configured(struct repository *r,\n \n \t\thook->kind = HOOK_CONFIGURED;\n \t\thook->u.configured.friendly_name = xstrdup(friendly_name);\n-\t\thook->u.configured.command = xstrdup(entry->command);\n+\t\thook->u.configured.command =\n+\t\t\tentry->command ? xstrdup(entry->command) : NULL;\n \t\thook->u.configured.scope = entry->scope;\n+\t\thook->u.configured.disabled = entry->disabled;\n \n \t\tstring_list_append(list, friendly_name)->util = hook;\n \t}\n@@ -397,7 +407,16 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,\n int hook_exists(struct repository *r, const char *name)\n {\n \tstruct string_list *hooks = list_hooks(r, name, NULL);\n-\tint exists = hooks->nr > 0;\n+\tint exists = 0;\n+\n+\tfor (size_t i = 0; i < hooks->nr; i++) {\n+\t\tstruct hook *h = hooks->items[i].util;\n+\t\tif (h->kind == HOOK_TRADITIONAL ||\n+\t\t    !h->u.configured.disabled) {\n+\t\t\texists = 1;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n \tstring_list_clear_func(hooks, hook_free);\n \tfree(hooks);\n \treturn exists;\n@@ -412,10 +431,11 @@ static int pick_next_hook(struct child_process *cp,\n \tstruct string_list *hook_list = hook_cb->hook_command_list;\n \tstruct hook *h;\n \n-\tif (hook_cb->hook_to_run_index >= hook_list->nr)\n-\t\treturn 0;\n-\n-\th = hook_list->items[hook_cb->hook_to_run_index++].util;\n+\tdo {\n+\t\tif (hook_cb->hook_to_run_index >= hook_list->nr)\n+\t\t\treturn 0;\n+\t\th = hook_list->items[hook_cb->hook_to_run_index++].util;\n+\t} while (h->kind == HOOK_CONFIGURED && h->u.configured.disabled);\n \n \tcp->no_stdin = 1;\n \tstrvec_pushv(&cp->env, hook_cb->options->env.v);\ndiff --git a/hook.h b/hook.h\nindex a0432e8307..5c5628dd1f 100644\n--- a/hook.h\n+++ b/hook.h\n@@ -31,6 +31,7 @@ struct hook {\n \t\t\tconst char *friendly_name;\n \t\t\tconst char *command;\n \t\t\tenum config_scope scope;\n+\t\t\tbool disabled;\n \t\t} configured;\n \t} u;\n \ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 6fc6603da8..8c5237449d 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -357,7 +357,15 @@ test_expect_success 'disabled hook is not run' '\n \ttest_must_be_empty actual\n '\n \n-test_expect_success 'disabled hook does not appear in git hook list' '\n+test_expect_success 'disabled hook with no command warns' '\n+\ttest_config hook.nocommand.event \"pre-commit\" &&\n+\ttest_config hook.nocommand.enabled false &&\n+\n+\tgit hook list pre-commit 2>actual &&\n+\ttest_grep \"disabled hook.*nocommand.*no command configured\" actual\n+'\n+\n+test_expect_success 'disabled hook appears as disabled in git hook list' '\n \ttest_config hook.active.event \"pre-commit\" &&\n \ttest_config hook.active.command \"echo active\" &&\n \ttest_config hook.inactive.event \"pre-commit\" &&\n@@ -365,8 +373,27 @@ test_expect_success 'disabled hook does not appear in git hook list' '\n \ttest_config hook.inactive.enabled false &&\n \n \tgit hook list pre-commit >actual &&\n-\ttest_grep \"active\" actual &&\n-\ttest_grep ! \"inactive\" actual\n+\ttest_grep \"^active$\" actual &&\n+\ttest_grep \"^disabled\tinactive$\" actual\n+'\n+\n+test_expect_success 'disabled hook shows scope with --show-scope' '\n+\ttest_config hook.myhook.event \"pre-commit\" &&\n+\ttest_config hook.myhook.command \"echo hi\" &&\n+\ttest_config hook.myhook.enabled false &&\n+\n+\tgit hook list --show-scope pre-commit >actual &&\n+\ttest_grep \"^local\tdisabled\tmyhook$\" actual\n+'\n+\n+test_expect_success 'disabled configured hook is not reported as existing by hook_exists' '\n+\ttest_when_finished \"rm -f git-bugreport-hook-exists-test.txt\" &&\n+\ttest_config hook.linter.event \"pre-commit\" &&\n+\ttest_config hook.linter.command \"echo lint\" &&\n+\ttest_config hook.linter.enabled false &&\n+\n+\tgit bugreport -s hook-exists-test &&\n+\ttest_grep ! \"pre-commit\" git-bugreport-hook-exists-test.txt\n '\n \n test_expect_success 'globally disabled hook can be re-enabled locally' '\n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"540002","messageId":"20260325195503.1139418-13-adrian.ratiu@collabora.com","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 12/12] hook: reject unknown hook names in git-hook(1)","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-25T19:55:03Z","receivedAt":"2026-03-25T19:56:16Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"Teach \"git hook run\" and \"git hook list\" to reject hook event names\nthat are not recognized by Git. This helps catch typos such as\n\"prereceive\" when \"pre-receive\" was intended, since in 99% of the\ncases users want known (already-existing) hook names.\n\nThe list of known hooks is derived from the generated hook-list.h\n(built from Documentation/githooks.adoc). This is why the Makefile\nis updated, so builtin/hook.c depends on hook-list.h. In meson the\nheader is already a dependency for all builtins, no change required.\n\nThe \"--allow-unknown-hook-name\" flag can be used to bypass this check.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/git-hook.adoc | 13 ++++--\n Makefile                    |  1 +\n builtin/hook.c              | 35 +++++++++++++++-\n t/t1800-hook.sh             | 82 +++++++++++++++++++++++++------------\n 4 files changed, 100 insertions(+), 31 deletions(-)\n\ndiff --git a/Documentation/git-hook.adoc b/Documentation/git-hook.adoc\nindex e7d399ae57..318c637bd8 100644\n--- a/Documentation/git-hook.adoc\n+++ b/Documentation/git-hook.adoc\n@@ -8,8 +8,8 @@ git-hook - Run git hooks\n SYNOPSIS\n --------\n [verse]\n-'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\n-'git hook' list [-z] [--show-scope] <hook-name>\n+'git hook' run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\n+'git hook' list [--allow-unknown-hook-name] [-z] [--show-scope] <hook-name>\n \n DESCRIPTION\n -----------\n@@ -121,6 +121,13 @@ list [-z] [--show-scope]::\n OPTIONS\n -------\n \n+--allow-unknown-hook-name::\n+\tBy default `git hook run` and `git hook list` will bail out when\n+\t`<hook-name>` is not a hook event known to Git (see linkgit:githooks[5]\n+\tfor the list of known hooks). This is meant to help catch typos\n+\tsuch as `prereceive` when `pre-receive` was intended. Pass this\n+\tflag to allow unknown hook names.\n+\n --to-stdin::\n \tFor \"run\"; specify a file which will be streamed into the\n \thook's stdin. The hook will receive the entire file from\n@@ -159,7 +166,7 @@ Then, in your 'mywrapper' tool, you can invoke any users' configured hooks by\n running:\n \n ----\n-git hook run mywrapper-start-tests \\\n+git hook run --allow-unknown-hook-name mywrapper-start-tests \\\n   # providing something to stdin\n   --stdin some-tempfile-123 \\\n   # execute hooks in serial\ndiff --git a/Makefile b/Makefile\nindex bf2228de9d..6d64431219 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2673,6 +2673,7 @@ git$X: git.o GIT-LDFLAGS $(BUILTIN_OBJS) $(GITLIBS)\n \n help.sp help.s help.o: command-list.h\n builtin/bugreport.sp builtin/bugreport.s builtin/bugreport.o: hook-list.h\n+builtin/hook.sp builtin/hook.s builtin/hook.o: hook-list.h\n \n builtin/help.sp builtin/help.s builtin/help.o: config-list.h GIT-PREFIX\n builtin/help.sp builtin/help.s builtin/help.o: EXTRA_CPPFLAGS = \\\ndiff --git a/builtin/hook.c b/builtin/hook.c\nindex f671e7f91a..c0585587e5 100644\n--- a/builtin/hook.c\n+++ b/builtin/hook.c\n@@ -4,12 +4,22 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hook.h\"\n+#include \"hook-list.h\"\n #include \"parse-options.h\"\n \n #define BUILTIN_HOOK_RUN_USAGE \\\n-\tN_(\"git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n+\tN_(\"git hook run [--allow-unknown-hook-name] [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]\")\n #define BUILTIN_HOOK_LIST_USAGE \\\n-\tN_(\"git hook list [-z] [--show-scope] <hook-name>\")\n+\tN_(\"git hook list [--allow-unknown-hook-name] [-z] [--show-scope] <hook-name>\")\n+\n+static int is_known_hook(const char *name)\n+{\n+\tconst char **p;\n+\tfor (p = hook_name_list; *p; p++)\n+\t\tif (!strcmp(*p, name))\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n \n static const char * const builtin_hook_usage[] = {\n \tBUILTIN_HOOK_RUN_USAGE,\n@@ -34,6 +44,7 @@ static int list(int argc, const char **argv, const char *prefix,\n \tconst char *hookname = NULL;\n \tint line_terminator = '\\n';\n \tint show_scope = 0;\n+\tint allow_unknown = 0;\n \tint ret = 0;\n \n \tstruct option list_options[] = {\n@@ -41,6 +52,8 @@ static int list(int argc, const char **argv, const char *prefix,\n \t\t\t    N_(\"use NUL as line terminator\"), '\\0'),\n \t\tOPT_BOOL(0, \"show-scope\", &show_scope,\n \t\t\t N_(\"show the config scope that defined each hook\")),\n+\t\tOPT_BOOL(0, \"allow-unknown-hook-name\", &allow_unknown,\n+\t\t\t N_(\"allow running a hook with a non-native hook name\")),\n \t\tOPT_END(),\n \t};\n \n@@ -57,6 +70,13 @@ static int list(int argc, const char **argv, const char *prefix,\n \n \thookname = argv[0];\n \n+\tif (!allow_unknown && !is_known_hook(hookname)) {\n+\t\terror(_(\"unknown hook event '%s';\\n\"\n+\t\t\t\"use --allow-unknown-hook-name to allow non-native hook names\"),\n+\t\t      hookname);\n+\t\treturn 1;\n+\t}\n+\n \thead = list_hooks(repo, hookname, NULL);\n \n \tif (!head->nr) {\n@@ -103,8 +123,11 @@ static int run(int argc, const char **argv, const char *prefix,\n \tint i;\n \tstruct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;\n \tint ignore_missing = 0;\n+\tint allow_unknown = 0;\n \tconst char *hook_name;\n \tstruct option run_options[] = {\n+\t\tOPT_BOOL(0, \"allow-unknown-hook-name\", &allow_unknown,\n+\t\t\t N_(\"allow running a hook with a non-native hook name\")),\n \t\tOPT_BOOL(0, \"ignore-missing\", &ignore_missing,\n \t\t\t N_(\"silently ignore missing requested <hook-name>\")),\n \t\tOPT_STRING(0, \"to-stdin\", &opt.path_to_stdin, N_(\"path\"),\n@@ -136,6 +159,14 @@ static int run(int argc, const char **argv, const char *prefix,\n \trepo_config(the_repository, git_default_config, NULL);\n \n \thook_name = argv[0];\n+\n+\tif (!allow_unknown && !is_known_hook(hook_name)) {\n+\t\terror(_(\"unknown hook event '%s';\\n\"\n+\t\t\t\"use --allow-unknown-hook-name to allow non-native hook names\"),\n+\t\t      hook_name);\n+\t\treturn 1;\n+\t}\n+\n \tif (!ignore_missing)\n \t\topt.error_if_missing = 1;\n \tret = run_hooks_opt(the_repository, hook_name, &opt);\ndiff --git a/t/t1800-hook.sh b/t/t1800-hook.sh\nindex 8c5237449d..96749fc06d 100755\n--- a/t/t1800-hook.sh\n+++ b/t/t1800-hook.sh\n@@ -31,11 +31,41 @@ test_expect_success 'git hook usage' '\n \tgrep \"unknown option\" err\n '\n \n+test_expect_success 'git hook list: unknown hook name is rejected' '\n+\ttest_must_fail git hook list prereceive 2>err &&\n+\ttest_grep \"unknown hook event\" err\n+'\n+\n+test_expect_success 'git hook run: unknown hook name is rejected' '\n+\ttest_must_fail git hook run prereceive 2>err &&\n+\ttest_grep \"unknown hook event\" err\n+'\n+\n+test_expect_success 'git hook list: known hook name is accepted' '\n+\ttest_must_fail git hook list pre-receive 2>err &&\n+\ttest_grep ! \"unknown hook event\" err\n+'\n+\n+test_expect_success 'git hook run: known hook name is accepted' '\n+\tgit hook run --ignore-missing pre-receive 2>err &&\n+\ttest_grep ! \"unknown hook event\" err\n+'\n+\n+test_expect_success 'git hook run: --allow-unknown-hook-name overrides rejection' '\n+\tgit hook run --allow-unknown-hook-name --ignore-missing custom-hook 2>err &&\n+\ttest_grep ! \"unknown hook event\" err\n+'\n+\n+test_expect_success 'git hook list: --allow-unknown-hook-name overrides rejection' '\n+\ttest_must_fail git hook list --allow-unknown-hook-name custom-hook 2>err &&\n+\ttest_grep ! \"unknown hook event\" err\n+'\n+\n test_expect_success 'git hook list: nonexistent hook' '\n \tcat >stderr.expect <<-\\EOF &&\n \twarning: no hooks found for event '\\''test-hook'\\''\n \tEOF\n-\ttest_expect_code 1 git hook list test-hook 2>stderr.actual &&\n+\ttest_expect_code 1 git hook list --allow-unknown-hook-name test-hook 2>stderr.actual &&\n \ttest_cmp stderr.expect stderr.actual\n '\n \n@@ -47,7 +77,7 @@ test_expect_success 'git hook list: traditional hook from hookdir' '\n \tcat >expect <<-\\EOF &&\n \thook from hookdir\n \tEOF\n-\tgit hook list test-hook >actual &&\n+\tgit hook list --allow-unknown-hook-name test-hook >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -56,7 +86,7 @@ test_expect_success 'git hook list: configured hook' '\n \ttest_config hook.myhook.event test-hook --add &&\n \n \techo \"myhook\" >expect &&\n-\tgit hook list test-hook >actual &&\n+\tgit hook list --allow-unknown-hook-name test-hook >actual &&\n \ttest_cmp expect actual\n '\n \n@@ -68,7 +98,7 @@ test_expect_success 'git hook list: -z shows NUL-terminated output' '\n \ttest_config hook.myhook.event test-hook --add &&\n \n \tprintf \"myhookQhook from hookdirQ\" >expect &&\n-\tgit hook list -z test-hook >actual.raw &&\n+\tgit hook list --allow-unknown-hook-name -z test-hook >actual.raw &&\n \tnul_to_q <actual.raw >actual &&\n \ttest_cmp expect actual\n '\n@@ -77,12 +107,12 @@ test_expect_success 'git hook run: nonexistent hook' '\n \tcat >stderr.expect <<-\\EOF &&\n \terror: cannot find a hook named test-hook\n \tEOF\n-\ttest_expect_code 1 git hook run test-hook 2>stderr.actual &&\n+\ttest_expect_code 1 git hook run --allow-unknown-hook-name test-hook 2>stderr.actual &&\n \ttest_cmp stderr.expect stderr.actual\n '\n \n test_expect_success 'git hook run: nonexistent hook with --ignore-missing' '\n-\tgit hook run --ignore-missing does-not-exist 2>stderr.actual &&\n+\tgit hook run --allow-unknown-hook-name --ignore-missing does-not-exist 2>stderr.actual &&\n \ttest_must_be_empty stderr.actual\n '\n \n@@ -94,7 +124,7 @@ test_expect_success 'git hook run: basic' '\n \tcat >expect <<-\\EOF &&\n \tTest hook\n \tEOF\n-\tgit hook run test-hook 2>actual &&\n+\tgit hook run --allow-unknown-hook-name test-hook 2>actual &&\n \ttest_cmp expect actual\n '\n \n@@ -108,7 +138,7 @@ test_expect_success 'git hook run: stdout and stderr both write to our stderr' '\n \tWill end up on stderr\n \tWill end up on stderr\n \tEOF\n-\tgit hook run test-hook >stdout.actual 2>stderr.actual &&\n+\tgit hook run --allow-unknown-hook-name test-hook >stdout.actual 2>stderr.actual &&\n \ttest_cmp stderr.expect stderr.actual &&\n \ttest_must_be_empty stdout.actual\n '\n@@ -120,12 +150,12 @@ do\n \t\texit $code\n \t\tEOF\n \n-\t\ttest_expect_code $code git hook run test-hook\n+\t\ttest_expect_code $code git hook run --allow-unknown-hook-name test-hook\n \t'\n done\n \n test_expect_success 'git hook run arg u ments without -- is not allowed' '\n-\ttest_expect_code 129 git hook run test-hook arg u ments\n+\ttest_expect_code 129 git hook run --allow-unknown-hook-name test-hook arg u ments\n '\n \n test_expect_success 'git hook run -- pass arguments' '\n@@ -139,7 +169,7 @@ test_expect_success 'git hook run -- pass arguments' '\n \tu ments\n \tEOF\n \n-\tgit hook run test-hook -- arg \"u ments\" 2>actual &&\n+\tgit hook run --allow-unknown-hook-name test-hook -- arg \"u ments\" 2>actual &&\n \ttest_cmp expect actual\n '\n \n@@ -148,12 +178,12 @@ test_expect_success 'git hook run: out-of-repo runs execute global hooks' '\n \ttest_config_global hook.global-hook.command \"echo no repo no problems\" --add &&\n \n \techo \"global-hook\" >expect &&\n-\tnongit git hook list test-hook >actual &&\n+\tnongit git hook list --allow-unknown-hook-name test-hook >actual &&\n \ttest_cmp expect actual &&\n \n \techo \"no repo no problems\" >expect &&\n \n-\tnongit git hook run test-hook 2>actual &&\n+\tnongit git hook run --allow-unknown-hook-name test-hook 2>actual &&\n \ttest_cmp expect actual\n '\n \n@@ -178,11 +208,11 @@ test_expect_success 'git -c core.hooksPath=<PATH> hook run' '\n \t# Test various ways of specifying the path. See also\n \t# t1350-config-hooks-path.sh\n \t>actual &&\n-\tgit hook run test-hook -- ignored 2>>actual &&\n-\tgit -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&\n-\tgit -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&\n-\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run test-hook -- three 2>>actual &&\n-\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run test-hook -- four 2>>actual &&\n+\tgit hook run --allow-unknown-hook-name test-hook -- ignored 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks hook run --allow-unknown-hook-name test-hook -- one 2>>actual &&\n+\tgit -c core.hooksPath=my-hooks/ hook run --allow-unknown-hook-name test-hook -- two 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks\" hook run --allow-unknown-hook-name test-hook -- three 2>>actual &&\n+\tgit -c core.hooksPath=\"$PWD/my-hooks/\" hook run --allow-unknown-hook-name test-hook -- four 2>>actual &&\n \ttest_cmp expect actual\n '\n \n@@ -262,7 +292,7 @@ test_expect_success 'hook can be configured for multiple events' '\n \t# 'ghi' should be included in both 'pre-commit' and 'test-hook'\n \tgit hook list pre-commit >actual &&\n \tgrep \"ghi\" actual &&\n-\tgit hook list test-hook >actual &&\n+\tgit hook list --allow-unknown-hook-name test-hook >actual &&\n \tgrep \"ghi\" actual\n '\n \n@@ -336,15 +366,15 @@ test_expect_success 'stdin to multiple hooks' '\n \tb3\n \tEOF\n \n-\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\tgit hook run --allow-unknown-hook-name --to-stdin=input test-hook 2>actual &&\n \ttest_cmp expected actual\n '\n \n test_expect_success 'rejects hooks with no commands configured' '\n \ttest_config hook.broken.event \"test-hook\" &&\n-\ttest_must_fail git hook list test-hook 2>actual &&\n+\ttest_must_fail git hook list --allow-unknown-hook-name test-hook 2>actual &&\n \ttest_grep \"hook.broken.command\" actual &&\n-\ttest_must_fail git hook run test-hook 2>actual &&\n+\ttest_must_fail git hook run --allow-unknown-hook-name test-hook 2>actual &&\n \ttest_grep \"hook.broken.command\" actual\n '\n \n@@ -353,7 +383,7 @@ test_expect_success 'disabled hook is not run' '\n \ttest_config hook.skipped.command \"echo \\\"Should not run\\\"\" &&\n \ttest_config hook.skipped.enabled false &&\n \n-\tgit hook run --ignore-missing test-hook 2>actual &&\n+\tgit hook run --allow-unknown-hook-name --ignore-missing test-hook 2>actual &&\n \ttest_must_be_empty actual\n '\n \n@@ -403,7 +433,7 @@ test_expect_success 'globally disabled hook can be re-enabled locally' '\n \ttest_config hook.global-hook.enabled true &&\n \n \techo \"global-hook ran\" >expected &&\n-\tgit hook run test-hook 2>actual &&\n+\tgit hook run --allow-unknown-hook-name test-hook 2>actual &&\n \ttest_cmp expected actual\n '\n \n@@ -463,7 +493,7 @@ test_expect_success 'git hook run a hook with a bad shebang' '\n \n \ttest_expect_code 1 git \\\n \t\t-c core.hooksPath=bad-hooks \\\n-\t\thook run test-hook >out 2>err &&\n+\t\thook run --allow-unknown-hook-name test-hook >out 2>err &&\n \ttest_must_be_empty out &&\n \n \t# TODO: We should emit the same (or at least a more similar)\n@@ -487,7 +517,7 @@ test_expect_success 'stdin to hooks' '\n \tEOF\n \n \techo hello >input &&\n-\tgit hook run --to-stdin=input test-hook 2>actual &&\n+\tgit hook run --allow-unknown-hook-name --to-stdin=input test-hook 2>actual &&\n \ttest_cmp expect actual\n '\n \n-- \n2.52.0.732.gb351b5166d.dirty\n\n"},{"id":"540016","messageId":"xmqqse9nsj6g.fsf@gitster.g","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 00/12] config-hook cleanups and three small git-hook features","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-25T21:17:27Z","receivedAt":"2026-03-25T21:17:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":null},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Hello everyone,\n>\n> v3 addresses all the feedback and requests received in v2, many thanks to all\n> who contributed.\n>\n> Let's please stop adding features since this is getting rather big again. :)\n> New features can be added in subsequent patches.\n>\n> This series is mostly for minor cleanups, bug fixes and refactorings + three\n> minor feature additions to git-hook, which resulted from review discussions:\n>\n> 1. The ability to show the config scope (--show-scope).\n> 2. The ability to show which hooks are disabled.\n> 3. The ability reject unknown hook names with \"--allow-unknown-hook-name\" as\n>    an escape hatch.\n>\n> The series is based on the master branch.\n\nReplaced the old one, and then rebuilt ar/parallel-hooks on top.\nPlease sanity-check the latter when I later push out the result of\ntoday's integration.\n\nOne thing I noticed a bit annoying was that we have \"event_disabled\"\nboolean in \"struct hook\", plus a string-list of the same name in\n\"struct repository\", which means \"git grep event_disabled\" hits\nboth.  Perhaps the caching \"struct string_list event_disabled\" can\nbe renamed to reflect what it is a bit better, like \"disabled_events\"?\n\n"},{"id":"540053","messageId":"87fr5mdh7e.fsf@collabora.com","threadId":"65171","inReplyTo":"xmqqse9nsj6g.fsf@gitster.g","subject":"Re: [PATCH v3 00/12] config-hook cleanups and three small git-hook features","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-03-26T10:21:25Z","receivedAt":"2026-03-26T10:21:48Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":null},"body":"On Wed, 25 Mar 2026, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n>> Hello everyone,\n>>\n>> v3 addresses all the feedback and requests received in v2, many thanks to all\n>> who contributed.\n>>\n>> Let's please stop adding features since this is getting rather big again. :)\n>> New features can be added in subsequent patches.\n>>\n>> This series is mostly for minor cleanups, bug fixes and refactorings + three\n>> minor feature additions to git-hook, which resulted from review discussions:\n>>\n>> 1. The ability to show the config scope (--show-scope).\n>> 2. The ability to show which hooks are disabled.\n>> 3. The ability reject unknown hook names with \"--allow-unknown-hook-name\" as\n>>    an escape hatch.\n>>\n>> The series is based on the master branch.\n>\n> Replaced the old one, and then rebuilt ar/parallel-hooks on top.\n> Please sanity-check the latter when I later push out the result of\n> today's integration.\n\nar/parallel-hooks is expected to break with this v3.\nSorry I didn't make this clear in the cover letter.\n\nWhat broke it is the new --allow-unknown-hook-name feature and the\ncode-review changes (I'd be very surprised if the old ar/parallel-hooks\napplied cleanly on top of this, I fixed quite a few conflicts).\n\nPlease use v5 of ar/parallel-hooks which I just posted and should apply\ncleanly on top of this v3.\n\n> One thing I noticed a bit annoying was that we have \"event_disabled\"\n> boolean in \"struct hook\", plus a string-list of the same name in\n> \"struct repository\", which means \"git grep event_disabled\" hits\n> both.  Perhaps the caching \"struct string_list event_disabled\" can\n> be renamed to reflect what it is a bit better, like \"disabled_events\"?\n\nFixed in parallel-hooks v5.\n\nThanks!\n"},{"id":"540170","messageId":"acY6KtREkGcf6dyp@pks.im","threadId":"65171","inReplyTo":"20260325195503.1139418-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 00/12] config-hook cleanups and three small git-hook features","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-27T08:04:58Z","receivedAt":"2026-03-27T08:05:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":null},"body":"On Wed, Mar 25, 2026 at 09:54:51PM +0200, Adrian Ratiu wrote:\n> Changes in v3:\n> * New commit: properly initialize strbuf in receive-pack.c (Patrick)\n> * New commit: add a check which prevents unknown hooks with git-hook(1) (Patrick)\n> * Removed duplicated function doc comment between .h and .c files (Patrick)\n> * Extended `git hook list` test to also include a hook from the hookdir (Patrick)\n> * Converted unsigned int disabled:1 to proper bool (Patrick)\n> * Minor commit rewording, header sorting, blank line fixes (Patrick)\n\nThanks, I'm happy with this version!\n\nPatrick\n"},{"id":"540191","messageId":"xmqq7bqxmev5.fsf@gitster.g","threadId":"65171","inReplyTo":"acY6KtREkGcf6dyp@pks.im","subject":"Re: [PATCH v3 00/12] config-hook cleanups and three small git-hook features","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-27T16:11:42Z","receivedAt":"2026-03-27T16:11:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":null},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> On Wed, Mar 25, 2026 at 09:54:51PM +0200, Adrian Ratiu wrote:\n>> Changes in v3:\n>> * New commit: properly initialize strbuf in receive-pack.c (Patrick)\n>> * New commit: add a check which prevents unknown hooks with git-hook(1) (Patrick)\n>> * Removed duplicated function doc comment between .h and .c files (Patrick)\n>> * Extended `git hook list` test to also include a hook from the hookdir (Patrick)\n>> * Converted unsigned int disabled:1 to proper bool (Patrick)\n>> * Minor commit rewording, header sorting, blank line fixes (Patrick)\n>\n> Thanks, I'm happy with this version!\n>\n> Patrick\n\nThanks, both.\n"}]}