@@ -77,6 +77,7 @@
/git-grep
/git-hash-object
/git-help
+/git-hook
/git-http-backend
/git-http-fetch
/git-http-push
new file mode 100644
@@ -0,0 +1,38 @@
+git-hook(1)
+===========
+
+NAME
+----
+git-hook - run git hooks
+
+SYNOPSIS
+--------
+[verse]
+'git hook' run <hook-name> [-- <hook-args>]
+
+DESCRIPTION
+-----------
+
+This command is an interface to git hooks (see linkgit:githooks[5]).
+Currently it only provides a convenience wrapper for running hooks for
+use by git itself. In the future it might gain other functionality.
+
+SUBCOMMANDS
+-----------
+
+run::
+ Run the `<hook-name>` hook. See linkgit:githooks[5] for
+ the hook names we support.
++
+Any positional arguments to the hook should be passed after an
+optional `--` (or `--end-of-options`, see linkgit:gitcli[7]). The
+arguments (if any) differ by hook name, see linkgit:githooks[5] for
+what those are.
+
+SEE ALSO
+--------
+linkgit:githooks[5]
+
+GIT
+---
+Part of the linkgit:git[1] suite
@@ -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
@@ -1106,6 +1106,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
BUILTIN_OBJS += builtin/grep.o
BUILTIN_OBJS += builtin/hash-object.o
BUILTIN_OBJS += builtin/help.o
+BUILTIN_OBJS += builtin/hook.o
BUILTIN_OBJS += builtin/index-pack.o
BUILTIN_OBJS += builtin/init-db.o
BUILTIN_OBJS += builtin/interpret-trailers.o
@@ -164,6 +164,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix);
int cmd_grep(int argc, const char **argv, const char *prefix);
int cmd_hash_object(int argc, const char **argv, const char *prefix);
int cmd_help(int argc, const char **argv, const char *prefix);
+int cmd_hook(int argc, const char **argv, const char *prefix);
int cmd_index_pack(int argc, const char **argv, const char *prefix);
int cmd_init_db(int argc, const char **argv, const char *prefix);
int cmd_interpret_trailers(int argc, const char **argv, const char *prefix);
new file mode 100644
@@ -0,0 +1,81 @@
+#include "cache.h"
+#include "builtin.h"
+#include "config.h"
+#include "hook.h"
+#include "parse-options.h"
+#include "strbuf.h"
+#include "strvec.h"
+
+static const char * const builtin_hook_usage[] = {
+ N_("git hook <command> [...]"),
+ N_("git hook run <hook-name> [-- <hook-args>]"),
+ NULL
+};
+
+static const char * const builtin_hook_run_usage[] = {
+ N_("git hook run <hook-name> [-- <hook-args>]"),
+ NULL
+};
+
+static int run(int argc, const char **argv, const char *prefix)
+{
+ int i;
+ struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+ int rc = 0;
+ const char *hook_name;
+ const char *hook_path;
+
+ struct option run_options[] = {
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, run_options,
+ builtin_hook_run_usage,
+ PARSE_OPT_KEEP_UNKNOWN | PARSE_OPT_KEEP_DASHDASH);
+
+ if (argc > 1) {
+ if (strcmp(argv[1], "--") &&
+ strcmp(argv[1], "--end-of-options"))
+ /* Having a -- for "run" is mandatory */
+ usage_with_options(builtin_hook_usage, run_options);
+ /* Add our arguments, start after -- */
+ for (i = 2 ; i < argc; i++)
+ strvec_push(&opt.args, argv[i]);
+ }
+
+ /* Need to take into account core.hooksPath */
+ git_config(git_default_config, NULL);
+
+ /*
+ * We are not using run_hooks() because we'd like to detect
+ * missing hooks. Let's find it ourselves and call
+ * run_found_hooks() instead.
+ */
+ hook_name = argv[0];
+ hook_path = find_hook(hook_name);
+ if (!hook_path) {
+ error("cannot find a hook named %s", hook_name);
+ return 1;
+ }
+ rc = run_found_hooks(hook_name, hook_path, &opt);
+
+ run_hooks_opt_clear(&opt);
+
+ return rc;
+}
+
+int cmd_hook(int argc, const char **argv, const char *prefix)
+{
+ struct option builtin_hook_options[] = {
+ OPT_END(),
+ };
+ argc = parse_options(argc, argv, NULL, builtin_hook_options,
+ builtin_hook_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+ if (!argc)
+ usage_with_options(builtin_hook_usage, builtin_hook_options);
+
+ if (!strcmp(argv[0], "run"))
+ return run(argc, argv, prefix);
+ else
+ usage_with_options(builtin_hook_usage, builtin_hook_options);
+}
@@ -103,6 +103,7 @@ git-grep mainporcelain info
git-gui mainporcelain
git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete
+git-hook mainporcelain
git-http-backend synchingrepositories
git-http-fetch synchelpers
git-http-push synchelpers
@@ -538,6 +538,7 @@ static struct cmd_struct commands[] = {
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
+ { "hook", cmd_hook, RUN_SETUP },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
{ "init-db", cmd_init_db },
@@ -2,11 +2,14 @@
#include "hook.h"
#include "run-command.h"
#include "hook-list.h"
+#include "config.h"
static int known_hook(const char *name)
{
const char **p;
size_t len = strlen(name);
+ static int test_hooks_ok = -1;
+
for (p = hook_name_list; *p; p++) {
const char *hook = *p;
@@ -14,6 +17,14 @@ static int known_hook(const char *name)
return 1;
}
+ if (test_hooks_ok == -1)
+ test_hooks_ok = git_env_bool("GIT_TEST_FAKE_HOOKS", 0);
+
+ if (test_hooks_ok &&
+ (!strcmp(name, "test-hook") ||
+ !strcmp(name, "does-not-exist")))
+ return 1;
+
return 0;
}
@@ -59,3 +70,111 @@ int hook_exists(const char *name)
{
return !!find_hook(name);
}
+
+void run_hooks_opt_clear(struct run_hooks_opt *o)
+{
+ strvec_clear(&o->env);
+ strvec_clear(&o->args);
+}
+
+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;
+ struct hook *run_me = hook_cb->run_me;
+
+ cp->no_stdin = 1;
+ cp->env = hook_cb->options->env.v;
+ cp->stdout_to_stderr = 1;
+ cp->trace2_hook_name = hook_cb->hook_name;
+
+ /* add command */
+ strvec_push(&cp->args, run_me->hook_path);
+
+ /*
+ * add passed-in argv, without expanding - let the user get back
+ * exactly what they put in
+ */
+ strvec_pushv(&cp->args, hook_cb->options->args.v);
+
+ /* Provide context for errors if necessary */
+ *pp_task_cb = run_me;
+
+ 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;
+ struct hook *attempted = pp_task_cp;
+
+ hook_cb->rc |= 1;
+
+ strbuf_addf(out, _("Couldn't start hook '%s'\n"),
+ attempted->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 1;
+}
+
+int run_found_hooks(const char *hook_name, const char *hook_path,
+ struct run_hooks_opt *options)
+{
+ struct hook my_hook = {
+ .hook_path = hook_path,
+ };
+ struct hook_cb_data cb_data = {
+ .rc = 0,
+ .hook_name = hook_name,
+ .options = options,
+ };
+ cb_data.run_me = &my_hook;
+
+ if (options->jobs != 1)
+ BUG("we do not handle %d or any other != 1 job number yet", options->jobs);
+
+ run_processes_parallel_tr2(options->jobs,
+ pick_next_hook,
+ notify_start_failure,
+ notify_hook_finished,
+ &cb_data,
+ "hook",
+ hook_name);
+
+ return cb_data.rc;
+}
+
+int run_hooks(const char *hook_name, struct run_hooks_opt *options)
+{
+ const char *hook_path;
+ int ret;
+ if (!options)
+ BUG("a struct run_hooks_opt must be provided to run_hooks");
+
+ hook_path = find_hook(hook_name);
+
+ /*
+ * If you need to act on a missing hook, use run_found_hooks()
+ * instead
+ */
+ if (!hook_path)
+ return 0;
+
+ ret = run_found_hooks(hook_name, hook_path, options);
+ return ret;
+}
@@ -1,5 +1,8 @@
#ifndef HOOK_H
#define HOOK_H
+#include "strbuf.h"
+#include "strvec.h"
+#include "run-command.h"
/*
* Returns the path to the hook file, or NULL if the hook is missing
@@ -13,4 +16,54 @@ const char *find_hook(const char *name);
*/
int hook_exists(const char *hookname);
+struct hook {
+ /* The path to the hook */
+ const char *hook_path;
+};
+
+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;
+
+ /*
+ * Number of threads to parallelize across, currently a stub,
+ * we use the parallel API for future-proofing, but we always
+ * have one hook of a given name, so this is always an
+ * implicit 1 for now.
+ */
+ int jobs;
+};
+
+#define RUN_HOOKS_OPT_INIT { \
+ .jobs = 1, \
+ .env = STRVEC_INIT, \
+ .args = STRVEC_INIT, \
+}
+
+struct hook_cb_data {
+ /* rc reflects the cumulative failure state */
+ int rc;
+ const char *hook_name;
+ struct hook *run_me;
+ struct run_hooks_opt *options;
+};
+
+void run_hooks_opt_clear(struct run_hooks_opt *o);
+
+/*
+ * Calls find_hook(hookname) and runs the hooks (if any) with
+ * run_found_hooks().
+ */
+int run_hooks(const char *hook_name, struct run_hooks_opt *options);
+
+/*
+ * Takes an already resolved hook and runs it. Internally the simpler
+ * run_hooks() will call this.
+ */
+int run_found_hooks(const char *hookname, const char *hook_path,
+ struct run_hooks_opt *options);
#endif
new file mode 100755
@@ -0,0 +1,131 @@
+#!/bin/bash
+
+test_description='git-hook command'
+
+. ./test-lib.sh
+
+test_expect_success 'git hook usage' '
+ test_expect_code 129 git hook &&
+ test_expect_code 129 git hook -h &&
+ test_expect_code 129 git hook run -h
+'
+
+test_expect_success 'setup GIT_TEST_FAKE_HOOKS=true to permit "test-hook" and "does-not-exist" names"' '
+ GIT_TEST_FAKE_HOOKS=true &&
+ export GIT_TEST_FAKE_HOOKS
+'
+
+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