Merge branch 'ab/config-based-hooks-2'

More "config-based hooks".

* ab/config-based-hooks-2:
  run-command: remove old run_hook_{le,ve}() hook API
  receive-pack: convert push-to-checkout hook to hook.h
  read-cache: convert post-index-change to use hook.h
  commit: convert {pre-commit,prepare-commit-msg} hook to hook.h
  git-p4: use 'git hook' to run hooks
  send-email: use 'git hook run' for 'sendemail-validate'
  git hook run: add an --ignore-missing flag
  hooks: convert worktree 'post-checkout' hook to hook library
  hooks: convert non-worktree 'post-checkout' hook to hook library
  merge: convert post-merge to use hook.h
  am: convert applypatch-msg to use hook.h
  rebase: convert pre-rebase to use hook.h
  hook API: add a run_hooks_l() wrapper
  am: convert {pre,post}-applypatch to use hook.h
  gc: use hook library for pre-auto-gc hook
  hook API: add a run_hooks() wrapper
  hook: add 'run' subcommand
This commit is contained in:
Junio C Hamano 2022-02-09 14:21:00 -08:00
commit c70bc338e9
27 changed files with 522 additions and 158 deletions

1
.gitignore vendored
View File

@ -77,6 +77,7 @@
/git-grep
/git-hash-object
/git-help
/git-hook
/git-http-backend
/git-http-fetch
/git-http-push

View File

@ -0,0 +1,45 @@
git-hook(1)
===========
NAME
----
git-hook - Run git hooks
SYNOPSIS
--------
[verse]
'git hook' run [--ignore-missing] <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).
OPTIONS
-------
--ignore-missing::
Ignore any missing hook by quietly returning zero. Used for
tools that want to do a blind one-shot run of a hook that may
or may not be present.
SEE ALSO
--------
linkgit:githooks[5]
GIT
---
Part of the linkgit:git[1] suite

View File

@ -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

View File

@ -1109,6 +1109,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

View File

@ -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);

View File

@ -474,7 +474,7 @@ static int run_applypatch_msg_hook(struct am_state *state)
int ret;
assert(state->msg);
ret = run_hook_le(NULL, "applypatch-msg", am_path(state, "final-commit"), NULL);
ret = run_hooks_l("applypatch-msg", am_path(state, "final-commit"), NULL);
if (!ret) {
FREE_AND_NULL(state->msg);
@ -1636,7 +1636,7 @@ static void do_commit(const struct am_state *state)
const char *reflog_msg, *author, *committer = NULL;
struct strbuf sb = STRBUF_INIT;
if (run_hook_le(NULL, "pre-applypatch", NULL))
if (run_hooks("pre-applypatch"))
exit(1);
if (write_cache_as_tree(&tree, 0, NULL))
@ -1688,7 +1688,7 @@ static void do_commit(const struct am_state *state)
fclose(fp);
}
run_hook_le(NULL, "post-applypatch", NULL);
run_hooks("post-applypatch");
strbuf_release(&sb);
}

View File

@ -9,6 +9,7 @@
#include "config.h"
#include "diff.h"
#include "dir.h"
#include "hook.h"
#include "ll-merge.h"
#include "lockfile.h"
#include "merge-recursive.h"
@ -114,7 +115,7 @@ static void branch_info_release(struct branch_info *info)
static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit,
int changed)
{
return run_hook_le(NULL, "post-checkout",
return run_hooks_l("post-checkout",
oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()),
oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()),
changed ? "1" : "0", NULL);

View File

@ -32,6 +32,7 @@
#include "connected.h"
#include "packfile.h"
#include "list-objects-filter-options.h"
#include "hook.h"
/*
* Overall FIXMEs:
@ -705,7 +706,7 @@ static int checkout(int submodule_progress)
if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
die(_("unable to write new index file"));
err |= run_hook_le(NULL, "post-checkout", oid_to_hex(null_oid()),
err |= run_hooks_l("post-checkout", oid_to_hex(null_oid()),
oid_to_hex(&oid), "1", NULL);
if (!err && (option_recurse_submodules.nr > 0)) {

View File

@ -32,6 +32,7 @@
#include "remote.h"
#include "object-store.h"
#include "exec-cmd.h"
#include "hook.h"
#define FAILED_RUN "failed to run %s"
@ -394,7 +395,7 @@ static int need_to_gc(void)
else
return 0;
if (run_hook_le(NULL, "pre-auto-gc", NULL))
if (run_hooks("pre-auto-gc"))
return 0;
return 1;
}

84
builtin/hook.c Normal file
View File

@ -0,0 +1,84 @@
#include "cache.h"
#include "builtin.h"
#include "config.h"
#include "hook.h"
#include "parse-options.h"
#include "strbuf.h"
#include "strvec.h"
#define BUILTIN_HOOK_RUN_USAGE \
N_("git hook run [--ignore-missing] <hook-name> [-- <hook-args>]")
static const char * const builtin_hook_usage[] = {
BUILTIN_HOOK_RUN_USAGE,
NULL
};
static const char * const builtin_hook_run_usage[] = {
BUILTIN_HOOK_RUN_USAGE,
NULL
};
static int run(int argc, const char **argv, const char *prefix)
{
int i;
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
int ignore_missing = 0;
const char *hook_name;
struct option run_options[] = {
OPT_BOOL(0, "ignore-missing", &ignore_missing,
N_("silently ignore missing requested <hook-name>")),
OPT_END(),
};
int ret;
argc = parse_options(argc, argv, prefix, run_options,
builtin_hook_run_usage,
PARSE_OPT_KEEP_DASHDASH);
if (!argc)
goto usage;
/*
* Having a -- for "run" when providing <hook-args> is
* mandatory.
*/
if (argc > 1 && strcmp(argv[1], "--") &&
strcmp(argv[1], "--end-of-options"))
goto usage;
/* 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);
hook_name = argv[0];
if (!ignore_missing)
opt.error_if_missing = 1;
ret = run_hooks_opt(hook_name, &opt);
if (ret < 0) /* error() return */
ret = 1;
return ret;
usage:
usage_with_options(builtin_hook_run_usage, run_options);
}
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)
goto usage;
if (!strcmp(argv[0], "run"))
return run(argc, argv, prefix);
usage:
usage_with_options(builtin_hook_usage, builtin_hook_options);
}

View File

@ -490,7 +490,7 @@ static void finish(struct commit *head_commit,
}
/* Run a post-merge hook */
run_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL);
run_hooks_l("post-merge", squash ? "1" : "0", NULL);
apply_autostash(git_path_merge_autostash(the_repository));
strbuf_release(&reflog_message);

View File

@ -28,6 +28,7 @@
#include "sequencer.h"
#include "rebase-interactive.h"
#include "reset.h"
#include "hook.h"
#define DEFAULT_REFLOG_ACTION "rebase"
@ -1712,7 +1713,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
/* If a hook exists, give it a chance to interrupt*/
if (!ok_to_skip_pre_rebase &&
run_hook_le(NULL, "pre-rebase", options.upstream_arg,
run_hooks_l("pre-rebase", options.upstream_arg,
argc ? argv[0] : NULL, NULL))
die(_("The pre-rebase hook refused to rebase."));

View File

@ -1411,9 +1411,12 @@ static const char *push_to_checkout(unsigned char *hash,
struct strvec *env,
const char *work_tree)
{
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree));
if (run_hook_le(env->v, push_to_checkout_hook,
hash_to_hex(hash), NULL))
strvec_pushv(&opt.env, env->v);
strvec_push(&opt.args, hash_to_hex(hash));
if (run_hooks_opt(push_to_checkout_hook, &opt))
return "push-to-checkout hook declined";
else
return NULL;

View File

@ -382,21 +382,17 @@ done:
* is_junk is cleared, but do return appropriate code when hook fails.
*/
if (!ret && opts->checkout) {
const char *hook = find_hook("post-checkout");
if (hook) {
const char *env[] = { "GIT_DIR", "GIT_WORK_TREE", NULL };
struct child_process cp = CHILD_PROCESS_INIT;
cp.no_stdin = 1;
cp.stdout_to_stderr = 1;
cp.dir = path;
strvec_pushv(&cp.env_array, env);
cp.trace2_hook_name = "post-checkout";
strvec_pushl(&cp.args, absolute_path(hook),
oid_to_hex(null_oid()),
oid_to_hex(&commit->object.oid),
"1", NULL);
ret = run_command(&cp);
}
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
strvec_pushl(&opt.args,
oid_to_hex(null_oid()),
oid_to_hex(&commit->object.oid),
"1",
NULL);
opt.dir = path;
ret = run_hooks_opt("post-checkout", &opt);
}
strvec_clear(&child_env);

View File

@ -103,6 +103,7 @@ git-grep mainporcelain info
git-gui mainporcelain
git-hash-object plumbingmanipulators
git-help ancillaryinterrogators complete
git-hook purehelpers
git-http-backend synchingrepositories
git-http-fetch synchelpers
git-http-push synchelpers

View File

@ -21,6 +21,7 @@
#include "commit-reach.h"
#include "run-command.h"
#include "shallow.h"
#include "hook.h"
static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
@ -1714,22 +1715,22 @@ size_t ignore_non_trailer(const char *buf, size_t len)
int run_commit_hook(int editor_is_used, const char *index_file,
const char *name, ...)
{
struct strvec hook_env = STRVEC_INIT;
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
va_list args;
int ret;
const char *arg;
strvec_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file);
strvec_pushf(&opt.env, "GIT_INDEX_FILE=%s", index_file);
/*
* Let the hook know that no editor will be launched.
*/
if (!editor_is_used)
strvec_push(&hook_env, "GIT_EDITOR=:");
strvec_push(&opt.env, "GIT_EDITOR=:");
va_start(args, name);
ret = run_hook_ve(hook_env.v, name, args);
while ((arg = va_arg(args, const char *)))
strvec_push(&opt.args, arg);
va_end(args);
strvec_clear(&hook_env);
return ret;
return run_hooks_opt(name, &opt);
}

View File

@ -220,70 +220,12 @@ def decode_path(path):
def run_git_hook(cmd, param=[]):
"""Execute a hook if the hook exists."""
if verbose:
sys.stderr.write("Looking for hook: %s\n" % cmd)
sys.stderr.flush()
hooks_path = gitConfig("core.hooksPath")
if len(hooks_path) <= 0:
hooks_path = os.path.join(os.environ["GIT_DIR"], "hooks")
if not isinstance(param, list):
param=[param]
# resolve hook file name, OS depdenent
hook_file = os.path.join(hooks_path, cmd)
if platform.system() == 'Windows':
if not os.path.isfile(hook_file):
# look for the file with an extension
files = glob.glob(hook_file + ".*")
if not files:
return True
files.sort()
hook_file = files.pop()
while hook_file.upper().endswith(".SAMPLE"):
# The file is a sample hook. We don't want it
if len(files) > 0:
hook_file = files.pop()
else:
return True
if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK):
return True
return run_hook_command(hook_file, param) == 0
def run_hook_command(cmd, param):
"""Executes a git hook command
cmd = the command line file to be executed. This can be
a file that is run by OS association.
param = a list of parameters to pass to the cmd command
On windows, the extension is checked to see if it should
be run with the Git for Windows Bash shell. If there
is no file extension, the file is deemed a bash shell
and will be handed off to sh.exe. Otherwise, Windows
will be called with the shell to handle the file assocation.
For non Windows operating systems, the file is called
as an executable.
"""
cli = [cmd] + param
use_shell = False
if platform.system() == 'Windows':
(root,ext) = os.path.splitext(cmd)
if ext == "":
exe_path = os.environ.get("EXEPATH")
if exe_path is None:
exe_path = ""
else:
exe_path = os.path.join(exe_path, "bin")
cli = [os.path.join(exe_path, "SH.EXE")] + cli
else:
use_shell = True
return subprocess.call(cli, shell=use_shell)
args = ['git', 'hook', 'run', '--ignore-missing', cmd]
if param:
args.append("--")
for p in param:
args.append(p)
return subprocess.call(args) == 0
def write_pipe(c, stdin, *k, **kw):
if verbose:

View File

@ -225,13 +225,13 @@ my $multiedit;
my $editor;
sub system_or_msg {
my ($args, $msg) = @_;
my ($args, $msg, $cmd_name) = @_;
system(@$args);
my $signalled = $? & 127;
my $exit_code = $? >> 8;
return unless $signalled or $exit_code;
my @sprintf_args = ($args->[0], $exit_code);
my @sprintf_args = ($cmd_name ? $cmd_name : $args->[0], $exit_code);
if (defined $msg) {
# Quiet the 'redundant' warning category, except we
# need to support down to Perl 5.8, so we can't do a
@ -2075,10 +2075,10 @@ sub validate_patch {
my ($fn, $xfer_encoding) = @_;
if ($repo) {
my $hook_name = 'sendemail-validate';
my $hooks_path = $repo->command_oneline('rev-parse', '--git-path', 'hooks');
require File::Spec;
my $validate_hook = File::Spec->catfile($hooks_path,
'sendemail-validate');
my $validate_hook = File::Spec->catfile($hooks_path, $hook_name);
my $hook_error;
if (-x $validate_hook) {
require Cwd;
@ -2088,13 +2088,19 @@ sub validate_patch {
chdir($repo->wc_path() or $repo->repo_path())
or die("chdir: $!");
local $ENV{"GIT_DIR"} = $repo->repo_path();
$hook_error = system_or_msg([$validate_hook, $target]);
my @cmd = ("git", "hook", "run", "--ignore-missing",
$hook_name, "--");
my @cmd_msg = (@cmd, "<patch>");
my @cmd_run = (@cmd, $target);
$hook_error = system_or_msg(\@cmd_run, undef, "@cmd_msg");
chdir($cwd_save) or die("chdir: $!");
}
if ($hook_error) {
die sprintf(__("fatal: %s: rejected by sendemail-validate hook\n" .
"%s\n" .
"warning: no patches were sent\n"), $fn, $hook_error);
$hook_error = sprintf(__("fatal: %s: rejected by %s hook\n" .
$hook_error . "\n" .
"warning: no patches were sent\n"),
$fn, $hook_name);
die $hook_error;
}
}

1
git.c
View File

@ -541,6 +541,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 },

131
hook.c
View File

@ -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,133 @@ 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;
cp->dir = hook_cb->options->dir;
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 strbuf abs_path = STRBUF_INIT;
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;
if (options->dir) {
strbuf_add_absolute_path(&abs_path, hook_path);
cb_data.hook_path = abs_path.buf;
}
run_processes_parallel_tr2(jobs,
pick_next_hook,
notify_start_failure,
notify_hook_finished,
&cb_data,
"hook",
hook_name);
ret = cb_data.rc;
cleanup:
strbuf_release(&abs_path);
run_hooks_opt_clear(options);
return ret;
}
int run_hooks(const char *hook_name)
{
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
return run_hooks_opt(hook_name, &opt);
}
int run_hooks_l(const char *hook_name, ...)
{
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
va_list ap;
const char *arg;
va_start(ap, hook_name);
while ((arg = va_arg(ap, const char *)))
strvec_push(&opt.args, arg);
va_end(ap);
return run_hooks_opt(hook_name, &opt);
}

57
hook.h
View File

@ -1,5 +1,37 @@
#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;
/**
* An optional initial working directory for the hook,
* translates to "struct child_process"'s "dir" member.
*/
const char *dir;
};
#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 +45,29 @@ 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);
/**
* A wrapper for run_hooks_opt() which provides a dummy "struct
* run_hooks_opt" initialized with "RUN_HOOKS_OPT_INIT".
*/
int run_hooks(const char *hook_name);
/**
* Like run_hooks(), a wrapper for run_hooks_opt().
*
* In addition to the wrapping behavior provided by run_hooks(), this
* wrapper takes a list of strings terminated by a NULL
* argument. These things will be used as positional arguments to the
* hook. This function behaves like the old run_hook_le() API.
*/
int run_hooks_l(const char *hook_name, ...);
#endif

View File

@ -28,6 +28,7 @@
#include "sparse-index.h"
#include "csum-file.h"
#include "promisor-remote.h"
#include "hook.h"
/* Mask for the name length in ce_flags in the on-disk index */
@ -3150,7 +3151,7 @@ static int do_write_locked_index(struct index_state *istate, struct lock_file *l
else
ret = close_lock_file_gently(lock);
run_hook_le(NULL, "post-index-change",
run_hooks_l("post-index-change",
istate->updated_workdir ? "1" : "0",
istate->updated_skipworktree ? "1" : "0", NULL);
istate->updated_workdir = 0;

View File

@ -7,6 +7,7 @@
#include "tree-walk.h"
#include "tree.h"
#include "unpack-trees.h"
#include "hook.h"
int reset_head(struct repository *r, struct object_id *oid, const char *action,
const char *switch_to_branch, unsigned flags,
@ -127,7 +128,7 @@ reset_head_refs:
reflog_head);
}
if (run_hook)
run_hook_le(NULL, "post-checkout",
run_hooks_l("post-checkout",
oid_to_hex(orig ? orig : null_oid()),
oid_to_hex(oid), "1", NULL);

View File

@ -1307,39 +1307,6 @@ int async_with_fork(void)
#endif
}
int run_hook_ve(const char *const *env, const char *name, va_list args)
{
struct child_process hook = CHILD_PROCESS_INIT;
const char *p;
p = find_hook(name);
if (!p)
return 0;
strvec_push(&hook.args, p);
while ((p = va_arg(args, const char *)))
strvec_push(&hook.args, p);
if (env)
strvec_pushv(&hook.env_array, (const char **)env);
hook.no_stdin = 1;
hook.stdout_to_stderr = 1;
hook.trace2_hook_name = name;
return run_command(&hook);
}
int run_hook_le(const char *const *env, const char *name, ...)
{
va_list args;
int ret;
va_start(args, name);
ret = run_hook_ve(env, name, args);
va_end(args);
return ret;
}
struct io_pump {
/* initialized by caller */
int fd;

View File

@ -220,23 +220,6 @@ int finish_command_in_signal(struct child_process *);
*/
int run_command(struct child_process *);
/**
* Run a hook.
* The first argument is a pathname to an index file, or NULL
* if the hook uses the default index file or no index is needed.
* The second argument is the name of the hook.
* The further arguments correspond to the hook arguments.
* The last argument has to be NULL to terminate the arguments list.
* If the hook does not exist or is not executable, the return
* value will be zero.
* If it is executable, the hook will be executed and the exit
* status of the hook is returned.
* On execution, .stdout_to_stderr and .no_stdin will be set.
*/
LAST_ARG_MUST_BE_NULL
int run_hook_le(const char *const *env, const char *name, ...);
int run_hook_ve(const char *const *env, const char *name, va_list args);
/*
* Trigger an auto-gc
*/

134
t/t1800-hook.sh Executable file
View File

@ -0,0 +1,134 @@
#!/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: nonexistent hook with --ignore-missing' '
git hook run --ignore-missing does-not-exist 2>stderr.actual &&
test_must_be_empty 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

View File

@ -539,7 +539,7 @@ test_expect_success $PREREQ "--validate respects relative core.hooksPath path" '
test_path_is_file my-hooks.ran &&
cat >expect <<-EOF &&
fatal: longline.patch: rejected by sendemail-validate hook
fatal: command '"'"'my-hooks/sendemail-validate'"'"' died with exit code 1
fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
warning: no patches were sent
EOF
test_cmp expect actual
@ -558,7 +558,7 @@ test_expect_success $PREREQ "--validate respects absolute core.hooksPath path" '
test_path_is_file my-hooks.ran &&
cat >expect <<-EOF &&
fatal: longline.patch: rejected by sendemail-validate hook
fatal: command '"'"'$hooks_path/sendemail-validate'"'"' died with exit code 1
fatal: command '"'"'git hook run --ignore-missing sendemail-validate -- <patch>'"'"' died with exit code 1
warning: no patches were sent
EOF
test_cmp expect actual