Files
git/hook.c
Emily Shaffer 9fdaa67889 hook: add "git hook list" command
The previous commit introduced an ability to run multiple commands for
hook events and next commit will introduce the ability to define hooks
from configs, in addition to the "traditional" hooks from the hookdir.

Introduce a new command "git hook list" to make inspecting hooks easier
both for users and for the tests we will add.

Further commits will expand on this, e.g. by adding a -z output mode.

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>
2026-02-19 13:23:41 -08:00

297 lines
7.2 KiB
C

#include "git-compat-util.h"
#include "abspath.h"
#include "advice.h"
#include "gettext.h"
#include "hook.h"
#include "path.h"
#include "run-command.h"
#include "config.h"
#include "strbuf.h"
#include "environment.h"
#include "setup.h"
const char *find_hook(struct repository *r, const char *name)
{
static struct strbuf path = STRBUF_INIT;
int found_hook;
repo_git_path_replace(r, &path, "hooks/%s", name);
found_hook = access(path.buf, X_OK) >= 0;
#ifdef STRIP_EXTENSION
if (!found_hook) {
int err = errno;
strbuf_addstr(&path, STRIP_EXTENSION);
found_hook = access(path.buf, X_OK) >= 0;
if (!found_hook)
errno = err;
}
#endif
if (!found_hook) {
if (errno == EACCES && advice_enabled(ADVICE_IGNORED_HOOK)) {
static struct string_list advise_given = STRING_LIST_INIT_DUP;
if (!string_list_lookup(&advise_given, name)) {
string_list_insert(&advise_given, name);
advise(_("The '%s' hook was ignored because "
"it's not set as executable.\n"
"You can disable this warning with "
"`git config set advice.ignoredHook false`."),
path.buf);
}
}
return NULL;
}
return path.buf;
}
static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)
{
if (!h)
return;
if (h->kind == HOOK_TRADITIONAL)
free((void *)h->u.traditional.path);
if (cb_data_free)
cb_data_free(h->feed_pipe_cb_data);
free(h);
}
void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free)
{
struct string_list_item *item;
for_each_string_list_item(item, hooks)
hook_clear(item->util, cb_data_free);
string_list_clear(hooks, 0);
}
/* Helper to detect and add default "traditional" hooks from the hookdir. */
static void list_hooks_add_default(struct repository *r, const char *hookname,
struct string_list *hook_list,
struct run_hooks_opt *options)
{
const char *hook_path = find_hook(r, hookname);
struct hook *h;
if (!hook_path)
return;
h = xcalloc(1, sizeof(struct hook));
/*
* If the hook is to run in a specific dir, a relative path can
* become invalid in that dir, so convert to an absolute path.
*/
if (options && options->dir)
hook_path = absolute_path(hook_path);
/* Setup per-hook internal state cb data */
if (options && options->feed_pipe_cb_data_alloc)
h->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);
h->kind = HOOK_TRADITIONAL;
h->u.traditional.path = xstrdup(hook_path);
string_list_append(hook_list, hook_path)->util = h;
}
struct string_list *list_hooks(struct repository *r, const char *hookname,
struct run_hooks_opt *options)
{
struct string_list *hook_head;
if (!hookname)
BUG("null hookname was provided to hook_list()!");
hook_head = xmalloc(sizeof(struct string_list));
string_list_init_dup(hook_head);
/* Add the default "traditional" hooks from hookdir. */
list_hooks_add_default(r, hookname, hook_head, options);
return hook_head;
}
int hook_exists(struct repository *r, const char *name)
{
struct string_list *hooks = list_hooks(r, name, NULL);
int exists = hooks->nr > 0;
hook_list_clear(hooks, NULL);
free(hooks);
return exists;
}
static int pick_next_hook(struct child_process *cp,
struct strbuf *out UNUSED,
void *pp_cb,
void **pp_task_cb)
{
struct hook_cb_data *hook_cb = pp_cb;
struct string_list *hook_list = hook_cb->hook_command_list;
struct hook *h;
if (hook_cb->hook_to_run_index >= hook_list->nr)
return 0;
h = hook_list->items[hook_cb->hook_to_run_index++].util;
cp->no_stdin = 1;
strvec_pushv(&cp->env, hook_cb->options->env.v);
if (hook_cb->options->path_to_stdin && hook_cb->options->feed_pipe)
BUG("options path_to_stdin and feed_pipe are mutually exclusive");
/* reopen the file for stdin; run_command closes it. */
if (hook_cb->options->path_to_stdin) {
cp->no_stdin = 0;
cp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);
}
if (hook_cb->options->feed_pipe) {
cp->no_stdin = 0;
/* start_command() will allocate a pipe / stdin fd for us */
cp->in = -1;
}
cp->stdout_to_stderr = hook_cb->options->stdout_to_stderr;
cp->trace2_hook_name = hook_cb->hook_name;
cp->dir = hook_cb->options->dir;
/* Add hook exec paths or commands */
if (h->kind == HOOK_TRADITIONAL)
strvec_push(&cp->args, h->u.traditional.path);
if (!cp->args.nr)
BUG("hook must have at least one command or exec path");
strvec_pushv(&cp->args, hook_cb->options->args.v);
/*
* Provide per-hook internal state via task_cb for easy access, so
* hook callbacks don't have to go through hook_cb->options.
*/
*pp_task_cb = h->feed_pipe_cb_data;
return 1;
}
static int notify_start_failure(struct strbuf *out UNUSED,
void *pp_cb,
void *pp_task_cp UNUSED)
{
struct hook_cb_data *hook_cb = pp_cb;
hook_cb->rc |= 1;
return 1;
}
static int notify_hook_finished(int result,
struct strbuf *out UNUSED,
void *pp_cb,
void *pp_task_cb UNUSED)
{
struct hook_cb_data *hook_cb = pp_cb;
struct run_hooks_opt *opt = hook_cb->options;
hook_cb->rc |= result;
if (opt->invoked_hook)
*opt->invoked_hook = 1;
return 0;
}
static void run_hooks_opt_clear(struct run_hooks_opt *options)
{
strvec_clear(&options->env);
strvec_clear(&options->args);
}
int run_hooks_opt(struct repository *r, const char *hook_name,
struct run_hooks_opt *options)
{
struct hook_cb_data cb_data = {
.rc = 0,
.hook_name = hook_name,
.options = options,
};
int ret = 0;
const struct run_process_parallel_opts opts = {
.tr2_category = "hook",
.tr2_label = hook_name,
.processes = options->jobs,
.ungroup = options->jobs == 1,
.get_next_task = pick_next_hook,
.start_failure = notify_start_failure,
.feed_pipe = options->feed_pipe,
.task_finished = notify_hook_finished,
.data = &cb_data,
};
if (!options)
BUG("a struct run_hooks_opt must be provided to run_hooks");
if (options->path_to_stdin && options->feed_pipe)
BUG("options path_to_stdin and feed_pipe are mutually exclusive");
if (!options->jobs)
BUG("run_hooks_opt must be called with options.jobs >= 1");
/*
* Ensure cb_data copy and free functions are either provided together,
* or neither one is provided.
*/
if ((options->feed_pipe_cb_data_alloc && !options->feed_pipe_cb_data_free) ||
(!options->feed_pipe_cb_data_alloc && options->feed_pipe_cb_data_free))
BUG("feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together");
if (options->invoked_hook)
*options->invoked_hook = 0;
cb_data.hook_command_list = list_hooks(r, hook_name, options);
if (!cb_data.hook_command_list->nr) {
if (options->error_if_missing)
ret = error("cannot find a hook named %s", hook_name);
goto cleanup;
}
run_processes_parallel(&opts);
ret = cb_data.rc;
cleanup:
hook_list_clear(cb_data.hook_command_list, options->feed_pipe_cb_data_free);
free(cb_data.hook_command_list);
run_hooks_opt_clear(options);
return ret;
}
int run_hooks(struct repository *r, const char *hook_name)
{
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
return run_hooks_opt(r, hook_name, &opt);
}
int run_hooks_l(struct repository *r, const char *hook_name, ...)
{
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
va_list ap;
const char *arg;
va_start(ap, hook_name);
while ((arg = va_arg(ap, const char *)))
strvec_push(&opt.args, arg);
va_end(ap);
return run_hooks_opt(r, hook_name, &opt);
}