diff --git a/git.c b/git.c index c5fad56813..a9e462ee32 100644 --- 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 fee0a7ab4f..2c8252b2c4 100644 --- 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, diff --git a/t/t1800-hook.sh b/t/t1800-hook.sh index fb6bc554b9..e58151e8f8 100755 --- a/t/t1800-hook.sh +++ b/t/t1800-hook.sh @@ -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= hook run' '