hook: include hooks from the config

Teach the hook.[hc] library to parse configs to populate the list of hooks to run for a given event. Multiple commands can be specified for a given hook by providing "hook.<friendly-name>.command = <path-to-hook>" and "hook.<friendly-name>.event = <hook-event>" lines. Hooks will be started in config order of the "hook.<name>.event" lines and will be run sequentially (.jobs == 1) like before. Running the hooks in parallel will be enabled in a future patch. The "traditional" hook from the hookdir is run last, if present. A strmap cache is added to struct repository to avoid re-reading the configs on each rook run. This is useful for hooks like the ref-transaction which gets executed multiple times per process. Examples: $ git config --get-regexp "^hook\." hook.bar.command=~/bar.sh hook.bar.event=pre-commit # Will run ~/bar.sh, then .git/hooks/pre-commit $ git hook run pre-commit 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>

Adrian Ratiu committed Feb 19, 2026 at 00:23 UTC 03b4043b9182bd3d36541371fa39f04d6d038286
8 files changed +513 -5
Documentation/config/hook.adoc new
+15
@@ -0,0 +1,15 @@
1 +hook.<name>.command::
2 + The command to execute for `hook.<name>`. `<name>` is a unique
3 + "friendly" name that identifies this hook. (The hook events that
4 + trigger the command are configured with `hook.<name>.event`.) The
5 + value can be an executable path or a shell oneliner. If more than
6 + one value is specified for the same `<name>`, only the last value
7 + parsed is used. See linkgit:git-hook[1].
8 +
9 +hook.<name>.event::
10 + The hook events that trigger `hook.<name>`. The value is the name
11 + of a hook event, like "pre-commit" or "update". (See
12 + linkgit:githooks[5] for a complete list of hook events.) On the
13 + specified event, the associated `hook.<name>.command` is executed.
14 + This is a multi-valued key. To run `hook.<name>` on multiple
15 + events, specify the key more than once. See linkgit:git-hook[1].
Documentation/git-hook.adoc
+126 -2
@@ -17,12 +17,96 @@ DESCRIPTION
17 A command interface for running git hooks (see linkgit:githooks[5]),
18 for use by other scripted git commands.
19
20 +This command parses the default configuration files for sets of configs like
21 +so:
22 +
23 + [hook "linter"]
24 + event = pre-commit
25 + command = ~/bin/linter --cpp20
26 +
27 +In this example, `[hook "linter"]` represents one script - `~/bin/linter
28 +--cpp20` - which can be shared by many repos, and even by many hook events, if
29 +appropriate.
30 +
31 +To add an unrelated hook which runs on a different event, for example a
32 +spell-checker for your commit messages, you would write a configuration like so:
33 +
34 + [hook "linter"]
35 + event = pre-commit
36 + command = ~/bin/linter --cpp20
37 + [hook "spellcheck"]
38 + event = commit-msg
39 + command = ~/bin/spellchecker
40 +
41 +With this config, when you run 'git commit', first `~/bin/linter --cpp20` will
42 +have a chance to check your files to be committed (during the `pre-commit` hook
43 +event`), and then `~/bin/spellchecker` will have a chance to check your commit
44 +message (during the `commit-msg` hook event).
45 +
46 +Commands are run in the order Git encounters their associated
47 +`hook.<name>.event` configs during the configuration parse (see
48 +linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be
49 +added, only one `hook.linter.command` event is valid - Git uses "last-one-wins"
50 +to determine which command to run.
51 +
52 +So if you wanted your linter to run when you commit as well as when you push,
53 +you would configure it like so:
54 +
55 + [hook "linter"]
56 + event = pre-commit
57 + event = pre-push
58 + command = ~/bin/linter --cpp20
59 +
60 +With this config, `~/bin/linter --cpp20` would be run by Git before a commit is
61 +generated (during `pre-commit`) as well as before a push is performed (during
62 +`pre-push`).
63 +
64 +And if you wanted to run your linter as well as a secret-leak detector during
65 +only the "pre-commit" hook event, you would configure it instead like so:
66 +
67 + [hook "linter"]
68 + event = pre-commit
69 + command = ~/bin/linter --cpp20
70 + [hook "no-leaks"]
71 + event = pre-commit
72 + command = ~/bin/leak-detector
73 +
74 +With this config, before a commit is generated (during `pre-commit`), Git would
75 +first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It
76 +would evaluate the output of each when deciding whether to proceed with the
77 +commit.
78 +
79 +For a full list of hook events which you can set your `hook.<name>.event` to,
80 +and how hooks are invoked during those events, see linkgit:githooks[5].
81 +
82 +Git will ignore any `hook.<name>.event` that specifies an event it doesn't
83 +recognize. This is intended so that tools which wrap Git can use the hook
84 +infrastructure to run their own hooks; see "WRAPPERS" for more guidance.
85 +
86 +In general, when instructions suggest adding a script to
87 +`.git/hooks/<hook-event>`, you can specify it in the config instead by running:
88 +
89 +----
90 +git config set hook.<some-name>.command <path-to-script>
91 +git config set --append hook.<some-name>.event <hook-event>
92 +----
93 +
94 +This way you can share the script between multiple repos. That is, `cp
95 +~/my-script.sh ~/project/.git/hooks/pre-commit` would become:
96 +
97 +----
98 +git config set hook.my-script.command ~/my-script.sh
99 +git config set --append hook.my-script.event pre-commit
100 +----
101 +
102 SUBCOMMANDS
103 -----------
104
105 run::
24 - Run the `<hook-name>` hook. See linkgit:githooks[5] for
25 - supported hook names.
106 + Runs hooks configured for `<hook-name>`, in the order they are
107 + discovered during the config parse. The default `<hook-name>` from
108 + the hookdir is run last. See linkgit:githooks[5] for supported
109 + hook names.
110 +
111
112 Any positional arguments to the hook should be passed after a
@@ -46,6 +130,46 @@ OPTIONS
130 tools that want to do a blind one-shot run of a hook that may
131 or may not be present.
132
133 +WRAPPERS
134 +--------
135 +
136 +`git hook run` has been designed to make it easy for tools which wrap Git to
137 +configure and execute hooks using the Git hook infrastructure. It is possible to
138 +provide arguments and stdin via the command line, as well as specifying parallel
139 +or series execution if the user has provided multiple hooks.
140 +
141 +Assuming your wrapper wants to support a hook named "mywrapper-start-tests", you
142 +can have your users specify their hooks like so:
143 +
144 + [hook "setup-test-dashboard"]
145 + event = mywrapper-start-tests
146 + command = ~/mywrapper/setup-dashboard.py --tap
147 +
148 +Then, in your 'mywrapper' tool, you can invoke any users' configured hooks by
149 +running:
150 +
151 +----
152 +git hook run mywrapper-start-tests \
153 + # providing something to stdin
154 + --stdin some-tempfile-123 \
155 + # execute hooks in serial
156 + # plus some arguments of your own...
157 + -- \
158 + --testname bar \
159 + baz
160 +----
161 +
162 +Take care to name your wrapper's hook events in a way which is unlikely to
163 +overlap with Git's native hooks (see linkgit:githooks[5]) - a hook event named
164 +`mywrappertool-validate-commit` is much less likely to be added to native Git
165 +than a hook event named `validate-commit`. If Git begins to use a hook event
166 +named the same thing as your wrapper hook, it may invoke your users' hooks in
167 +unintended and unsupported ways.
168 +
169 +CONFIGURATION
170 +-------------
171 +include::config/hook.adoc[]
172 +
173 SEE ALSO
174 --------
175 linkgit:githooks[5]
builtin/hook.c
+3
@@ -68,6 +68,9 @@ static int list(int argc, const char **argv, const char *prefix,
68 case HOOK_TRADITIONAL:
69 printf("%s\n", _("hook from hookdir"));
70 break;
71 + case HOOK_CONFIGURED:
72 + printf("%s\n", h->u.configured.friendly_name);
73 + break;
74 default:
75 BUG("unknown hook kind");
76 }
hook.c
+196 -1
@@ -4,9 +4,11 @@
4 #include "gettext.h"
5 #include "hook.h"
6 #include "path.h"
7 +#include "parse.h"
8 #include "run-command.h"
9 #include "config.h"
10 #include "strbuf.h"
11 +#include "strmap.h"
12 #include "environment.h"
13 #include "setup.h"
14
@@ -54,6 +56,10 @@ static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)
56
57 if (h->kind == HOOK_TRADITIONAL)
58 free((void *)h->u.traditional.path);
59 + else if (h->kind == HOOK_CONFIGURED) {
60 + free((void *)h->u.configured.friendly_name);
61 + free((void *)h->u.configured.command);
62 + }
63
64 if (cb_data_free)
65 cb_data_free(h->feed_pipe_cb_data);
@@ -101,6 +107,187 @@ static void list_hooks_add_default(struct repository *r, const char *hookname,
107 string_list_append(hook_list, hook_path)->util = h;
108 }
109
110 +static void unsorted_string_list_remove(struct string_list *list,
111 + const char *str)
112 +{
113 + struct string_list_item *item = unsorted_string_list_lookup(list, str);
114 + if (item)
115 + unsorted_string_list_delete_item(list, item - list->items, 0);
116 +}
117 +
118 +/*
119 + * Callback struct to collect all hook.* keys in a single config pass.
120 + * commands: friendly-name to command map.
121 + * event_hooks: event-name to list of friendly-names map.
122 + * disabled_hooks: set of friendly-names with hook.name.enabled = false.
123 + */
124 +struct hook_all_config_cb {
125 + struct strmap commands;
126 + struct strmap event_hooks;
127 + struct string_list disabled_hooks;
128 +};
129 +
130 +/* repo_config() callback that collects all hook.* configuration in one pass. */
131 +static int hook_config_lookup_all(const char *key, const char *value,
132 + const struct config_context *ctx UNUSED,
133 + void *cb_data)
134 +{
135 + struct hook_all_config_cb *data = cb_data;
136 + const char *name, *subkey;
137 + char *hook_name;
138 + size_t name_len = 0;
139 +
140 + if (parse_config_key(key, "hook", &name, &name_len, &subkey))
141 + return 0;
142 +
143 + if (!value)
144 + return config_error_nonbool(key);
145 +
146 + /* Extract name, ensuring it is null-terminated. */
147 + hook_name = xmemdupz(name, name_len);
148 +
149 + if (!strcmp(subkey, "event")) {
150 + struct string_list *hooks =
151 + strmap_get(&data->event_hooks, value);
152 +
153 + if (!hooks) {
154 + hooks = xcalloc(1, sizeof(*hooks));
155 + string_list_init_dup(hooks);
156 + strmap_put(&data->event_hooks, value, hooks);
157 + }
158 +
159 + /* Re-insert if necessary to preserve last-seen order. */
160 + unsorted_string_list_remove(hooks, hook_name);
161 + string_list_append(hooks, hook_name);
162 + } else if (!strcmp(subkey, "command")) {
163 + /* Store command overwriting the old value */
164 + char *old = strmap_put(&data->commands, hook_name,
165 + xstrdup(value));
166 + free(old);
167 + }
168 +
169 + free(hook_name);
170 + return 0;
171 +}
172 +
173 +/*
174 + * The hook config cache maps each hook event name to a string_list where
175 + * every item's string is the hook's friendly-name and its util pointer is
176 + * the corresponding command string. Both strings are owned by the map.
177 + *
178 + * Disabled hooks and hooks missing a command are already filtered out at
179 + * parse time, so callers can iterate the list directly.
180 + */
181 +void hook_cache_clear(struct strmap *cache)
182 +{
183 + struct hashmap_iter iter;
184 + struct strmap_entry *e;
185 +
186 + strmap_for_each_entry(cache, &iter, e) {
187 + struct string_list *hooks = e->value;
188 + string_list_clear(hooks, 1); /* free util (command) pointers */
189 + free(hooks);
190 + }
191 + strmap_clear(cache, 0);
192 +}
193 +
194 +/* Populate `cache` with the complete hook configuration */
195 +static void build_hook_config_map(struct repository *r, struct strmap *cache)
196 +{
197 + struct hook_all_config_cb cb_data;
198 + struct hashmap_iter iter;
199 + struct strmap_entry *e;
200 +
201 + strmap_init(&cb_data.commands);
202 + strmap_init(&cb_data.event_hooks);
203 + string_list_init_dup(&cb_data.disabled_hooks);
204 +
205 + /* Parse all configs in one run. */
206 + repo_config(r, hook_config_lookup_all, &cb_data);
207 +
208 + /* Construct the cache from parsed configs. */
209 + strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
210 + struct string_list *hook_names = e->value;
211 + struct string_list *hooks = xcalloc(1, sizeof(*hooks));
212 +
213 + string_list_init_dup(hooks);
214 +
215 + for (size_t i = 0; i < hook_names->nr; i++) {
216 + const char *hname = hook_names->items[i].string;
217 + char *command;
218 +
219 + command = strmap_get(&cb_data.commands, hname);
220 + if (!command)
221 + die(_("'hook.%s.command' must be configured or "
222 + "'hook.%s.event' must be removed;"
223 + " aborting."), hname, hname);
224 +
225 + /* util stores the command; owned by the cache. */
226 + string_list_append(hooks, hname)->util =
227 + xstrdup(command);
228 + }
229 +
230 + strmap_put(cache, e->key, hooks);
231 + }
232 +
233 + strmap_clear(&cb_data.commands, 1);
234 + string_list_clear(&cb_data.disabled_hooks, 0);
235 + strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
236 + string_list_clear(e->value, 0);
237 + free(e->value);
238 + }
239 + strmap_clear(&cb_data.event_hooks, 0);
240 +}
241 +
242 +/* Return the hook config map for `r`, populating it first if needed. */
243 +static struct strmap *get_hook_config_cache(struct repository *r)
244 +{
245 + struct strmap *cache = NULL;
246 +
247 + if (r) {
248 + /*
249 + * For in-repo calls, the map is stored in r->hook_config_cache,
250 + * so repeated invocations don't parse the configs, so allocate
251 + * it just once on the first call.
252 + */
253 + if (!r->hook_config_cache) {
254 + r->hook_config_cache = xcalloc(1, sizeof(*cache));
255 + strmap_init(r->hook_config_cache);
256 + build_hook_config_map(r, r->hook_config_cache);
257 + }
258 + cache = r->hook_config_cache;
259 + }
260 +
261 + return cache;
262 +}
263 +
264 +static void list_hooks_add_configured(struct repository *r,
265 + const char *hookname,
266 + struct string_list *list,
267 + struct run_hooks_opt *options)
268 +{
269 + struct strmap *cache = get_hook_config_cache(r);
270 + struct string_list *configured_hooks = strmap_get(cache, hookname);
271 +
272 + /* Iterate through configured hooks and initialize internal states */
273 + for (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {
274 + const char *friendly_name = configured_hooks->items[i].string;
275 + const char *command = configured_hooks->items[i].util;
276 + struct hook *hook = xcalloc(1, sizeof(struct hook));
277 +
278 + if (options && options->feed_pipe_cb_data_alloc)
279 + hook->feed_pipe_cb_data =
280 + options->feed_pipe_cb_data_alloc(
281 + options->feed_pipe_ctx);
282 +
283 + hook->kind = HOOK_CONFIGURED;
284 + hook->u.configured.friendly_name = xstrdup(friendly_name);
285 + hook->u.configured.command = xstrdup(command);
286 +
287 + string_list_append(list, friendly_name)->util = hook;
288 + }
289 +}
290 +
291 struct string_list *list_hooks(struct repository *r, const char *hookname,
292 struct run_hooks_opt *options)
293 {
@@ -112,6 +299,9 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,
299 hook_head = xmalloc(sizeof(struct string_list));
300 string_list_init_dup(hook_head);
301
302 + /* Add hooks from the config, e.g. hook.myhook.event = pre-commit */
303 + list_hooks_add_configured(r, hookname, hook_head, options);
304 +
305 /* Add the default "traditional" hooks from hookdir. */
306 list_hooks_add_default(r, hookname, hook_head, options);
307
@@ -164,8 +354,13 @@ static int pick_next_hook(struct child_process *cp,
354 cp->dir = hook_cb->options->dir;
355
356 /* Add hook exec paths or commands */
167 - if (h->kind == HOOK_TRADITIONAL)
357 + if (h->kind == HOOK_TRADITIONAL) {
358 strvec_push(&cp->args, h->u.traditional.path);
359 + } else if (h->kind == HOOK_CONFIGURED) {
360 + /* to enable oneliners, let config-specified hooks run in shell. */
361 + cp->use_shell = true;
362 + strvec_push(&cp->args, h->u.configured.command);
363 + }
364
365 if (!cp->args.nr)
366 BUG("hook must have at least one command or exec path");
hook.h
+13 -1
@@ -3,6 +3,7 @@
3 #include "strvec.h"
4 #include "run-command.h"
5 #include "string-list.h"
6 +#include "strmap.h"
7
8 struct repository;
9
@@ -10,17 +11,22 @@ struct repository;
11 * Represents a hook command to be run.
12 * Hooks can be:
13 * 1. "traditional" (found in the hooks directory)
13 - * 2. "configured" (defined in Git's configuration, not yet implemented).
14 + * 2. "configured" (defined in Git's configuration via hook.<name>.event).
15 * The 'kind' field determines which part of the union 'u' is valid.
16 */
17 struct hook {
18 enum {
19 HOOK_TRADITIONAL,
20 + HOOK_CONFIGURED,
21 } kind;
22 union {
23 struct {
24 const char *path;
25 } traditional;
26 + struct {
27 + const char *friendly_name;
28 + const char *command;
29 + } configured;
30 } u;
31
32 /**
@@ -185,6 +191,12 @@ struct string_list *list_hooks(struct repository *r, const char *hookname,
191 */
192 void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free);
193
194 +/**
195 + * Frees the hook configuration cache stored in `struct repository`.
196 + * Called by repo_clear().
197 + */
198 +void hook_cache_clear(struct strmap *cache);
199 +
200 /**
201 * Returns the path to the hook file, or NULL if the hook is missing
202 * or disabled. Note that this points to static storage that will be
repository.c
+6
@@ -1,6 +1,7 @@
1 #include "git-compat-util.h"
2 #include "abspath.h"
3 #include "repository.h"
4 +#include "hook.h"
5 #include "odb.h"
6 #include "config.h"
7 #include "object.h"
@@ -393,6 +394,11 @@ void repo_clear(struct repository *repo)
394 FREE_AND_NULL(repo->index);
395 }
396
397 + if (repo->hook_config_cache) {
398 + hook_cache_clear(repo->hook_config_cache);
399 + FREE_AND_NULL(repo->hook_config_cache);
400 + }
401 +
402 if (repo->promisor_remote_config) {
403 promisor_remote_clear(repo->promisor_remote_config);
404 FREE_AND_NULL(repo->promisor_remote_config);
repository.h
+6
@@ -157,6 +157,12 @@ struct repository {
157 /* True if commit-graph has been disabled within this process. */
158 int commit_graph_disabled;
159
160 + /*
161 + * Lazily-populated cache mapping hook event names to configured hooks.
162 + * NULL until first hook use.
163 + */
164 + struct strmap *hook_config_cache;
165 +
166 /* Configurations related to promisor remotes. */
167 char *repository_format_partial_clone;
168 struct promisor_remote_config *promisor_remote_config;
t/t1800-hook.sh
+148 -1
@@ -1,14 +1,31 @@
1 #!/bin/sh
2
3 -test_description='git-hook command'
3 +test_description='git-hook command and config-managed multihooks'
4
5 . ./test-lib.sh
6 . "$TEST_DIRECTORY"/lib-terminal.sh
7
8 +setup_hooks () {
9 + test_config hook.ghi.command "/path/ghi"
10 + test_config hook.ghi.event pre-commit --add
11 + test_config hook.ghi.event test-hook --add
12 + test_config_global hook.def.command "/path/def"
13 + test_config_global hook.def.event pre-commit --add
14 +}
15 +
16 +setup_hookdir () {
17 + mkdir .git/hooks
18 + write_script .git/hooks/pre-commit <<-EOF
19 + echo \"Legacy Hook\"
20 + EOF
21 + test_when_finished rm -rf .git/hooks
22 +}
23 +
24 test_expect_success 'git hook usage' '
25 test_expect_code 129 git hook &&
26 test_expect_code 129 git hook run &&
27 test_expect_code 129 git hook run -h &&
28 + test_expect_code 129 git hook list -h &&
29 test_expect_code 129 git hook run --unknown 2>err &&
30 test_expect_code 129 git hook list &&
31 test_expect_code 129 git hook list -h &&
@@ -35,6 +52,15 @@ test_expect_success 'git hook list: traditional hook from hookdir' '
52 test_cmp expect actual
53 '
54
55 +test_expect_success 'git hook list: configured hook' '
56 + test_config hook.myhook.command "echo Hello" &&
57 + test_config hook.myhook.event test-hook --add &&
58 +
59 + echo "myhook" >expect &&
60 + git hook list test-hook >actual &&
61 + test_cmp expect actual
62 +'
63 +
64 test_expect_success 'git hook run: nonexistent hook' '
65 cat >stderr.expect <<-\EOF &&
66 error: cannot find a hook named test-hook
@@ -172,6 +198,126 @@ test_expect_success TTY 'git commit: stdout and stderr are connected to a TTY' '
198 test_hook_tty commit -m"B.new"
199 '
200
201 +test_expect_success 'git hook list orders by config order' '
202 + setup_hooks &&
203 +
204 + cat >expected <<-\EOF &&
205 + def
206 + ghi
207 + EOF
208 +
209 + git hook list pre-commit >actual &&
210 + test_cmp expected actual
211 +'
212 +
213 +test_expect_success 'git hook list reorders on duplicate event declarations' '
214 + setup_hooks &&
215 +
216 + # 'def' is usually configured globally; move it to the end by
217 + # configuring it locally.
218 + test_config hook.def.event "pre-commit" --add &&
219 +
220 + cat >expected <<-\EOF &&
221 + ghi
222 + def
223 + EOF
224 +
225 + git hook list pre-commit >actual &&
226 + test_cmp expected actual
227 +'
228 +
229 +test_expect_success 'hook can be configured for multiple events' '
230 + setup_hooks &&
231 +
232 + # 'ghi' should be included in both 'pre-commit' and 'test-hook'
233 + git hook list pre-commit >actual &&
234 + grep "ghi" actual &&
235 + git hook list test-hook >actual &&
236 + grep "ghi" actual
237 +'
238 +
239 +test_expect_success 'git hook list shows hooks from the hookdir' '
240 + setup_hookdir &&
241 +
242 + cat >expected <<-\EOF &&
243 + hook from hookdir
244 + EOF
245 +
246 + git hook list pre-commit >actual &&
247 + test_cmp expected actual
248 +'
249 +
250 +test_expect_success 'inline hook definitions execute oneliners' '
251 + test_config hook.oneliner.event "pre-commit" &&
252 + test_config hook.oneliner.command "echo \"Hello World\"" &&
253 +
254 + echo "Hello World" >expected &&
255 +
256 + # hooks are run with stdout_to_stderr = 1
257 + git hook run pre-commit 2>actual &&
258 + test_cmp expected actual
259 +'
260 +
261 +test_expect_success 'inline hook definitions resolve paths' '
262 + write_script sample-hook.sh <<-\EOF &&
263 + echo \"Sample Hook\"
264 + EOF
265 +
266 + test_when_finished "rm sample-hook.sh" &&
267 +
268 + test_config hook.sample-hook.event pre-commit &&
269 + test_config hook.sample-hook.command "\"$(pwd)/sample-hook.sh\"" &&
270 +
271 + echo \"Sample Hook\" >expected &&
272 +
273 + # hooks are run with stdout_to_stderr = 1
274 + git hook run pre-commit 2>actual &&
275 + test_cmp expected actual
276 +'
277 +
278 +test_expect_success 'hookdir hook included in git hook run' '
279 + setup_hookdir &&
280 +
281 + echo \"Legacy Hook\" >expected &&
282 +
283 + # hooks are run with stdout_to_stderr = 1
284 + git hook run pre-commit 2>actual &&
285 + test_cmp expected actual
286 +'
287 +
288 +test_expect_success 'stdin to multiple hooks' '
289 + test_config hook.stdin-a.event "test-hook" &&
290 + test_config hook.stdin-a.command "xargs -P1 -I% echo a%" &&
291 + test_config hook.stdin-b.event "test-hook" &&
292 + test_config hook.stdin-b.command "xargs -P1 -I% echo b%" &&
293 +
294 + cat >input <<-\EOF &&
295 + 1
296 + 2
297 + 3
298 + EOF
299 +
300 + cat >expected <<-\EOF &&
301 + a1
302 + a2
303 + a3
304 + b1
305 + b2
306 + b3
307 + EOF
308 +
309 + git hook run --to-stdin=input test-hook 2>actual &&
310 + test_cmp expected actual
311 +'
312 +
313 +test_expect_success 'rejects hooks with no commands configured' '
314 + test_config hook.broken.event "test-hook" &&
315 + test_must_fail git hook list test-hook 2>actual &&
316 + test_grep "hook.broken.command" actual &&
317 + test_must_fail git hook run test-hook 2>actual &&
318 + test_grep "hook.broken.command" actual
319 +'
320 +
321 test_expect_success 'git hook run a hook with a bad shebang' '
322 test_when_finished "rm -rf bad-hooks" &&
323 mkdir bad-hooks &&
@@ -189,6 +335,7 @@ test_expect_success 'git hook run a hook with a bad shebang' '
335 '
336
337 test_expect_success 'stdin to hooks' '
338 + mkdir -p .git/hooks &&
339 write_script .git/hooks/test-hook <<-\EOF &&
340 echo BEGIN stdin
341 cat