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 committedDec 22, 2021 at 04:59 UTC96e7225b310cb45a9b1198fb7bb1621e638e3329
new file mode 100644index 0000000000..e39b1b5d06--- /dev/null+++ b/Documentation/git-hook.txt@@ -0,0 +1,37 @@+git-hook(1)+===========++NAME+----+git-hook - Run git hooks++SYNOPSIS+--------+[verse]+'git hook' run <hook-name> [-- <hook-args>]++DESCRIPTION+-----------++A command interface to running git hooks (see linkgit:githooks[5]),+for use by other scripted git commands.++SUBCOMMANDS+-----------++run::+ Run the `<hook-name>` hook. See linkgit:githooks[5] for+ supported hook names.++++Any positional arguments to the hook should be passed after a+mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See+linkgit:githooks[5] for arguments hooks might expect (if any).++SEE ALSO+--------+linkgit:githooks[5]++GIT+---+Part of the linkgit:git[1] suite
Documentation/githooks.txt
+4
index b51959ff94..a16e62bc8c 100644--- a/Documentation/githooks.txt+++ b/Documentation/githooks.txt@@ -698,6 +698,10 @@ and "0" meaning they were not. Only one parameter should be set to "1" when the hook runs. The hook running passing "1", "1" should not be possible.+SEE ALSO+--------+linkgit:git-hook[1]+ GIT --- Part of the linkgit:git[1] suite
index 55e1145a4b..a0917cf877 100644--- a/hook.c+++ b/hook.c@@ -1,6 +1,7 @@ #include "cache.h" #include "hook.h" #include "run-command.h"+#include "config.h" const char *find_hook(const char *name) {@@ -40,3 +41,104 @@ int hook_exists(const char *name) { return !!find_hook(name); }++static int pick_next_hook(struct child_process *cp,+ struct strbuf *out,+ void *pp_cb,+ void **pp_task_cb)+{+ struct hook_cb_data *hook_cb = pp_cb;+ const char *hook_path = hook_cb->hook_path;++ if (!hook_path)+ return 0;++ cp->no_stdin = 1;+ strvec_pushv(&cp->env_array, hook_cb->options->env.v);+ cp->stdout_to_stderr = 1;+ cp->trace2_hook_name = hook_cb->hook_name;++ strvec_push(&cp->args, hook_path);+ strvec_pushv(&cp->args, hook_cb->options->args.v);++ /* Provide context for errors if necessary */+ *pp_task_cb = (char *)hook_path;++ /*+ * This pick_next_hook() will be called again, we're only+ * running one hook, so indicate that no more work will be+ * done.+ */+ hook_cb->hook_path = NULL;++ return 1;+}++static int notify_start_failure(struct strbuf *out,+ void *pp_cb,+ void *pp_task_cp)+{+ struct hook_cb_data *hook_cb = pp_cb;+ const char *hook_path = pp_task_cp;++ hook_cb->rc |= 1;++ strbuf_addf(out, _("Couldn't start hook '%s'\n"),+ hook_path);++ return 1;+}++static int notify_hook_finished(int result,+ struct strbuf *out,+ void *pp_cb,+ void *pp_task_cb)+{+ struct hook_cb_data *hook_cb = pp_cb;++ hook_cb->rc |= result;++ 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(const char *hook_name, struct run_hooks_opt *options)+{+ struct hook_cb_data cb_data = {+ .rc = 0,+ .hook_name = hook_name,+ .options = options,+ };+ const char *const hook_path = find_hook(hook_name);+ int jobs = 1;+ int ret = 0;++ if (!options)+ BUG("a struct run_hooks_opt must be provided to run_hooks");++ if (!hook_path && !options->error_if_missing)+ goto cleanup;++ if (!hook_path) {+ ret = error("cannot find a hook named %s", hook_name);+ goto cleanup;+ }++ cb_data.hook_path = hook_path;+ run_processes_parallel_tr2(jobs,+ pick_next_hook,+ notify_start_failure,+ notify_hook_finished,+ &cb_data,+ "hook",+ hook_name);+ ret = cb_data.rc;+cleanup:+ run_hooks_opt_clear(options);+ return ret;+}
hook.h
+35
index 6aa36fc7ff..782385cc23 100644--- a/hook.h+++ b/hook.h@@ -1,5 +1,31 @@ #ifndef HOOK_H #define HOOK_H+#include "strvec.h"++struct run_hooks_opt+{+ /* Environment vars to be set for each hook */+ struct strvec env;++ /* Args to be passed to each hook */+ struct strvec args;++ /* Emit an error if the hook is missing */+ unsigned int error_if_missing:1;+};++#define RUN_HOOKS_OPT_INIT { \+ .env = STRVEC_INIT, \+ .args = STRVEC_INIT, \+}++struct hook_cb_data {+ /* rc reflects the cumulative failure state */+ int rc;+ const char *hook_name;+ const char *hook_path;+ struct run_hooks_opt *options;+}; /* * 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); */ int hook_exists(const char *hookname);+/**+ * Takes a `hook_name`, resolves it to a path with find_hook(), and+ * runs the hook for you with the options specified in "struct+ * run_hooks opt". Will free memory associated with the "struct run_hooks_opt".+ *+ * Returns the status code of the run hook, or a negative value on+ * error().+ */+int run_hooks_opt(const char *hook_name, struct run_hooks_opt *options); #endif
t/t1800-hook.sh
+129
new file mode 100755index 0000000000..3aea1b105f--- /dev/null+++ b/t/t1800-hook.sh@@ -0,0 +1,129 @@+#!/bin/sh++test_description='git-hook command'++TEST_PASSES_SANITIZE_LEAK=true+. ./test-lib.sh++test_expect_success 'git hook usage' '+ test_expect_code 129 git hook &&+ test_expect_code 129 git hook run &&+ test_expect_code 129 git hook run -h &&+ test_expect_code 129 git hook run --unknown 2>err &&+ grep "unknown option" err+'++test_expect_success 'git hook run: nonexistent hook' '+ cat >stderr.expect <<-\EOF &&+ error: cannot find a hook named test-hook+ EOF+ test_expect_code 1 git hook run test-hook 2>stderr.actual &&+ test_cmp stderr.expect stderr.actual+'++test_expect_success 'git hook run: basic' '+ write_script .git/hooks/test-hook <<-EOF &&+ echo Test hook+ EOF++ cat >expect <<-\EOF &&+ Test hook+ EOF+ git hook run test-hook 2>actual &&+ test_cmp expect actual+'++test_expect_success 'git hook run: stdout and stderr both write to our stderr' '+ write_script .git/hooks/test-hook <<-EOF &&+ echo >&1 Will end up on stderr+ echo >&2 Will end up on stderr+ EOF++ cat >stderr.expect <<-\EOF &&+ Will end up on stderr+ Will end up on stderr+ EOF+ git hook run test-hook >stdout.actual 2>stderr.actual &&+ test_cmp stderr.expect stderr.actual &&+ test_must_be_empty stdout.actual+'++test_expect_success 'git hook run: exit codes are passed along' '+ write_script .git/hooks/test-hook <<-EOF &&+ exit 1+ EOF++ test_expect_code 1 git hook run test-hook &&++ write_script .git/hooks/test-hook <<-EOF &&+ exit 2+ EOF++ test_expect_code 2 git hook run test-hook &&++ write_script .git/hooks/test-hook <<-EOF &&+ exit 128+ EOF++ test_expect_code 128 git hook run test-hook &&++ write_script .git/hooks/test-hook <<-EOF &&+ exit 129+ EOF++ test_expect_code 129 git hook run test-hook+'++test_expect_success 'git hook run arg u ments without -- is not allowed' '+ test_expect_code 129 git hook run test-hook arg u ments+'++test_expect_success 'git hook run -- pass arguments' '+ write_script .git/hooks/test-hook <<-\EOF &&+ echo $1+ echo $2+ EOF++ cat >expect <<-EOF &&+ arg+ u ments+ EOF++ git hook run test-hook -- arg "u ments" 2>actual &&+ test_cmp expect actual+'++test_expect_success 'git hook run -- out-of-repo runs excluded' '+ write_script .git/hooks/test-hook <<-EOF &&+ echo Test hook+ EOF++ nongit test_must_fail git hook run test-hook+'++test_expect_success 'git -c core.hooksPath=<PATH> hook run' '+ mkdir my-hooks &&+ write_script my-hooks/test-hook <<-\EOF &&+ echo Hook ran $1 >>actual+ EOF++ cat >expect <<-\EOF &&+ Test hook+ Hook ran one+ Hook ran two+ Hook ran three+ Hook ran four+ EOF++ # Test various ways of specifying the path. See also+ # t1350-config-hooks-path.sh+ >actual &&+ git hook run test-hook -- ignored 2>>actual &&+ git -c core.hooksPath=my-hooks hook run test-hook -- one 2>>actual &&+ git -c core.hooksPath=my-hooks/ hook run test-hook -- two 2>>actual &&+ git -c core.hooksPath="$PWD/my-hooks" hook run test-hook -- three 2>>actual &&+ git -c core.hooksPath="$PWD/my-hooks/" hook run test-hook -- four 2>>actual &&+ test_cmp expect actual+'++test_done