hook: allow out-of-repo 'git hook' invocations

Since hooks can now be supplied via the config, and a config can be present without a gitdir via the global and system configs, we can start to allow 'git hook run' to occur without a gitdir. This enables us to do things like run sendemail-validate hooks when running 'git send-email' from a nongit directory. It still doesn't make sense to look for hooks in the hookdir in nongit repos, though, as there is no hookdir. Signed-off-by: Emily Shaffer <emilyshaffer@google.com> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Emily Shaffer committed Feb 19, 2026 at 00:23 UTC b51e238ddf896439d2746b883d892f8f9bba649f
3 files changed +40 -8
git.c
+1 -1
@@ -586,7 +586,7 @@ static struct cmd_struct commands[] = {
586 { "grep", cmd_grep, RUN_SETUP_GENTLY },
587 { "hash-object", cmd_hash_object },
588 { "help", cmd_help },
589 - { "hook", cmd_hook, RUN_SETUP },
589 + { "hook", cmd_hook, RUN_SETUP_GENTLY },
590 { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
591 { "init", cmd_init_db },
592 { "init-db", cmd_init_db },
hook.c
+28 -2
@@ -18,6 +18,9 @@ const char *find_hook(struct repository *r, const char *name)
18
19 int found_hook;
20
21 + if (!r || !r->gitdir)
22 + return NULL;
23 +
24 repo_git_path_replace(r, &path, "hooks/%s", name);
25 found_hook = access(path.buf, X_OK) >= 0;
26 #ifdef STRIP_EXTENSION
@@ -268,12 +271,18 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
271 strmap_clear(&cb_data.event_hooks, 0);
272 }
273
271 -/* Return the hook config map for `r`, populating it first if needed. */
274 +/*
275 + * Return the hook config map for `r`, populating it first if needed.
276 + *
277 + * Out-of-repo calls (r->gitdir == NULL) allocate and return a temporary
278 + * cache map; the caller is responsible for freeing it with
279 + * hook_cache_clear() + free().
280 + */
281 static struct strmap *get_hook_config_cache(struct repository *r)
282 {
283 struct strmap *cache = NULL;
284
276 - if (r) {
285 + if (r && r->gitdir) {
286 /*
287 * For in-repo calls, the map is stored in r->hook_config_cache,
288 * so repeated invocations don't parse the configs, so allocate
@@ -285,6 +294,14 @@ static struct strmap *get_hook_config_cache(struct repository *r)
294 build_hook_config_map(r, r->hook_config_cache);
295 }
296 cache = r->hook_config_cache;
297 + } else {
298 + /*
299 + * Out-of-repo calls (no gitdir) allocate and return a temporary
300 + * map cache which gets free'd immediately by the caller.
301 + */
302 + cache = xcalloc(1, sizeof(*cache));
303 + strmap_init(cache);
304 + build_hook_config_map(r, cache);
305 }
306
307 return cache;
@@ -315,6 +332,15 @@ static void list_hooks_add_configured(struct repository *r,
332
333 string_list_append(list, friendly_name)->util = hook;
334 }
335 +
336 + /*
337 + * Cleanup temporary cache for out-of-repo calls since they can't be
338 + * stored persistently. Next out-of-repo calls will have to re-parse.
339 + */
340 + if (!r || !r->gitdir) {
341 + hook_cache_clear(cache);
342 + free(cache);
343 + }
344 }
345
346 struct string_list *list_hooks(struct repository *r, const char *hookname,
t/t1800-hook.sh
+11 -5
@@ -131,12 +131,18 @@ test_expect_success 'git hook run -- pass arguments' '
131 test_cmp expect actual
132 '
133
134 -test_expect_success 'git hook run -- out-of-repo runs excluded' '
135 - test_hook test-hook <<-EOF &&
136 - echo Test hook
137 - EOF
134 +test_expect_success 'git hook run: out-of-repo runs execute global hooks' '
135 + test_config_global hook.global-hook.event test-hook --add &&
136 + test_config_global hook.global-hook.command "echo no repo no problems" --add &&
137
139 - nongit test_must_fail git hook run test-hook
138 + echo "global-hook" >expect &&
139 + nongit git hook list test-hook >actual &&
140 + test_cmp expect actual &&
141 +
142 + echo "no repo no problems" >expect &&
143 +
144 + nongit git hook run test-hook 2>actual &&
145 + test_cmp expect actual
146 '
147
148 test_expect_success 'git -c core.hooksPath=<PATH> hook run' '