hook: allow event = "" to overwrite previous values

Add the ability for empty events to clear previously set multivalue variables, so the newly added "hook.*.event" behave like the other multivalued keys. Suggested-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Adrian Ratiu committed Feb 19, 2026 at 00:23 UTC d084fa2a915784d65257fbaff43f00b3ea5c8a44
3 files changed +34 -11
Documentation/config/hook.adoc
+3 -1
@@ -12,7 +12,9 @@ hook.<name>.event::
12 linkgit:githooks[5] for a complete list of hook events.) On the
13 specified event, the associated `hook.<name>.command` is executed.
14 This is a multi-valued key. To run `hook.<name>` on multiple
15 - events, specify the key more than once. See linkgit:git-hook[1].
15 + events, specify the key more than once. An empty value resets
16 + the list of events, clearing any previously defined events for
17 + `hook.<name>`. See linkgit:git-hook[1].
18
19 hook.<name>.enabled::
20 Whether the hook `hook.<name>` is enabled. Defaults to `true`.
hook.c
+19 -10
@@ -147,18 +147,27 @@ static int hook_config_lookup_all(const char *key, const char *value,
147 hook_name = xmemdupz(name, name_len);
148
149 if (!strcmp(subkey, "event")) {
150 - struct string_list *hooks =
151 - strmap_get(&data->event_hooks, value);
150 + if (!*value) {
151 + /* Empty values reset previous events for this hook. */
152 + struct hashmap_iter iter;
153 + struct strmap_entry *e;
154 +
155 + strmap_for_each_entry(&data->event_hooks, &iter, e)
156 + unsorted_string_list_remove(e->value, hook_name);
157 + } else {
158 + struct string_list *hooks =
159 + strmap_get(&data->event_hooks, value);
160 +
161 + if (!hooks) {
162 + hooks = xcalloc(1, sizeof(*hooks));
163 + string_list_init_dup(hooks);
164 + strmap_put(&data->event_hooks, value, hooks);
165 + }
166
153 - if (!hooks) {
154 - hooks = xcalloc(1, sizeof(*hooks));
155 - string_list_init_dup(hooks);
156 - strmap_put(&data->event_hooks, value, hooks);
167 + /* Re-insert if necessary to preserve last-seen order. */
168 + unsorted_string_list_remove(hooks, hook_name);
169 + string_list_append(hooks, hook_name);
170 }
158 -
159 - /* Re-insert if necessary to preserve last-seen order. */
160 - unsorted_string_list_remove(hooks, hook_name);
161 - string_list_append(hooks, hook_name);
171 } else if (!strcmp(subkey, "command")) {
172 /* Store command overwriting the old value */
173 char *old = strmap_put(&data->commands, hook_name,
t/t1800-hook.sh
+12
@@ -226,6 +226,18 @@ test_expect_success 'git hook list reorders on duplicate event declarations' '
226 test_cmp expected actual
227 '
228
229 +test_expect_success 'git hook list: empty event value resets events' '
230 + setup_hooks &&
231 +
232 + # ghi is configured for pre-commit; reset it with an empty value
233 + test_config hook.ghi.event "" --add &&
234 +
235 + # only def should remain for pre-commit
236 + echo "def" >expected &&
237 + git hook list pre-commit >actual &&
238 + test_cmp expected actual
239 +'
240 +
241 test_expect_success 'hook can be configured for multiple events' '
242 setup_hooks &&
243