]> git.ipfire.org Git - thirdparty/git.git/commitdiff
hook: allow out-of-repo 'git hook' invocations
authorEmily Shaffer <emilyshaffer@google.com>
Wed, 18 Feb 2026 22:23:51 +0000 (00:23 +0200)
committerJunio C Hamano <gitster@pobox.com>
Thu, 19 Feb 2026 21:24:39 +0000 (13:24 -0800)
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>
git.c
hook.c
t/t1800-hook.sh

diff --git a/git.c b/git.c
index c5fad56813f437cac4e3f52a7f96b2dc594fa25a..a9e462ee3228f944d9a7e820bd5aa39e45cc6ae6 100644 (file)
--- a/git.c
+++ b/git.c
@@ -586,7 +586,7 @@ static struct cmd_struct commands[] = {
        { "grep", cmd_grep, RUN_SETUP_GENTLY },
        { "hash-object", cmd_hash_object },
        { "help", cmd_help },
-       { "hook", cmd_hook, RUN_SETUP },
+       { "hook", cmd_hook, RUN_SETUP_GENTLY },
        { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
        { "init", cmd_init_db },
        { "init-db", cmd_init_db },
diff --git a/hook.c b/hook.c
index fee0a7ab4fcb4d6438744723fade1e12cb5502bf..2c8252b2c4fceb01b3a5e71b9be74900470a2082 100644 (file)
--- a/hook.c
+++ b/hook.c
@@ -18,6 +18,9 @@ const char *find_hook(struct repository *r, const char *name)
 
        int found_hook;
 
+       if (!r || !r->gitdir)
+               return NULL;
+
        repo_git_path_replace(r, &path, "hooks/%s", name);
        found_hook = access(path.buf, X_OK) >= 0;
 #ifdef STRIP_EXTENSION
@@ -268,12 +271,18 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
        strmap_clear(&cb_data.event_hooks, 0);
 }
 
-/* Return the hook config map for `r`, populating it first if needed. */
+/*
+ * Return the hook config map for `r`, populating it first if needed.
+ *
+ * Out-of-repo calls (r->gitdir == NULL) allocate and return a temporary
+ * cache map; the caller is responsible for freeing it with
+ * hook_cache_clear() + free().
+ */
 static struct strmap *get_hook_config_cache(struct repository *r)
 {
        struct strmap *cache = NULL;
 
-       if (r) {
+       if (r && r->gitdir) {
                /*
                 * For in-repo calls, the map is stored in r->hook_config_cache,
                 * so repeated invocations don't parse the configs, so allocate
@@ -285,6 +294,14 @@ static struct strmap *get_hook_config_cache(struct repository *r)
                        build_hook_config_map(r, r->hook_config_cache);
                }
                cache = r->hook_config_cache;
+       } else {
+               /*
+                * Out-of-repo calls (no gitdir) allocate and return a temporary
+                * map cache which gets free'd immediately by the caller.
+                */
+               cache = xcalloc(1, sizeof(*cache));
+               strmap_init(cache);
+               build_hook_config_map(r, cache);
        }
 
        return cache;
@@ -315,6 +332,15 @@ static void list_hooks_add_configured(struct repository *r,
 
                string_list_append(list, friendly_name)->util = hook;
        }
+
+       /*
+        * Cleanup temporary cache for out-of-repo calls since they can't be
+        * stored persistently. Next out-of-repo calls will have to re-parse.
+        */
+       if (!r || !r->gitdir) {
+               hook_cache_clear(cache);
+               free(cache);
+       }
 }
 
 struct string_list *list_hooks(struct repository *r, const char *hookname,
index fb6bc554b9677e21ef91e903c97b8a5066d11f75..e58151e8f868ba7f9bd80b120ad98a40c7296cf2 100755 (executable)
@@ -131,12 +131,18 @@ test_expect_success 'git hook run -- pass arguments' '
        test_cmp expect actual
 '
 
-test_expect_success 'git hook run -- out-of-repo runs excluded' '
-       test_hook test-hook <<-EOF &&
-       echo Test hook
-       EOF
+test_expect_success 'git hook run: out-of-repo runs execute global hooks' '
+       test_config_global hook.global-hook.event test-hook --add &&
+       test_config_global hook.global-hook.command "echo no repo no problems" --add &&
 
-       nongit test_must_fail git hook run test-hook
+       echo "global-hook" >expect &&
+       nongit git hook list test-hook >actual &&
+       test_cmp expect actual &&
+
+       echo "no repo no problems" >expect &&
+
+       nongit git hook run test-hook 2>actual &&
+       test_cmp expect actual
 '
 
 test_expect_success 'git -c core.hooksPath=<PATH> hook run' '