hook: add 'run' subcommand

In order to enable hooks to be run as an external process, by a standalone Git command, or by tools which wrap Git, provide an external means to run all configured hook commands for a given hook event. Most of our hooks require more complex functionality than this, but let's start with the bare minimum required to support our simplest hooks. In terms of implementation the usage_with_options() and "goto usage" pattern here mirrors that of builtin/{commit-graph,multi-pack-index}.c. Some of the implementation here, such as a function being named run_hooks_opt() when it's tasked with running one hook, to using the run_processes_parallel_tr2() API to run with jobs=1 is somewhere between a bit odd and and an overkill for the current features of this "hook run" command and the hook.[ch] API. This code will eventually be able to run multiple hooks declared in config in parallel, by starting out with these names and APIs we reduce the later churn of renaming functions, switching from the run_command() to run_processes_parallel_tr2() API etc. Signed-off-by: Emily Shaffer <emilyshaffer@google.com> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Acked-by: Emily Shaffer <emilyshaffer@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Emily Shaffer committed Dec 22, 2021 at 04:59 UTC 96e7225b310cb45a9b1198fb7bb1621e638e3329
11 files changed +392
.gitignore
+1
@@ -77,6 +77,7 @@
77 /git-grep
78 /git-hash-object
79 /git-help
80 +/git-hook
81 /git-http-backend
82 /git-http-fetch
83 /git-http-push
Documentation/git-hook.txt new
+37
@@ -0,0 +1,37 @@
1 +git-hook(1)
2 +===========
3 +
4 +NAME
5 +----
6 +git-hook - Run git hooks
7 +
8 +SYNOPSIS
9 +--------
10 +[verse]
11 +'git hook' run <hook-name> [-- <hook-args>]
12 +
13 +DESCRIPTION
14 +-----------
15 +
16 +A command interface to running git hooks (see linkgit:githooks[5]),
17 +for use by other scripted git commands.
18 +
19 +SUBCOMMANDS
20 +-----------
21 +
22 +run::
23 + Run the `<hook-name>` hook. See linkgit:githooks[5] for
24 + supported hook names.
25 ++
26 +
27 +Any positional arguments to the hook should be passed after a
28 +mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
29 +linkgit:githooks[5] for arguments hooks might expect (if any).
30 +
31 +SEE ALSO
32 +--------
33 +linkgit:githooks[5]
34 +
35 +GIT
36 +---
37 +Part of the linkgit:git[1] suite
Documentation/githooks.txt
+4
@@ -698,6 +698,10 @@ and "0" meaning they were not.
698 Only one parameter should be set to "1" when the hook runs. The hook
699 running passing "1", "1" should not be possible.
700
701 +SEE ALSO
702 +--------
703 +linkgit:git-hook[1]
704 +
705 GIT
706 ---
707 Part of the linkgit:git[1] suite
Makefile
+1
@@ -1109,6 +1109,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
1109 BUILTIN_OBJS += builtin/grep.o
1110 BUILTIN_OBJS += builtin/hash-object.o
1111 BUILTIN_OBJS += builtin/help.o
1112 +BUILTIN_OBJS += builtin/hook.o
1113 BUILTIN_OBJS += builtin/index-pack.o
1114 BUILTIN_OBJS += builtin/init-db.o
1115 BUILTIN_OBJS += builtin/interpret-trailers.o
builtin.h
+1
@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
164 int cmd_grep(int argc, const char **argv, const char *prefix);
165 int cmd_hash_object(int argc, const char **argv, const char *prefix);
166 int cmd_help(int argc, const char **argv, const char *prefix);
167 +int cmd_hook(int argc, const char **argv, const char *prefix);
168 int cmd_index_pack(int argc, const char **argv, const char *prefix);
169 int cmd_init_db(int argc, const char **argv, const char *prefix);
170 int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);
builtin/hook.c new
+80
@@ -0,0 +1,80 @@
1 +#include "cache.h"
2 +#include "builtin.h"
3 +#include "config.h"
4 +#include "hook.h"
5 +#include "parse-options.h"
6 +#include "strbuf.h"
7 +#include "strvec.h"
8 +
9 +#define BUILTIN_HOOK_RUN_USAGE \
10 + N_("git hook run <hook-name> [-- <hook-args>]")
11 +
12 +static const char * const builtin_hook_usage[] = {
13 + BUILTIN_HOOK_RUN_USAGE,
14 + NULL
15 +};
16 +
17 +static const char * const builtin_hook_run_usage[] = {
18 + BUILTIN_HOOK_RUN_USAGE,
19 + NULL
20 +};
21 +
22 +static int run(int argc, const char **argv, const char *prefix)
23 +{
24 + int i;
25 + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
26 + const char *hook_name;
27 + struct option run_options[] = {
28 + OPT_END(),
29 + };
30 + int ret;
31 +
32 + argc = parse_options(argc, argv, prefix, run_options,
33 + builtin_hook_run_usage,
34 + PARSE_OPT_KEEP_DASHDASH);
35 +
36 + if (!argc)
37 + goto usage;
38 +
39 + /*
40 + * Having a -- for "run" when providing <hook-args> is
41 + * mandatory.
42 + */
43 + if (argc > 1 && strcmp(argv[1], "--") &&
44 + strcmp(argv[1], "--end-of-options"))
45 + goto usage;
46 +
47 + /* Add our arguments, start after -- */
48 + for (i = 2 ; i < argc; i++)
49 + strvec_push(&opt.args, argv[i]);
50 +
51 + /* Need to take into account core.hooksPath */
52 + git_config(git_default_config, NULL);
53 +
54 + hook_name = argv[0];
55 + opt.error_if_missing = 1;
56 + ret = run_hooks_opt(hook_name, &opt);
57 + if (ret < 0) /* error() return */
58 + ret = 1;
59 + return ret;
60 +usage:
61 + usage_with_options(builtin_hook_run_usage, run_options);
62 +}
63 +
64 +int cmd_hook(int argc, const char **argv, const char *prefix)
65 +{
66 + struct option builtin_hook_options[] = {
67 + OPT_END(),
68 + };
69 +
70 + argc = parse_options(argc, argv, NULL, builtin_hook_options,
71 + builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
72 + if (!argc)
73 + goto usage;
74 +
75 + if (!strcmp(argv[0], "run"))
76 + return run(argc, argv, prefix);
77 +
78 +usage:
79 + usage_with_options(builtin_hook_usage, builtin_hook_options);
80 +}
command-list.txt
+1
@@ -103,6 +103,7 @@ git-grep mainporcelain info
103 git-gui mainporcelain
104 git-hash-object plumbingmanipulators
105 git-help ancillaryinterrogators complete
106 +git-hook purehelpers
107 git-http-backend synchingrepositories
108 git-http-fetch synchelpers
109 git-http-push synchelpers
git.c
+1
@@ -541,6 +541,7 @@ static struct cmd_struct commands[] = {
541 { "grep", cmd_grep, RUN_SETUP_GENTLY },
542 { "hash-object", cmd_hash_object },
543 { "help", cmd_help },
544 + { "hook", cmd_hook, RUN_SETUP },
545 { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
546 { "init", cmd_init_db },
547 { "init-db", cmd_init_db },
hook.c
+102
@@ -1,6 +1,7 @@
1 #include "cache.h"
2 #include "hook.h"
3 #include "run-command.h"
4 +#include "config.h"
5
6 const char *find_hook(const char *name)
7 {
@@ -40,3 +41,104 @@ int hook_exists(const char *name)
41 {
42 return !!find_hook(name);
43 }
44 +
45 +static int pick_next_hook(struct child_process *cp,
46 + struct strbuf *out,
47 + void *pp_cb,
48 + void **pp_task_cb)
49 +{
50 + struct hook_cb_data *hook_cb = pp_cb;
51 + const char *hook_path = hook_cb->hook_path;
52 +
53 + if (!hook_path)
54 + return 0;
55 +
56 + cp->no_stdin = 1;
57 + strvec_pushv(&cp->env_array, hook_cb->options->env.v);
58 + cp->stdout_to_stderr = 1;
59 + cp->trace2_hook_name = hook_cb->hook_name;
60 +
61 + strvec_push(&cp->args, hook_path);
62 + strvec_pushv(&cp->args, hook_cb->options->args.v);
63 +
64 + /* Provide context for errors if necessary */
65 + *pp_task_cb = (char *)hook_path;
66 +
67 + /*
68 + * This pick_next_hook() will be called again, we're only
69 + * running one hook, so indicate that no more work will be
70 + * done.
71 + */
72 + hook_cb->hook_path = NULL;
73 +
74 + return 1;
75 +}
76 +
77 +static int notify_start_failure(struct strbuf *out,
78 + void *pp_cb,
79 + void *pp_task_cp)
80 +{
81 + struct hook_cb_data *hook_cb = pp_cb;
82 + const char *hook_path = pp_task_cp;
83 +
84 + hook_cb->rc |= 1;
85 +
86 + strbuf_addf(out, _("Couldn't start hook '%s'\n"),
87 + hook_path);
88 +
89 + return 1;
90 +}
91 +
92 +static int notify_hook_finished(int result,
93 + struct strbuf *out,
94 + void *pp_cb,
95 + void *pp_task_cb)
96 +{
97 + struct hook_cb_data *hook_cb = pp_cb;
98 +
99 + hook_cb->rc |= result;
100 +
101 + return 0;
102 +}
103 +
104 +static void run_hooks_opt_clear(struct run_hooks_opt *options)
105 +{
106 + strvec_clear(&options->env);
107 + strvec_clear(&options->args);
108 +}
109 +
110 +int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options)
111 +{
112 + struct hook_cb_data cb_data = {
113 + .rc = 0,
114 + .hook_name = hook_name,
115 + .options = options,
116 + };
117 + const char *const hook_path = find_hook(hook_name);
118 + int jobs = 1;
119 + int ret = 0;
120 +
121 + if (!options)
122 + BUG("a struct run_hooks_opt must be provided to run_hooks");
123 +
124 + if (!hook_path && !options->error_if_missing)
125 + goto cleanup;
126 +
127 + if (!hook_path) {
128 + ret = error("cannot find a hook named %s", hook_name);
129 + goto cleanup;
130 + }
131 +
132 + cb_data.hook_path = hook_path;
133 + run_processes_parallel_tr2(jobs,
134 + pick_next_hook,
135 + notify_start_failure,
136 + notify_hook_finished,
137 + &cb_data,
138 + "hook",
139 + hook_name);
140 + ret = cb_data.rc;
141 +cleanup:
142 + run_hooks_opt_clear(options);
143 + return ret;
144 +}
hook.h
+35
@@ -1,5 +1,31 @@
1 #ifndef HOOK_H
2 #define HOOK_H
3 +#include "strvec.h"
4 +
5 +struct run_hooks_opt
6 +{
7 + /* Environment vars to be set for each hook */
8 + struct strvec env;
9 +
10 + /* Args to be passed to each hook */
11 + struct strvec args;
12 +
13 + /* Emit an error if the hook is missing */
14 + unsigned int error_if_missing:1;
15 +};
16 +
17 +#define RUN_HOOKS_OPT_INIT { \
18 + .env = STRVEC_INIT, \
19 + .args = STRVEC_INIT, \
20 +}
21 +
22 +struct hook_cb_data {
23 + /* rc reflects the cumulative failure state */
24 + int rc;
25 + const char *hook_name;
26 + const char *hook_path;
27 + struct run_hooks_opt *options;
28 +};
29
30 /*
31 * Returns the path to the hook file, or NULL if the hook is missing
@@ -13,4 +39,13 @@ const char *find_hook(const char *name);
39 */
40 int hook_exists(const char *hookname);
41
42 +/**
43 + * Takes a `hook_name`, resolves it to a path with find_hook(), and
44 + * runs the hook for you with the options specified in "struct
45 + * run_hooks opt". Will free memory associated with the "struct run_hooks_opt".
46 + *
47 + * Returns the status code of the run hook, or a negative value on
48 + * error().
49 + */
50 +int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options);
51 #endif
t/t1800-hook.sh new
+129
@@ -0,0 +1,129 @@
1 +#!/bin/sh
2 +
3 +test_description='git-hook command'
4 +
5 +TEST_PASSES_SANITIZE_LEAK=true
6 +. ./test-lib.sh
7 +
8 +test_expect_success 'git hook usage' '
9 + test_expect_code 129 git hook &&
10 + test_expect_code 129 git hook run &&
11 + test_expect_code 129 git hook run -h &&
12 + test_expect_code 129 git hook run --unknown 2>err &&
13 + grep "unknown option" err
14 +'
15 +
16 +test_expect_success 'git hook run: nonexistent hook' '
17 + cat >stderr.expect <<-\EOF &&
18 + error: cannot find a hook named test-hook
19 + EOF
20 + test_expect_code 1 git hook run test-hook 2>stderr.actual &&
21 + test_cmp stderr.expect stderr.actual
22 +'
23 +
24 +test_expect_success 'git hook run: basic' '
25 + write_script .git/hooks/test-hook <<-EOF &&
26 + echo Test hook
27 + EOF
28 +
29 + cat >expect <<-\EOF &&
30 + Test hook
31 + EOF
32 + git hook run test-hook 2>actual &&
33 + test_cmp expect actual
34 +'
35 +
36 +test_expect_success 'git hook run: stdout and stderr both write to our stderr' '
37 + write_script .git/hooks/test-hook <<-EOF &&
38 + echo >&1 Will end up on stderr
39 + echo >&2 Will end up on stderr
40 + EOF
41 +
42 + cat >stderr.expect <<-\EOF &&
43 + Will end up on stderr
44 + Will end up on stderr
45 + EOF
46 + git hook run test-hook >stdout.actual 2>stderr.actual &&
47 + test_cmp stderr.expect stderr.actual &&
48 + test_must_be_empty stdout.actual
49 +'
50 +
51 +test_expect_success 'git hook run: exit codes are passed along' '
52 + write_script .git/hooks/test-hook <<-EOF &&
53 + exit 1
54 + EOF
55 +
56 + test_expect_code 1 git hook run test-hook &&
57 +
58 + write_script .git/hooks/test-hook <<-EOF &&
59 + exit 2
60 + EOF
61 +
62 + test_expect_code 2 git hook run test-hook &&
63 +
64 + write_script .git/hooks/test-hook <<-EOF &&
65 + exit 128
66 + EOF
67 +
68 + test_expect_code 128 git hook run test-hook &&
69 +
70 + write_script .git/hooks/test-hook <<-EOF &&
71 + exit 129
72 + EOF
73 +
74 + test_expect_code 129 git hook run test-hook
75 +'
76 +
77 +test_expect_success 'git hook run arg u ments without -- is not allowed' '
78 + test_expect_code 129 git hook run test-hook arg u ments
79 +'
80 +
81 +test_expect_success 'git hook run -- pass arguments' '
82 + write_script .git/hooks/test-hook <<-\EOF &&
83 + echo $1
84 + echo $2
85 + EOF
86 +
87 + cat >expect <<-EOF &&
88 + arg
89 + u ments
90 + EOF
91 +
92 + git hook run test-hook -- arg "u ments" 2>actual &&
93 + test_cmp expect actual
94 +'
95 +
96 +test_expect_success 'git hook run -- out-of-repo runs excluded' '
97 + write_script .git/hooks/test-hook <<-EOF &&
98 + echo Test hook
99 + EOF
100 +
101 + nongit test_must_fail git hook run test-hook
102 +'
103 +
104 +test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
105 + mkdir my-hooks &&
106 + write_script my-hooks/test-hook <<-\EOF &&
107 + echo Hook ran $1 >>actual
108 + EOF
109 +
110 + cat >expect <<-\EOF &&
111 + Test hook
112 + Hook ran one
113 + Hook ran two
114 + Hook ran three
115 + Hook ran four
116 + EOF
117 +
118 + # Test various ways of specifying the path. See also
119 + # t1350-config-hooks-path.sh
120 + >actual &&
121 + git hook run test-hook -- ignored 2>>actual &&
122 + git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&
123 + git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&
124 + git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&
125 + git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&
126 + test_cmp expect actual
127 +'
128 +
129 +test_done