hook: run a list of hooks to prepare for multihook support

Hooks are limited to run one command (the default from the hookdir) for each event. This limitation makes it impossible to run multiple commands via config files, which the next commits will add. Implement the ability to run a list of hooks in hook.[ch]. For now, the list contains only one entry representing the "default" hook from the hookdir, so there is no user-visible change in this commit. All hook commands still run sequentially like before. A separate patch series will enable running them in parallel. 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 4a36cb4c9f0f508db2e5dda75673e0d4b1242007
2 files changed +153 -45
hook.c
+108 -31
@@ -47,9 +47,97 @@ const char *find_hook(struct repository *r, const char *name)
47 return path.buf;
48 }
49
50 +static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)
51 +{
52 + if (!h)
53 + return;
54 +
55 + if (h->kind == HOOK_TRADITIONAL)
56 + free((void *)h->u.traditional.path);
57 +
58 + if (cb_data_free)
59 + cb_data_free(h->feed_pipe_cb_data);
60 +
61 + free(h);
62 +}
63 +
64 +static void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free)
65 +{
66 + struct string_list_item *item;
67 +
68 + for_each_string_list_item(item, hooks)
69 + hook_clear(item->util, cb_data_free);
70 +
71 + string_list_clear(hooks, 0);
72 +}
73 +
74 +/* Helper to detect and add default "traditional" hooks from the hookdir. */
75 +static void list_hooks_add_default(struct repository *r, const char *hookname,
76 + struct string_list *hook_list,
77 + struct run_hooks_opt *options)
78 +{
79 + const char *hook_path = find_hook(r, hookname);
80 + struct hook *h;
81 +
82 + if (!hook_path)
83 + return;
84 +
85 + h = xcalloc(1, sizeof(struct hook));
86 +
87 + /*
88 + * If the hook is to run in a specific dir, a relative path can
89 + * become invalid in that dir, so convert to an absolute path.
90 + */
91 + if (options && options->dir)
92 + hook_path = absolute_path(hook_path);
93 +
94 + /* Setup per-hook internal state cb data */
95 + if (options && options->feed_pipe_cb_data_alloc)
96 + h->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);
97 +
98 + h->kind = HOOK_TRADITIONAL;
99 + h->u.traditional.path = xstrdup(hook_path);
100 +
101 + string_list_append(hook_list, hook_path)->util = h;
102 +}
103 +
104 +/*
105 + * Provides a list of hook commands to run for the 'hookname' event.
106 + *
107 + * This function consolidates hooks from two sources:
108 + * 1. The config-based hooks (not yet implemented).
109 + * 2. The "traditional" hook found in the repository hooks directory
110 + * (e.g., .git/hooks/pre-commit).
111 + *
112 + * The list is ordered by execution priority.
113 + *
114 + * The caller is responsible for freeing the memory of the returned list
115 + * using string_list_clear() and free().
116 + */
117 +static struct string_list *list_hooks(struct repository *r, const char *hookname,
118 + struct run_hooks_opt *options)
119 +{
120 + struct string_list *hook_head;
121 +
122 + if (!hookname)
123 + BUG("null hookname was provided to hook_list()!");
124 +
125 + hook_head = xmalloc(sizeof(struct string_list));
126 + string_list_init_dup(hook_head);
127 +
128 + /* Add the default "traditional" hooks from hookdir. */
129 + list_hooks_add_default(r, hookname, hook_head, options);
130 +
131 + return hook_head;
132 +}
133 +
134 int hook_exists(struct repository *r, const char *name)
135 {
52 - return !!find_hook(r, name);
136 + struct string_list *hooks = list_hooks(r, name, NULL);
137 + int exists = hooks->nr > 0;
138 + hook_list_clear(hooks, NULL);
139 + free(hooks);
140 + return exists;
141 }
142
143 static int pick_next_hook(struct child_process *cp,
@@ -58,11 +146,14 @@ static int pick_next_hook(struct child_process *cp,
146 void **pp_task_cb)
147 {
148 struct hook_cb_data *hook_cb = pp_cb;
61 - const char *hook_path = hook_cb->hook_path;
149 + struct string_list *hook_list = hook_cb->hook_command_list;
150 + struct hook *h;
151
63 - if (!hook_path)
152 + if (hook_cb->hook_to_run_index >= hook_list->nr)
153 return 0;
154
155 + h = hook_list->items[hook_cb->hook_to_run_index++].util;
156 +
157 cp->no_stdin = 1;
158 strvec_pushv(&cp->env, hook_cb->options->env.v);
159
@@ -85,21 +176,20 @@ static int pick_next_hook(struct child_process *cp,
176 cp->trace2_hook_name = hook_cb->hook_name;
177 cp->dir = hook_cb->options->dir;
178
88 - strvec_push(&cp->args, hook_path);
179 + /* Add hook exec paths or commands */
180 + if (h->kind == HOOK_TRADITIONAL)
181 + strvec_push(&cp->args, h->u.traditional.path);
182 +
183 + if (!cp->args.nr)
184 + BUG("hook must have at least one command or exec path");
185 +
186 strvec_pushv(&cp->args, hook_cb->options->args.v);
187
188 /*
189 * Provide per-hook internal state via task_cb for easy access, so
190 * hook callbacks don't have to go through hook_cb->options.
191 */
95 - *pp_task_cb = hook_cb->options->feed_pipe_cb_data;
96 -
97 - /*
98 - * This pick_next_hook() will be called again, we're only
99 - * running one hook, so indicate that no more work will be
100 - * done.
101 - */
102 - hook_cb->hook_path = NULL;
192 + *pp_task_cb = h->feed_pipe_cb_data;
193
194 return 1;
195 }
@@ -133,8 +223,6 @@ static int notify_hook_finished(int result,
223
224 static void run_hooks_opt_clear(struct run_hooks_opt *options)
225 {
136 - if (options->feed_pipe_cb_data_free)
137 - options->feed_pipe_cb_data_free(options->feed_pipe_cb_data);
226 strvec_clear(&options->env);
227 strvec_clear(&options->args);
228 }
@@ -142,13 +230,11 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
230 int run_hooks_opt(struct repository *r, const char *hook_name,
231 struct run_hooks_opt *options)
232 {
145 - struct strbuf abs_path = STRBUF_INIT;
233 struct hook_cb_data cb_data = {
234 .rc = 0,
235 .hook_name = hook_name,
236 .options = options,
237 };
151 - const char *const hook_path = find_hook(r, hook_name);
238 int ret = 0;
239 const struct run_process_parallel_opts opts = {
240 .tr2_category = "hook",
@@ -182,30 +268,21 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
268 (!options->feed_pipe_cb_data_alloc && options->feed_pipe_cb_data_free))
269 BUG("feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together");
270
185 - if (options->feed_pipe_cb_data_alloc)
186 - options->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);
187 -
271 if (options->invoked_hook)
272 *options->invoked_hook = 0;
273
191 - if (!hook_path && !options->error_if_missing)
192 - goto cleanup;
193 -
194 - if (!hook_path) {
195 - ret = error("cannot find a hook named %s", hook_name);
274 + cb_data.hook_command_list = list_hooks(r, hook_name, options);
275 + if (!cb_data.hook_command_list->nr) {
276 + if (options->error_if_missing)
277 + ret = error("cannot find a hook named %s", hook_name);
278 goto cleanup;
279 }
280
199 - cb_data.hook_path = hook_path;
200 - if (options->dir) {
201 - strbuf_add_absolute_path(&abs_path, hook_path);
202 - cb_data.hook_path = abs_path.buf;
203 - }
204 -
281 run_processes_parallel(&opts);
282 ret = cb_data.rc;
283 cleanup:
208 - strbuf_release(&abs_path);
284 + hook_list_clear(cb_data.hook_command_list, options->feed_pipe_cb_data_free);
285 + free(cb_data.hook_command_list);
286 run_hooks_opt_clear(options);
287 return ret;
288 }
hook.h
+45 -14
@@ -2,9 +2,41 @@
2 #define HOOK_H
3 #include "strvec.h"
4 #include "run-command.h"
5 +#include "string-list.h"
6
7 struct repository;
8
9 +/**
10 + * Represents a hook command to be run.
11 + * Hooks can be:
12 + * 1. "traditional" (found in the hooks directory)
13 + * 2. "configured" (defined in Git's configuration, not yet implemented).
14 + * The 'kind' field determines which part of the union 'u' is valid.
15 + */
16 +struct hook {
17 + enum {
18 + HOOK_TRADITIONAL,
19 + } kind;
20 + union {
21 + struct {
22 + const char *path;
23 + } traditional;
24 + } u;
25 +
26 + /**
27 + * Opaque data pointer used to keep internal state across callback calls.
28 + *
29 + * It can be accessed directly via the third hook callback arg:
30 + * struct ... *state = pp_task_cb;
31 + *
32 + * The caller is responsible for managing the memory for this data by
33 + * providing alloc/free callbacks to `run_hooks_opt`.
34 + *
35 + * Only useful when using `run_hooks_opt.feed_pipe`, otherwise ignore it.
36 + */
37 + void *feed_pipe_cb_data;
38 +};
39 +
40 typedef void (*cb_data_free_fn)(void *data);
41 typedef void *(*cb_data_alloc_fn)(void *init_ctx);
42
@@ -85,19 +117,6 @@ struct run_hooks_opt
117 */
118 void *feed_pipe_ctx;
119
88 - /**
89 - * Opaque data pointer used to keep internal state across callback calls.
90 - *
91 - * It can be accessed directly via the third callback arg 'pp_task_cb':
92 - * struct ... *state = pp_task_cb;
93 - *
94 - * The caller is responsible for managing the memory for this data by
95 - * providing alloc/free callbacks to `run_hooks_opt`.
96 - *
97 - * Only useful when using `run_hooks_opt.feed_pipe`, otherwise ignore it.
98 - */
99 - void *feed_pipe_cb_data;
100 -
120 /**
121 * Some hooks need to create a fresh `feed_pipe_cb_data` internal state,
122 * so they can keep track of progress without affecting one another.
@@ -128,7 +147,19 @@ struct hook_cb_data {
147 /* rc reflects the cumulative failure state */
148 int rc;
149 const char *hook_name;
131 - const char *hook_path;
150 +
151 + /**
152 + * A list of hook commands/paths to run for the 'hook_name' event.
153 + *
154 + * The 'string' member of each item holds the path (for traditional hooks)
155 + * or the unique friendly-name for hooks specified in configs.
156 + * The 'util' member of each item points to the corresponding struct hook.
157 + */
158 + struct string_list *hook_command_list;
159 +
160 + /* Iterator/cursor for the above list, pointing to the next hook to run. */
161 + size_t hook_to_run_index;
162 +
163 struct run_hooks_opt *options;
164 };
165