Merge branch 'ar/config-hooks' into ar/config-hook-cleanups

* ar/config-hooks: (21 commits)
  builtin/receive-pack: avoid spinning no-op sideband async threads
  hook: add -z option to "git hook list"
  hook: allow out-of-repo 'git hook' invocations
  hook: allow event = "" to overwrite previous values
  hook: allow disabling config hooks
  hook: include hooks from the config
  hook: add "git hook list" command
  hook: run a list of hooks to prepare for multihook support
  hook: add internal state alloc/free callbacks
  receive-pack: convert receive hooks to hook API
  receive-pack: convert update hooks to new API
  run-command: poll child input in addition to output
  hook: add jobs option
  reference-transaction: use hook API instead of run-command
  transport: convert pre-push to hook API
  hook: allow separate std[out|err] streams
  hook: convert 'post-rewrite' hook in sequencer.c to hook API
  hook: provide stdin via callback
  run-command: add stdin callback for parallelization
  run-command: add helper for pp child states
  ...
This commit is contained in:
Junio C Hamano
2026-03-09 13:07:50 -07:00
17 changed files with 1726 additions and 327 deletions

View File

@@ -0,0 +1,24 @@
hook.<name>.command::
The command to execute for `hook.<name>`. `<name>` is a unique
"friendly" name that identifies this hook. (The hook events that
trigger the command are configured with `hook.<name>.event`.) The
value can be an executable path or a shell oneliner. If more than
one value is specified for the same `<name>`, only the last value
parsed is used. See linkgit:git-hook[1].
hook.<name>.event::
The hook events that trigger `hook.<name>`. The value is the name
of a hook event, like "pre-commit" or "update". (See
linkgit:githooks[5] for a complete list of hook events.) On the
specified event, the associated `hook.<name>.command` is executed.
This is a multi-valued key. To run `hook.<name>` on multiple
events, specify the key more than once. An empty value resets
the list of events, clearing any previously defined events for
`hook.<name>`. See linkgit:git-hook[1].
hook.<name>.enabled::
Whether the hook `hook.<name>` is enabled. Defaults to `true`.
Set to `false` to disable the hook without removing its
configuration. This is particularly useful when a hook is defined
in a system or global config file and needs to be disabled for a
specific repository. See linkgit:git-hook[1].

View File

@@ -9,6 +9,7 @@ SYNOPSIS
--------
[verse]
'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
'git hook' list [-z] <hook-name>
DESCRIPTION
-----------
@@ -16,18 +17,107 @@ DESCRIPTION
A command interface for running git hooks (see linkgit:githooks[5]),
for use by other scripted git commands.
This command parses the default configuration files for sets of configs like
so:
[hook "linter"]
event = pre-commit
command = ~/bin/linter --cpp20
In this example, `[hook "linter"]` represents one script - `~/bin/linter
--cpp20` - which can be shared by many repos, and even by many hook events, if
appropriate.
To add an unrelated hook which runs on a different event, for example a
spell-checker for your commit messages, you would write a configuration like so:
[hook "linter"]
event = pre-commit
command = ~/bin/linter --cpp20
[hook "spellcheck"]
event = commit-msg
command = ~/bin/spellchecker
With this config, when you run 'git commit', first `~/bin/linter --cpp20` will
have a chance to check your files to be committed (during the `pre-commit` hook
event`), and then `~/bin/spellchecker` will have a chance to check your commit
message (during the `commit-msg` hook event).
Commands are run in the order Git encounters their associated
`hook.<name>.event` configs during the configuration parse (see
linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be
added, only one `hook.linter.command` event is valid - Git uses "last-one-wins"
to determine which command to run.
So if you wanted your linter to run when you commit as well as when you push,
you would configure it like so:
[hook "linter"]
event = pre-commit
event = pre-push
command = ~/bin/linter --cpp20
With this config, `~/bin/linter --cpp20` would be run by Git before a commit is
generated (during `pre-commit`) as well as before a push is performed (during
`pre-push`).
And if you wanted to run your linter as well as a secret-leak detector during
only the "pre-commit" hook event, you would configure it instead like so:
[hook "linter"]
event = pre-commit
command = ~/bin/linter --cpp20
[hook "no-leaks"]
event = pre-commit
command = ~/bin/leak-detector
With this config, before a commit is generated (during `pre-commit`), Git would
first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It
would evaluate the output of each when deciding whether to proceed with the
commit.
For a full list of hook events which you can set your `hook.<name>.event` to,
and how hooks are invoked during those events, see linkgit:githooks[5].
Git will ignore any `hook.<name>.event` that specifies an event it doesn't
recognize. This is intended so that tools which wrap Git can use the hook
infrastructure to run their own hooks; see "WRAPPERS" for more guidance.
In general, when instructions suggest adding a script to
`.git/hooks/<hook-event>`, you can specify it in the config instead by running:
----
git config set hook.<some-name>.command <path-to-script>
git config set --append hook.<some-name>.event <hook-event>
----
This way you can share the script between multiple repos. That is, `cp
~/my-script.sh ~/project/.git/hooks/pre-commit` would become:
----
git config set hook.my-script.command ~/my-script.sh
git config set --append hook.my-script.event pre-commit
----
SUBCOMMANDS
-----------
run::
Run the `<hook-name>` hook. See linkgit:githooks[5] for
supported hook names.
Runs hooks configured for `<hook-name>`, in the order they are
discovered during the config parse. The default `<hook-name>` from
the hookdir is run last. 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).
list [-z]::
Print a list of hooks which will be run on `<hook-name>` event. If no
hooks are configured for that event, print a warning and return 1.
Use `-z` to terminate output lines with NUL instead of newlines.
OPTIONS
-------
@@ -41,6 +131,49 @@ OPTIONS
tools that want to do a blind one-shot run of a hook that may
or may not be present.
-z::
Terminate "list" output lines with NUL instead of newlines.
WRAPPERS
--------
`git hook run` has been designed to make it easy for tools which wrap Git to
configure and execute hooks using the Git hook infrastructure. It is possible to
provide arguments and stdin via the command line, as well as specifying parallel
or series execution if the user has provided multiple hooks.
Assuming your wrapper wants to support a hook named "mywrapper-start-tests", you
can have your users specify their hooks like so:
[hook "setup-test-dashboard"]
event = mywrapper-start-tests
command = ~/mywrapper/setup-dashboard.py --tap
Then, in your 'mywrapper' tool, you can invoke any users' configured hooks by
running:
----
git hook run mywrapper-start-tests \
# providing something to stdin
--stdin some-tempfile-123 \
# execute hooks in serial
# plus some arguments of your own...
-- \
--testname bar \
baz
----
Take care to name your wrapper's hook events in a way which is unlikely to
overlap with Git's native hooks (see linkgit:githooks[5]) - a hook event named
`mywrappertool-validate-commit` is much less likely to be added to native Git
than a hook event named `validate-commit`. If Git begins to use a hook event
named the same thing as your wrapper hook, it may invoke your users' hooks in
unintended and unsupported ways.
CONFIGURATION
-------------
include::config/hook.adoc[]
SEE ALSO
--------
linkgit:githooks[5]

View File

@@ -6,12 +6,16 @@
#include "hook.h"
#include "parse-options.h"
#include "strvec.h"
#include "abspath.h"
#define BUILTIN_HOOK_RUN_USAGE \
N_("git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
#define BUILTIN_HOOK_LIST_USAGE \
N_("git hook list [-z] <hook-name>")
static const char * const builtin_hook_usage[] = {
BUILTIN_HOOK_RUN_USAGE,
BUILTIN_HOOK_LIST_USAGE,
NULL
};
@@ -20,6 +24,67 @@ static const char * const builtin_hook_run_usage[] = {
NULL
};
static int list(int argc, const char **argv, const char *prefix,
struct repository *repo)
{
static const char *const builtin_hook_list_usage[] = {
BUILTIN_HOOK_LIST_USAGE,
NULL
};
struct string_list *head;
struct string_list_item *item;
const char *hookname = NULL;
int line_terminator = '\n';
int ret = 0;
struct option list_options[] = {
OPT_SET_INT('z', NULL, &line_terminator,
N_("use NUL as line terminator"), '\0'),
OPT_END(),
};
argc = parse_options(argc, argv, prefix, list_options,
builtin_hook_list_usage, 0);
/*
* The only unnamed argument provided should be the hook-name; if we add
* arguments later they probably should be caught by parse_options.
*/
if (argc != 1)
usage_msg_opt(_("You must specify a hook event name to list."),
builtin_hook_list_usage, list_options);
hookname = argv[0];
head = list_hooks(repo, hookname, NULL);
if (!head->nr) {
warning(_("No hooks found for event '%s'"), hookname);
ret = 1; /* no hooks found */
goto cleanup;
}
for_each_string_list_item(item, head) {
struct hook *h = item->util;
switch (h->kind) {
case HOOK_TRADITIONAL:
printf("%s%c", _("hook from hookdir"), line_terminator);
break;
case HOOK_CONFIGURED:
printf("%s%c", h->u.configured.friendly_name, line_terminator);
break;
default:
BUG("unknown hook kind");
}
}
cleanup:
hook_list_clear(head, NULL);
free(head);
return ret;
}
static int run(int argc, const char **argv, const char *prefix,
struct repository *repo UNUSED)
{
@@ -77,6 +142,7 @@ int cmd_hook(int argc,
parse_opt_subcommand_fn *fn = NULL;
struct option builtin_hook_options[] = {
OPT_SUBCOMMAND("run", &fn, run),
OPT_SUBCOMMAND("list", &fn, list),
OPT_END(),
};

View File

@@ -561,6 +561,48 @@ static int copy_to_sideband(int in, int out UNUSED, void *arg UNUSED)
return 0;
}
/*
* Start an async thread which redirects hook stderr over the sideband.
* The original stderr fd is saved to `saved_stderr` and STDERR_FILENO is
* redirected to the async's input pipe.
*/
static void prepare_sideband_async(struct async *sideband_async, int *saved_stderr, int *started)
{
*started = 0;
if (!use_sideband)
return;
memset(sideband_async, 0, sizeof(*sideband_async));
sideband_async->proc = copy_to_sideband;
sideband_async->in = -1;
if (!start_async(sideband_async)) {
*started = 1;
*saved_stderr = dup(STDERR_FILENO);
if (*saved_stderr >= 0)
dup2(sideband_async->in, STDERR_FILENO);
close(sideband_async->in);
}
}
/*
* Restore the original stderr and wait for the async sideband thread to finish.
*/
static void finish_sideband_async(struct async *sideband_async, int saved_stderr, int started)
{
if (!use_sideband)
return;
if (saved_stderr >= 0) {
dup2(saved_stderr, STDERR_FILENO);
close(saved_stderr);
}
if (started)
finish_async(sideband_async);
}
static void hmac_hash(unsigned char *out,
const char *key_in, size_t key_len,
const char *text, size_t text_len)
@@ -749,7 +791,7 @@ static int check_cert_push_options(const struct string_list *push_options)
return retval;
}
static void prepare_push_cert_sha1(struct child_process *proc)
static void prepare_push_cert_sha1(struct run_hooks_opt *opt)
{
static int already_done;
@@ -775,23 +817,23 @@ static void prepare_push_cert_sha1(struct child_process *proc)
nonce_status = check_nonce(sigcheck.payload);
}
if (!is_null_oid(&push_cert_oid)) {
strvec_pushf(&proc->env, "GIT_PUSH_CERT=%s",
strvec_pushf(&opt->env, "GIT_PUSH_CERT=%s",
oid_to_hex(&push_cert_oid));
strvec_pushf(&proc->env, "GIT_PUSH_CERT_SIGNER=%s",
strvec_pushf(&opt->env, "GIT_PUSH_CERT_SIGNER=%s",
sigcheck.signer ? sigcheck.signer : "");
strvec_pushf(&proc->env, "GIT_PUSH_CERT_KEY=%s",
strvec_pushf(&opt->env, "GIT_PUSH_CERT_KEY=%s",
sigcheck.key ? sigcheck.key : "");
strvec_pushf(&proc->env, "GIT_PUSH_CERT_STATUS=%c",
strvec_pushf(&opt->env, "GIT_PUSH_CERT_STATUS=%c",
sigcheck.result);
if (push_cert_nonce) {
strvec_pushf(&proc->env,
strvec_pushf(&opt->env,
"GIT_PUSH_CERT_NONCE=%s",
push_cert_nonce);
strvec_pushf(&proc->env,
strvec_pushf(&opt->env,
"GIT_PUSH_CERT_NONCE_STATUS=%s",
nonce_status);
if (nonce_status == NONCE_SLOP)
strvec_pushf(&proc->env,
strvec_pushf(&opt->env,
"GIT_PUSH_CERT_NONCE_SLOP=%ld",
nonce_stamp_slop);
}
@@ -803,94 +845,25 @@ struct receive_hook_feed_state {
struct ref_push_report *report;
int skip_broken;
struct strbuf buf;
const struct string_list *push_options;
};
typedef int (*feed_fn)(void *, const char **, size_t *);
static int run_and_feed_hook(const char *hook_name, feed_fn feed,
struct receive_hook_feed_state *feed_state)
static int feed_receive_hook_cb(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_task_cb)
{
struct child_process proc = CHILD_PROCESS_INIT;
struct async muxer;
int code;
const char *hook_path = find_hook(the_repository, hook_name);
if (!hook_path)
return 0;
strvec_push(&proc.args, hook_path);
proc.in = -1;
proc.stdout_to_stderr = 1;
proc.trace2_hook_name = hook_name;
if (feed_state->push_options) {
size_t i;
for (i = 0; i < feed_state->push_options->nr; i++)
strvec_pushf(&proc.env,
"GIT_PUSH_OPTION_%"PRIuMAX"=%s",
(uintmax_t)i,
feed_state->push_options->items[i].string);
strvec_pushf(&proc.env, "GIT_PUSH_OPTION_COUNT=%"PRIuMAX"",
(uintmax_t)feed_state->push_options->nr);
} else
strvec_pushf(&proc.env, "GIT_PUSH_OPTION_COUNT");
if (tmp_objdir)
strvec_pushv(&proc.env, tmp_objdir_env(tmp_objdir));
if (use_sideband) {
memset(&muxer, 0, sizeof(muxer));
muxer.proc = copy_to_sideband;
muxer.in = -1;
code = start_async(&muxer);
if (code)
return code;
proc.err = muxer.in;
}
prepare_push_cert_sha1(&proc);
code = start_command(&proc);
if (code) {
if (use_sideband)
finish_async(&muxer);
return code;
}
sigchain_push(SIGPIPE, SIG_IGN);
while (1) {
const char *buf;
size_t n;
if (feed(feed_state, &buf, &n))
break;
if (write_in_full(proc.in, buf, n) < 0)
break;
}
close(proc.in);
if (use_sideband)
finish_async(&muxer);
sigchain_pop(SIGPIPE);
return finish_command(&proc);
}
static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
{
struct receive_hook_feed_state *state = state_;
struct receive_hook_feed_state *state = pp_task_cb;
struct command *cmd = state->cmd;
strbuf_reset(&state->buf);
while (cmd &&
state->skip_broken && (cmd->error_string || cmd->did_not_exist))
cmd = cmd->next;
if (!cmd)
return -1; /* EOF */
if (!bufp)
return 0; /* OK, can feed something. */
strbuf_reset(&state->buf);
return 1; /* no more commands left */
if (!state->report)
state->report = cmd->report;
if (state->report) {
struct object_id *old_oid;
struct object_id *new_oid;
@@ -899,23 +872,53 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
old_oid = state->report->old_oid ? state->report->old_oid : &cmd->old_oid;
new_oid = state->report->new_oid ? state->report->new_oid : &cmd->new_oid;
ref_name = state->report->ref_name ? state->report->ref_name : cmd->ref_name;
strbuf_addf(&state->buf, "%s %s %s\n",
oid_to_hex(old_oid), oid_to_hex(new_oid),
ref_name);
state->report = state->report->next;
if (!state->report)
state->cmd = cmd->next;
cmd = cmd->next;
} else {
strbuf_addf(&state->buf, "%s %s %s\n",
oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid),
cmd->ref_name);
state->cmd = cmd->next;
cmd = cmd->next;
}
if (bufp) {
*bufp = state->buf.buf;
*sizep = state->buf.len;
state->cmd = cmd;
if (state->buf.len > 0) {
int ret = write_in_full(hook_stdin_fd, state->buf.buf, state->buf.len);
if (ret < 0) {
if (errno == EPIPE)
return 1; /* child closed pipe */
return ret;
}
}
return 0;
return state->cmd ? 0 : 1; /* 0 = more to come, 1 = EOF */
}
static void *receive_hook_feed_state_alloc(void *feed_pipe_ctx)
{
struct receive_hook_feed_state *init_state = feed_pipe_ctx;
struct receive_hook_feed_state *data = xcalloc(1, sizeof(*data));
data->report = init_state->report;
data->cmd = init_state->cmd;
data->skip_broken = init_state->skip_broken;
strbuf_init(&data->buf, 0);
return data;
}
static void receive_hook_feed_state_free(void *data)
{
struct receive_hook_feed_state *d = data;
if (!d)
return;
strbuf_release(&d->buf);
free(d);
}
static int run_receive_hook(struct command *commands,
@@ -923,47 +926,80 @@ static int run_receive_hook(struct command *commands,
int skip_broken,
const struct string_list *push_options)
{
struct receive_hook_feed_state state;
int status;
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
struct command *iter = commands;
struct receive_hook_feed_state feed_init_state = { 0 };
struct async sideband_async;
int sideband_async_started = 0;
int saved_stderr = -1;
int ret;
strbuf_init(&state.buf, 0);
state.cmd = commands;
state.skip_broken = skip_broken;
state.report = NULL;
if (feed_receive_hook(&state, NULL, NULL))
if (!hook_exists(the_repository, hook_name))
return 0;
state.cmd = commands;
state.push_options = push_options;
status = run_and_feed_hook(hook_name, feed_receive_hook, &state);
strbuf_release(&state.buf);
return status;
/* if there are no valid commands, don't invoke the hook at all. */
while (iter && skip_broken && (iter->error_string || iter->did_not_exist))
iter = iter->next;
if (!iter)
return 0;
if (push_options) {
for (int i = 0; i < push_options->nr; i++)
strvec_pushf(&opt.env, "GIT_PUSH_OPTION_%d=%s", i,
push_options->items[i].string);
strvec_pushf(&opt.env, "GIT_PUSH_OPTION_COUNT=%"PRIuMAX"",
(uintmax_t)push_options->nr);
} else {
strvec_push(&opt.env, "GIT_PUSH_OPTION_COUNT");
}
if (tmp_objdir)
strvec_pushv(&opt.env, tmp_objdir_env(tmp_objdir));
prepare_push_cert_sha1(&opt);
prepare_sideband_async(&sideband_async, &saved_stderr, &sideband_async_started);
/* set up stdin callback */
feed_init_state.cmd = commands;
feed_init_state.skip_broken = skip_broken;
opt.feed_pipe_ctx = &feed_init_state;
opt.feed_pipe = feed_receive_hook_cb;
opt.feed_pipe_cb_data_alloc = receive_hook_feed_state_alloc;
opt.feed_pipe_cb_data_free = receive_hook_feed_state_free;
ret = run_hooks_opt(the_repository, hook_name, &opt);
finish_sideband_async(&sideband_async, saved_stderr, sideband_async_started);
return ret;
}
static int run_update_hook(struct command *cmd)
{
struct child_process proc = CHILD_PROCESS_INIT;
static const char hook_name[] = "update";
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
struct async sideband_async;
int sideband_async_started = 0;
int saved_stderr = -1;
int code;
const char *hook_path = find_hook(the_repository, "update");
if (!hook_path)
if (!hook_exists(the_repository, hook_name))
return 0;
strvec_push(&proc.args, hook_path);
strvec_push(&proc.args, cmd->ref_name);
strvec_push(&proc.args, oid_to_hex(&cmd->old_oid));
strvec_push(&proc.args, oid_to_hex(&cmd->new_oid));
strvec_pushl(&opt.args,
cmd->ref_name,
oid_to_hex(&cmd->old_oid),
oid_to_hex(&cmd->new_oid),
NULL);
proc.no_stdin = 1;
proc.stdout_to_stderr = 1;
proc.err = use_sideband ? -1 : 0;
proc.trace2_hook_name = "update";
prepare_sideband_async(&sideband_async, &saved_stderr, &sideband_async_started);
code = start_command(&proc);
if (code)
return code;
if (use_sideband)
copy_to_sideband(proc.err, -1, NULL);
return finish_command(&proc);
code = run_hooks_opt(the_repository, hook_name, &opt);
finish_sideband_async(&sideband_async, saved_stderr, sideband_async_started);
return code;
}
static struct command *find_command_by_refname(struct command *list,
@@ -1639,34 +1675,29 @@ out:
static void run_update_post_hook(struct command *commands)
{
static const char hook_name[] = "post-update";
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
struct async sideband_async;
struct command *cmd;
struct child_process proc = CHILD_PROCESS_INIT;
const char *hook;
int sideband_async_started = 0;
int saved_stderr = -1;
hook = find_hook(the_repository, "post-update");
if (!hook)
if (!hook_exists(the_repository, hook_name))
return;
for (cmd = commands; cmd; cmd = cmd->next) {
if (cmd->error_string || cmd->did_not_exist)
continue;
if (!proc.args.nr)
strvec_push(&proc.args, hook);
strvec_push(&proc.args, cmd->ref_name);
strvec_push(&opt.args, cmd->ref_name);
}
if (!proc.args.nr)
if (!opt.args.nr)
return;
proc.no_stdin = 1;
proc.stdout_to_stderr = 1;
proc.err = use_sideband ? -1 : 0;
proc.trace2_hook_name = "post-update";
prepare_sideband_async(&sideband_async, &saved_stderr, &sideband_async_started);
if (!start_command(&proc)) {
if (use_sideband)
copy_to_sideband(proc.err, -1, NULL);
finish_command(&proc);
}
run_hooks_opt(the_repository, hook_name, &opt);
finish_sideband_async(&sideband_async, saved_stderr, sideband_async_started);
}
static void check_aliased_update_internal(struct command *cmd,

2
git.c
View File

@@ -587,7 +587,7 @@ static struct cmd_struct commands[] = {
{ "hash-object", cmd_hash_object },
{ "help", cmd_help },
{ "history", cmd_history, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP },
{ "hook", cmd_hook, RUN_SETUP_GENTLY },
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
{ "init", cmd_init_db },
{ "init-db", cmd_init_db },

403
hook.c
View File

@@ -4,9 +4,11 @@
#include "gettext.h"
#include "hook.h"
#include "path.h"
#include "parse.h"
#include "run-command.h"
#include "config.h"
#include "strbuf.h"
#include "strmap.h"
#include "environment.h"
#include "setup.h"
@@ -16,6 +18,9 @@ const char *find_hook(struct repository *r, const char *name)
int found_hook;
if (!r || !r->gitdir)
return NULL;
repo_git_path_replace(r, &path, "hooks/%s", name);
found_hook = access(path.buf, X_OK) >= 0;
#ifdef STRIP_EXTENSION
@@ -47,42 +52,381 @@ const char *find_hook(struct repository *r, const char *name)
return path.buf;
}
static void hook_clear(struct hook *h, cb_data_free_fn cb_data_free)
{
if (!h)
return;
if (h->kind == HOOK_TRADITIONAL)
free((void *)h->u.traditional.path);
else if (h->kind == HOOK_CONFIGURED) {
free((void *)h->u.configured.friendly_name);
free((void *)h->u.configured.command);
}
if (cb_data_free)
cb_data_free(h->feed_pipe_cb_data);
free(h);
}
void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free)
{
struct string_list_item *item;
for_each_string_list_item(item, hooks)
hook_clear(item->util, cb_data_free);
string_list_clear(hooks, 0);
}
/* Helper to detect and add default "traditional" hooks from the hookdir. */
static void list_hooks_add_default(struct repository *r, const char *hookname,
struct string_list *hook_list,
struct run_hooks_opt *options)
{
const char *hook_path = find_hook(r, hookname);
struct hook *h;
if (!hook_path)
return;
h = xcalloc(1, sizeof(struct hook));
/*
* If the hook is to run in a specific dir, a relative path can
* become invalid in that dir, so convert to an absolute path.
*/
if (options && options->dir)
hook_path = absolute_path(hook_path);
/* Setup per-hook internal state cb data */
if (options && options->feed_pipe_cb_data_alloc)
h->feed_pipe_cb_data = options->feed_pipe_cb_data_alloc(options->feed_pipe_ctx);
h->kind = HOOK_TRADITIONAL;
h->u.traditional.path = xstrdup(hook_path);
string_list_append(hook_list, hook_path)->util = h;
}
static void unsorted_string_list_remove(struct string_list *list,
const char *str)
{
struct string_list_item *item = unsorted_string_list_lookup(list, str);
if (item)
unsorted_string_list_delete_item(list, item - list->items, 0);
}
/*
* Callback struct to collect all hook.* keys in a single config pass.
* commands: friendly-name to command map.
* event_hooks: event-name to list of friendly-names map.
* disabled_hooks: set of friendly-names with hook.name.enabled = false.
*/
struct hook_all_config_cb {
struct strmap commands;
struct strmap event_hooks;
struct string_list disabled_hooks;
};
/* repo_config() callback that collects all hook.* configuration in one pass. */
static int hook_config_lookup_all(const char *key, const char *value,
const struct config_context *ctx UNUSED,
void *cb_data)
{
struct hook_all_config_cb *data = cb_data;
const char *name, *subkey;
char *hook_name;
size_t name_len = 0;
if (parse_config_key(key, "hook", &name, &name_len, &subkey))
return 0;
if (!value)
return config_error_nonbool(key);
/* Extract name, ensuring it is null-terminated. */
hook_name = xmemdupz(name, name_len);
if (!strcmp(subkey, "event")) {
if (!*value) {
/* Empty values reset previous events for this hook. */
struct hashmap_iter iter;
struct strmap_entry *e;
strmap_for_each_entry(&data->event_hooks, &iter, e)
unsorted_string_list_remove(e->value, hook_name);
} else {
struct string_list *hooks =
strmap_get(&data->event_hooks, value);
if (!hooks) {
hooks = xcalloc(1, sizeof(*hooks));
string_list_init_dup(hooks);
strmap_put(&data->event_hooks, value, hooks);
}
/* Re-insert if necessary to preserve last-seen order. */
unsorted_string_list_remove(hooks, hook_name);
string_list_append(hooks, hook_name);
}
} else if (!strcmp(subkey, "command")) {
/* Store command overwriting the old value */
char *old = strmap_put(&data->commands, hook_name,
xstrdup(value));
free(old);
} else if (!strcmp(subkey, "enabled")) {
switch (git_parse_maybe_bool(value)) {
case 0: /* disabled */
if (!unsorted_string_list_lookup(&data->disabled_hooks,
hook_name))
string_list_append(&data->disabled_hooks,
hook_name);
break;
case 1: /* enabled: undo a prior disabled entry */
unsorted_string_list_remove(&data->disabled_hooks,
hook_name);
break;
default:
break; /* ignore unrecognised values */
}
}
free(hook_name);
return 0;
}
/*
* The hook config cache maps each hook event name to a string_list where
* every item's string is the hook's friendly-name and its util pointer is
* the corresponding command string. Both strings are owned by the map.
*
* Disabled hooks and hooks missing a command are already filtered out at
* parse time, so callers can iterate the list directly.
*/
void hook_cache_clear(struct strmap *cache)
{
struct hashmap_iter iter;
struct strmap_entry *e;
strmap_for_each_entry(cache, &iter, e) {
struct string_list *hooks = e->value;
string_list_clear(hooks, 1); /* free util (command) pointers */
free(hooks);
}
strmap_clear(cache, 0);
}
/* Populate `cache` with the complete hook configuration */
static void build_hook_config_map(struct repository *r, struct strmap *cache)
{
struct hook_all_config_cb cb_data;
struct hashmap_iter iter;
struct strmap_entry *e;
strmap_init(&cb_data.commands);
strmap_init(&cb_data.event_hooks);
string_list_init_dup(&cb_data.disabled_hooks);
/* Parse all configs in one run. */
repo_config(r, hook_config_lookup_all, &cb_data);
/* Construct the cache from parsed configs. */
strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
struct string_list *hook_names = e->value;
struct string_list *hooks = xcalloc(1, sizeof(*hooks));
string_list_init_dup(hooks);
for (size_t i = 0; i < hook_names->nr; i++) {
const char *hname = hook_names->items[i].string;
char *command;
/* filter out disabled hooks */
if (unsorted_string_list_lookup(&cb_data.disabled_hooks,
hname))
continue;
command = strmap_get(&cb_data.commands, hname);
if (!command)
die(_("'hook.%s.command' must be configured or "
"'hook.%s.event' must be removed;"
" aborting."), hname, hname);
/* util stores the command; owned by the cache. */
string_list_append(hooks, hname)->util =
xstrdup(command);
}
strmap_put(cache, e->key, hooks);
}
strmap_clear(&cb_data.commands, 1);
string_list_clear(&cb_data.disabled_hooks, 0);
strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
string_list_clear(e->value, 0);
free(e->value);
}
strmap_clear(&cb_data.event_hooks, 0);
}
/*
* Return the hook config map for `r`, populating it first if needed.
*
* Out-of-repo calls (r->gitdir == NULL) allocate and return a temporary
* cache map; the caller is responsible for freeing it with
* hook_cache_clear() + free().
*/
static struct strmap *get_hook_config_cache(struct repository *r)
{
struct strmap *cache = NULL;
if (r && r->gitdir) {
/*
* For in-repo calls, the map is stored in r->hook_config_cache,
* so repeated invocations don't parse the configs, so allocate
* it just once on the first call.
*/
if (!r->hook_config_cache) {
r->hook_config_cache = xcalloc(1, sizeof(*cache));
strmap_init(r->hook_config_cache);
build_hook_config_map(r, r->hook_config_cache);
}
cache = r->hook_config_cache;
} else {
/*
* Out-of-repo calls (no gitdir) allocate and return a temporary
* map cache which gets free'd immediately by the caller.
*/
cache = xcalloc(1, sizeof(*cache));
strmap_init(cache);
build_hook_config_map(r, cache);
}
return cache;
}
static void list_hooks_add_configured(struct repository *r,
const char *hookname,
struct string_list *list,
struct run_hooks_opt *options)
{
struct strmap *cache = get_hook_config_cache(r);
struct string_list *configured_hooks = strmap_get(cache, hookname);
/* Iterate through configured hooks and initialize internal states */
for (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {
const char *friendly_name = configured_hooks->items[i].string;
const char *command = configured_hooks->items[i].util;
struct hook *hook = xcalloc(1, sizeof(struct hook));
if (options && options->feed_pipe_cb_data_alloc)
hook->feed_pipe_cb_data =
options->feed_pipe_cb_data_alloc(
options->feed_pipe_ctx);
hook->kind = HOOK_CONFIGURED;
hook->u.configured.friendly_name = xstrdup(friendly_name);
hook->u.configured.command = xstrdup(command);
string_list_append(list, friendly_name)->util = hook;
}
/*
* Cleanup temporary cache for out-of-repo calls since they can't be
* stored persistently. Next out-of-repo calls will have to re-parse.
*/
if (!r || !r->gitdir) {
hook_cache_clear(cache);
free(cache);
}
}
struct string_list *list_hooks(struct repository *r, const char *hookname,
struct run_hooks_opt *options)
{
struct string_list *hook_head;
if (!hookname)
BUG("null hookname was provided to hook_list()!");
hook_head = xmalloc(sizeof(struct string_list));
string_list_init_dup(hook_head);
/* Add hooks from the config, e.g. hook.myhook.event = pre-commit */
list_hooks_add_configured(r, hookname, hook_head, options);
/* Add the default "traditional" hooks from hookdir. */
list_hooks_add_default(r, hookname, hook_head, options);
return hook_head;
}
int hook_exists(struct repository *r, const char *name)
{
return !!find_hook(r, name);
struct string_list *hooks = list_hooks(r, name, NULL);
int exists = hooks->nr > 0;
hook_list_clear(hooks, NULL);
free(hooks);
return exists;
}
static int pick_next_hook(struct child_process *cp,
struct strbuf *out UNUSED,
void *pp_cb,
void **pp_task_cb UNUSED)
void **pp_task_cb)
{
struct hook_cb_data *hook_cb = pp_cb;
const char *hook_path = hook_cb->hook_path;
struct string_list *hook_list = hook_cb->hook_command_list;
struct hook *h;
if (!hook_path)
if (hook_cb->hook_to_run_index >= hook_list->nr)
return 0;
h = hook_list->items[hook_cb->hook_to_run_index++].util;
cp->no_stdin = 1;
strvec_pushv(&cp->env, hook_cb->options->env.v);
if (hook_cb->options->path_to_stdin && hook_cb->options->feed_pipe)
BUG("options path_to_stdin and feed_pipe are mutually exclusive");
/* reopen the file for stdin; run_command closes it. */
if (hook_cb->options->path_to_stdin) {
cp->no_stdin = 0;
cp->in = xopen(hook_cb->options->path_to_stdin, O_RDONLY);
}
cp->stdout_to_stderr = 1;
if (hook_cb->options->feed_pipe) {
cp->no_stdin = 0;
/* start_command() will allocate a pipe / stdin fd for us */
cp->in = -1;
}
cp->stdout_to_stderr = hook_cb->options->stdout_to_stderr;
cp->trace2_hook_name = hook_cb->hook_name;
cp->dir = hook_cb->options->dir;
strvec_push(&cp->args, hook_path);
/* Add hook exec paths or commands */
if (h->kind == HOOK_TRADITIONAL) {
strvec_push(&cp->args, h->u.traditional.path);
} else if (h->kind == HOOK_CONFIGURED) {
/* to enable oneliners, let config-specified hooks run in shell. */
cp->use_shell = true;
strvec_push(&cp->args, h->u.configured.command);
}
if (!cp->args.nr)
BUG("hook must have at least one command or exec path");
strvec_pushv(&cp->args, hook_cb->options->args.v);
/*
* This pick_next_hook() will be called again, we're only
* running one hook, so indicate that no more work will be
* done.
* Provide per-hook internal state via task_cb for easy access, so
* hook callbacks don't have to go through hook_cb->options.
*/
hook_cb->hook_path = NULL;
*pp_task_cb = h->feed_pipe_cb_data;
return 1;
}
@@ -123,23 +467,22 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
int run_hooks_opt(struct repository *r, 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(r, hook_name);
int ret = 0;
const struct run_process_parallel_opts opts = {
.tr2_category = "hook",
.tr2_label = hook_name,
.processes = 1,
.ungroup = 1,
.processes = options->jobs,
.ungroup = options->jobs == 1,
.get_next_task = pick_next_hook,
.start_failure = notify_start_failure,
.feed_pipe = options->feed_pipe,
.task_finished = notify_hook_finished,
.data = &cb_data,
@@ -148,27 +491,35 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
if (!options)
BUG("a struct run_hooks_opt must be provided to run_hooks");
if (options->path_to_stdin && options->feed_pipe)
BUG("options path_to_stdin and feed_pipe are mutually exclusive");
if (!options->jobs)
BUG("run_hooks_opt must be called with options.jobs >= 1");
/*
* Ensure cb_data copy and free functions are either provided together,
* or neither one is provided.
*/
if ((options->feed_pipe_cb_data_alloc && !options->feed_pipe_cb_data_free) ||
(!options->feed_pipe_cb_data_alloc && options->feed_pipe_cb_data_free))
BUG("feed_pipe_cb_data_alloc and feed_pipe_cb_data_free must be set together");
if (options->invoked_hook)
*options->invoked_hook = 0;
if (!hook_path && !options->error_if_missing)
cb_data.hook_command_list = list_hooks(r, hook_name, options);
if (!cb_data.hook_command_list->nr) {
if (options->error_if_missing)
ret = error("cannot find a hook named %s", hook_name);
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(&opts);
ret = cb_data.rc;
cleanup:
strbuf_release(&abs_path);
hook_list_clear(cb_data.hook_command_list, options->feed_pipe_cb_data_free);
free(cb_data.hook_command_list);
run_hooks_opt_clear(options);
return ret;
}

149
hook.h
View File

@@ -1,9 +1,51 @@
#ifndef HOOK_H
#define HOOK_H
#include "strvec.h"
#include "run-command.h"
#include "string-list.h"
#include "strmap.h"
struct repository;
/**
* Represents a hook command to be run.
* Hooks can be:
* 1. "traditional" (found in the hooks directory)
* 2. "configured" (defined in Git's configuration via hook.<name>.event).
* The 'kind' field determines which part of the union 'u' is valid.
*/
struct hook {
enum {
HOOK_TRADITIONAL,
HOOK_CONFIGURED,
} kind;
union {
struct {
const char *path;
} traditional;
struct {
const char *friendly_name;
const char *command;
} configured;
} u;
/**
* Opaque data pointer used to keep internal state across callback calls.
*
* It can be accessed directly via the third hook callback arg:
* struct ... *state = pp_task_cb;
*
* The caller is responsible for managing the memory for this data by
* providing alloc/free callbacks to `run_hooks_opt`.
*
* Only useful when using `run_hooks_opt.feed_pipe`, otherwise ignore it.
*/
void *feed_pipe_cb_data;
};
typedef void (*cb_data_free_fn)(void *data);
typedef void *(*cb_data_alloc_fn)(void *init_ctx);
struct run_hooks_opt
{
/* Environment vars to be set for each hook */
@@ -15,6 +57,14 @@ struct run_hooks_opt
/* Emit an error if the hook is missing */
unsigned int error_if_missing:1;
/**
* Number of processes to parallelize across.
*
* If > 1, output will be buffered and de-interleaved (ungroup=0).
* If == 1, output will be real-time (ungroup=1).
*/
unsigned int jobs;
/**
* An optional initial working directory for the hook,
* translates to "struct child_process"'s "dir" member.
@@ -33,26 +83,121 @@ struct run_hooks_opt
*/
int *invoked_hook;
/**
* Send the hook's stdout to stderr.
*
* This is the default behavior for all hooks except pre-push,
* which has separate stdout and stderr streams for backwards
* compatibility reasons.
*/
unsigned int stdout_to_stderr:1;
/**
* Path to file which should be piped to stdin for each hook.
*/
const char *path_to_stdin;
/**
* Callback used to incrementally feed a child hook stdin pipe.
*
* Useful especially if a hook consumes large quantities of data
* (e.g. a list of all refs in a client push), so feeding it via
* in-memory strings or slurping to/from files is inefficient.
* While the callback allows piecemeal writing, it can also be
* used for smaller inputs, where it gets called only once.
*
* Add hook callback initalization context to `feed_pipe_ctx`.
* Add hook callback internal state to `feed_pipe_cb_data`.
*
*/
feed_pipe_fn feed_pipe;
/**
* Opaque data pointer used to pass context to `feed_pipe_fn`.
*
* It can be accessed via the second callback arg 'pp_cb':
* ((struct hook_cb_data *) pp_cb)->hook_cb->options->feed_pipe_ctx;
*
* The caller is responsible for managing the memory for this data.
* Only useful when using `run_hooks_opt.feed_pipe`, otherwise ignore it.
*/
void *feed_pipe_ctx;
/**
* Some hooks need to create a fresh `feed_pipe_cb_data` internal state,
* so they can keep track of progress without affecting one another.
*
* If provided, this function will be called to alloc & initialize the
* `feed_pipe_cb_data` for each hook.
*
* The `feed_pipe_ctx` pointer can be used to pass initialization data.
*/
cb_data_alloc_fn feed_pipe_cb_data_alloc;
/**
* Called to free the memory initialized by `feed_pipe_cb_data_alloc`.
*
* Must always be provided when `feed_pipe_cb_data_alloc` is provided.
*/
cb_data_free_fn feed_pipe_cb_data_free;
};
#define RUN_HOOKS_OPT_INIT { \
.env = STRVEC_INIT, \
.args = STRVEC_INIT, \
.stdout_to_stderr = 1, \
.jobs = 1, \
}
struct hook_cb_data {
/* rc reflects the cumulative failure state */
int rc;
const char *hook_name;
const char *hook_path;
/**
* A list of hook commands/paths to run for the 'hook_name' event.
*
* The 'string' member of each item holds the path (for traditional hooks)
* or the unique friendly-name for hooks specified in configs.
* The 'util' member of each item points to the corresponding struct hook.
*/
struct string_list *hook_command_list;
/* Iterator/cursor for the above list, pointing to the next hook to run. */
size_t hook_to_run_index;
struct run_hooks_opt *options;
};
/*
/**
* Provides a list of hook commands to run for the 'hookname' event.
*
* This function consolidates hooks from two sources:
* 1. The config-based hooks (not yet implemented).
* 2. The "traditional" hook found in the repository hooks directory
* (e.g., .git/hooks/pre-commit).
*
* The list is ordered by execution priority.
*
* The caller is responsible for freeing the memory of the returned list
* using string_list_clear() and free().
*/
struct string_list *list_hooks(struct repository *r, const char *hookname,
struct run_hooks_opt *options);
/**
* Frees the memory allocated for the hook list, including the `struct hook`
* items and their internal state.
*/
void hook_list_clear(struct string_list *hooks, cb_data_free_fn cb_data_free);
/**
* Frees the hook configuration cache stored in `struct repository`.
* Called by repo_clear().
*/
void hook_cache_clear(struct strmap *cache);
/**
* Returns the path to the hook file, or NULL if the hook is missing
* or disabled. Note that this points to static storage that will be
* overwritten by further calls to find_hook and run_hook_*.

128
refs.c
View File

@@ -16,7 +16,6 @@
#include "iterator.h"
#include "refs.h"
#include "refs/refs-internal.h"
#include "run-command.h"
#include "hook.h"
#include "object-name.h"
#include "odb.h"
@@ -27,7 +26,6 @@
#include "strvec.h"
#include "repo-settings.h"
#include "setup.h"
#include "sigchain.h"
#include "date.h"
#include "commit.h"
#include "wildmatch.h"
@@ -2551,68 +2549,86 @@ static int ref_update_reject_duplicates(struct string_list *refnames,
return 0;
}
struct transaction_feed_cb_data {
size_t index;
struct strbuf buf;
};
static int transaction_hook_feed_stdin(int hook_stdin_fd, void *pp_cb, void *pp_task_cb)
{
struct hook_cb_data *hook_cb = pp_cb;
struct ref_transaction *transaction = hook_cb->options->feed_pipe_ctx;
struct transaction_feed_cb_data *feed_cb_data = pp_task_cb;
struct strbuf *buf = &feed_cb_data->buf;
struct ref_update *update;
size_t i = feed_cb_data->index++;
int ret;
if (i >= transaction->nr)
return 1; /* No more refs to process */
update = transaction->updates[i];
if (update->flags & REF_LOG_ONLY)
return 0;
strbuf_reset(buf);
if (!(update->flags & REF_HAVE_OLD))
strbuf_addf(buf, "%s ", oid_to_hex(null_oid(the_hash_algo)));
else if (update->old_target)
strbuf_addf(buf, "ref:%s ", update->old_target);
else
strbuf_addf(buf, "%s ", oid_to_hex(&update->old_oid));
if (!(update->flags & REF_HAVE_NEW))
strbuf_addf(buf, "%s ", oid_to_hex(null_oid(the_hash_algo)));
else if (update->new_target)
strbuf_addf(buf, "ref:%s ", update->new_target);
else
strbuf_addf(buf, "%s ", oid_to_hex(&update->new_oid));
strbuf_addf(buf, "%s\n", update->refname);
ret = write_in_full(hook_stdin_fd, buf->buf, buf->len);
if (ret < 0 && errno != EPIPE)
return ret;
return 0; /* no more input to feed */
}
static void *transaction_feed_cb_data_alloc(void *feed_pipe_ctx UNUSED)
{
struct transaction_feed_cb_data *data = xmalloc(sizeof(*data));
strbuf_init(&data->buf, 0);
data->index = 0;
return data;
}
static void transaction_feed_cb_data_free(void *data)
{
struct transaction_feed_cb_data *d = data;
if (!d)
return;
strbuf_release(&d->buf);
free(d);
}
static int run_transaction_hook(struct ref_transaction *transaction,
const char *state)
{
struct child_process proc = CHILD_PROCESS_INIT;
struct strbuf buf = STRBUF_INIT;
const char *hook;
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
int ret = 0;
hook = find_hook(transaction->ref_store->repo, "reference-transaction");
if (!hook)
return ret;
strvec_push(&opt.args, state);
strvec_pushl(&proc.args, hook, state, NULL);
proc.in = -1;
proc.stdout_to_stderr = 1;
proc.trace2_hook_name = "reference-transaction";
opt.feed_pipe = transaction_hook_feed_stdin;
opt.feed_pipe_ctx = transaction;
opt.feed_pipe_cb_data_alloc = transaction_feed_cb_data_alloc;
opt.feed_pipe_cb_data_free = transaction_feed_cb_data_free;
ret = start_command(&proc);
if (ret)
return ret;
ret = run_hooks_opt(transaction->ref_store->repo, "reference-transaction", &opt);
sigchain_push(SIGPIPE, SIG_IGN);
for (size_t i = 0; i < transaction->nr; i++) {
struct ref_update *update = transaction->updates[i];
if (update->flags & REF_LOG_ONLY)
continue;
strbuf_reset(&buf);
if (!(update->flags & REF_HAVE_OLD))
strbuf_addf(&buf, "%s ", oid_to_hex(null_oid(the_hash_algo)));
else if (update->old_target)
strbuf_addf(&buf, "ref:%s ", update->old_target);
else
strbuf_addf(&buf, "%s ", oid_to_hex(&update->old_oid));
if (!(update->flags & REF_HAVE_NEW))
strbuf_addf(&buf, "%s ", oid_to_hex(null_oid(the_hash_algo)));
else if (update->new_target)
strbuf_addf(&buf, "ref:%s ", update->new_target);
else
strbuf_addf(&buf, "%s ", oid_to_hex(&update->new_oid));
strbuf_addf(&buf, "%s\n", update->refname);
if (write_in_full(proc.in, buf.buf, buf.len) < 0) {
if (errno != EPIPE) {
/* Don't leak errno outside this API */
errno = 0;
ret = -1;
}
break;
}
}
close(proc.in);
sigchain_pop(SIGPIPE);
strbuf_release(&buf);
ret |= finish_command(&proc);
return ret;
}

View File

@@ -1,6 +1,7 @@
#include "git-compat-util.h"
#include "abspath.h"
#include "repository.h"
#include "hook.h"
#include "odb.h"
#include "config.h"
#include "object.h"
@@ -413,6 +414,11 @@ void repo_clear(struct repository *repo)
FREE_AND_NULL(repo->index);
}
if (repo->hook_config_cache) {
hook_cache_clear(repo->hook_config_cache);
FREE_AND_NULL(repo->hook_config_cache);
}
if (repo->promisor_remote_config) {
promisor_remote_clear(repo->promisor_remote_config);
FREE_AND_NULL(repo->promisor_remote_config);

View File

@@ -166,6 +166,12 @@ struct repository {
/* True if commit-graph has been disabled within this process. */
int commit_graph_disabled;
/*
* Lazily-populated cache mapping hook event names to configured hooks.
* NULL until first hook use.
*/
struct strmap *hook_config_cache;
/* Configurations related to promisor remotes. */
char *repository_format_partial_clone;
struct promisor_remote_config *promisor_remote_config;

View File

@@ -1478,15 +1478,40 @@ enum child_state {
GIT_CP_WAIT_CLEANUP,
};
struct parallel_child {
enum child_state state;
struct child_process process;
struct strbuf err;
void *data;
};
static int child_is_working(const struct parallel_child *pp_child)
{
return pp_child->state == GIT_CP_WORKING;
}
static int child_is_ready_for_cleanup(const struct parallel_child *pp_child)
{
return child_is_working(pp_child) && !pp_child->process.in;
}
static int child_is_receiving_input(const struct parallel_child *pp_child)
{
return child_is_working(pp_child) && pp_child->process.in > 0;
}
static int child_is_sending_output(const struct parallel_child *pp_child)
{
/*
* all pp children which buffer output through run_command via ungroup=0
* redirect stdout to stderr, so we just need to check process.err.
*/
return child_is_working(pp_child) && pp_child->process.err > 0;
}
struct parallel_processes {
size_t nr_processes;
struct {
enum child_state state;
struct child_process process;
struct strbuf err;
void *data;
} *children;
struct parallel_child *children;
/*
* The struct pollfd is logically part of *children,
* but the system call expects it as its own array.
@@ -1509,7 +1534,7 @@ static void kill_children(const struct parallel_processes *pp,
int signo)
{
for (size_t i = 0; i < opts->processes; i++)
if (pp->children[i].state == GIT_CP_WORKING)
if (child_is_working(&pp->children[i]))
kill(pp->children[i].process.pid, signo);
}
@@ -1545,7 +1570,7 @@ static void pp_init(struct parallel_processes *pp,
CALLOC_ARRAY(pp->children, n);
if (!opts->ungroup)
CALLOC_ARRAY(pp->pfd, n);
CALLOC_ARRAY(pp->pfd, n * 2);
for (size_t i = 0; i < n; i++) {
strbuf_init(&pp->children[i].err, 0);
@@ -1652,21 +1677,101 @@ static int pp_start_one(struct parallel_processes *pp,
return 0;
}
static void pp_buffer_stderr(struct parallel_processes *pp,
const struct run_process_parallel_opts *opts,
int output_timeout)
static void pp_buffer_stdin(struct parallel_processes *pp,
const struct run_process_parallel_opts *opts)
{
while (poll(pp->pfd, opts->processes, output_timeout) < 0) {
/* Buffer stdin for each pipe. */
for (size_t i = 0; i < opts->processes; i++) {
struct child_process *proc = &pp->children[i].process;
int ret;
if (!child_is_receiving_input(&pp->children[i]))
continue;
/*
* child input is provided via path_to_stdin when the feed_pipe cb is
* missing, so we just signal an EOF.
*/
if (!opts->feed_pipe) {
close(proc->in);
proc->in = 0;
continue;
}
/**
* Feed the pipe:
* ret < 0 means error
* ret == 0 means there is more data to be fed
* ret > 0 means feeding finished
*/
ret = opts->feed_pipe(proc->in, opts->data, pp->children[i].data);
if (ret < 0)
die_errno("feed_pipe");
if (ret) {
close(proc->in);
proc->in = 0;
}
}
}
static void pp_buffer_io(struct parallel_processes *pp,
const struct run_process_parallel_opts *opts,
int timeout)
{
/* for each potential child slot, prepare two pollfd entries */
for (size_t i = 0; i < opts->processes; i++) {
if (child_is_sending_output(&pp->children[i])) {
pp->pfd[2*i].fd = pp->children[i].process.err;
pp->pfd[2*i].events = POLLIN | POLLHUP;
} else {
pp->pfd[2*i].fd = -1;
}
if (child_is_receiving_input(&pp->children[i])) {
pp->pfd[2*i+1].fd = pp->children[i].process.in;
pp->pfd[2*i+1].events = POLLOUT;
} else {
pp->pfd[2*i+1].fd = -1;
}
}
while (poll(pp->pfd, opts->processes * 2, timeout) < 0) {
if (errno == EINTR)
continue;
pp_cleanup(pp, opts);
die_errno("poll");
}
/* Buffer output from all pipes. */
for (size_t i = 0; i < opts->processes; i++) {
if (pp->children[i].state == GIT_CP_WORKING &&
pp->pfd[i].revents & (POLLIN | POLLHUP)) {
/* Handle input feeding (stdin) */
if (pp->pfd[2*i+1].revents & (POLLOUT | POLLHUP | POLLERR)) {
if (opts->feed_pipe) {
int ret = opts->feed_pipe(pp->children[i].process.in,
opts->data,
pp->children[i].data);
if (ret < 0)
die_errno("feed_pipe");
if (ret) {
/* done feeding */
close(pp->children[i].process.in);
pp->children[i].process.in = 0;
}
} else {
/*
* No feed_pipe means there is nothing to do, so
* close the fd. Child input can be fed by other
* methods, such as opts->path_to_stdin which
* slurps a file via dup2, so clean up here.
*/
close(pp->children[i].process.in);
pp->children[i].process.in = 0;
}
}
/* Handle output reading (stderr) */
if (child_is_working(&pp->children[i]) &&
pp->pfd[2*i].revents & (POLLIN | POLLHUP)) {
int n = strbuf_read_once(&pp->children[i].err,
pp->children[i].process.err, 0);
if (n == 0) {
@@ -1683,7 +1788,7 @@ static void pp_output(const struct parallel_processes *pp)
{
size_t i = pp->output_owner;
if (pp->children[i].state == GIT_CP_WORKING &&
if (child_is_working(&pp->children[i]) &&
pp->children[i].err.len) {
strbuf_write(&pp->children[i].err, stderr);
strbuf_reset(&pp->children[i].err);
@@ -1722,6 +1827,7 @@ static int pp_collect_finished(struct parallel_processes *pp,
pp->children[i].state = GIT_CP_FREE;
if (pp->pfd)
pp->pfd[i].fd = -1;
pp->children[i].process.in = 0;
child_process_init(&pp->children[i].process);
if (opts->ungroup) {
@@ -1748,7 +1854,7 @@ static int pp_collect_finished(struct parallel_processes *pp,
* running process time.
*/
for (i = 0; i < n; i++)
if (pp->children[(pp->output_owner + i) % n].state == GIT_CP_WORKING)
if (child_is_working(&pp->children[(pp->output_owner + i) % n]))
break;
pp->output_owner = (pp->output_owner + i) % n;
}
@@ -1756,10 +1862,25 @@ static int pp_collect_finished(struct parallel_processes *pp,
return result;
}
static void pp_handle_child_IO(struct parallel_processes *pp,
const struct run_process_parallel_opts *opts,
int timeout)
{
if (opts->ungroup) {
pp_buffer_stdin(pp, opts);
for (size_t i = 0; i < opts->processes; i++)
if (child_is_ready_for_cleanup(&pp->children[i]))
pp->children[i].state = GIT_CP_WAIT_CLEANUP;
} else {
pp_buffer_io(pp, opts, timeout);
pp_output(pp);
}
}
void run_processes_parallel(const struct run_process_parallel_opts *opts)
{
int i, code;
int output_timeout = 100;
int timeout = 100;
int spawn_cap = 4;
struct parallel_processes_for_signal pp_sig;
struct parallel_processes pp = {
@@ -1775,6 +1896,13 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts)
"max:%"PRIuMAX,
(uintmax_t)opts->processes);
/*
* Child tasks might receive input via stdin, terminating early (or not), so
* ignore the default SIGPIPE which gets handled by each feed_pipe_fn which
* actually writes the data to children stdin fds.
*/
sigchain_push(SIGPIPE, SIG_IGN);
pp_init(&pp, opts, &pp_sig);
while (1) {
for (i = 0;
@@ -1792,13 +1920,7 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts)
}
if (!pp.nr_processes)
break;
if (opts->ungroup) {
for (size_t i = 0; i < opts->processes; i++)
pp.children[i].state = GIT_CP_WAIT_CLEANUP;
} else {
pp_buffer_stderr(&pp, opts, output_timeout);
pp_output(&pp);
}
pp_handle_child_IO(&pp, opts, timeout);
code = pp_collect_finished(&pp, opts);
if (code) {
pp.shutdown = 1;
@@ -1809,6 +1931,8 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts)
pp_cleanup(&pp, opts);
sigchain_pop(SIGPIPE);
if (do_trace2)
trace2_region_leave(tr2_category, tr2_label, NULL);
}

View File

@@ -420,6 +420,21 @@ typedef int (*start_failure_fn)(struct strbuf *out,
void *pp_cb,
void *pp_task_cb);
/**
* This callback is repeatedly called on every child process who requests
* start_command() to create a pipe by setting child_process.in < 0.
*
* pp_cb is the callback cookie as passed into run_processes_parallel, and
* pp_task_cb is the callback cookie as passed into get_next_task_fn.
*
* Returns < 0 for error
* Returns == 0 when there is more data to be fed (will be called again)
* Returns > 0 when finished (child closed fd or no more data to be fed)
*/
typedef int (*feed_pipe_fn)(int child_in,
void *pp_cb,
void *pp_task_cb);
/**
* This callback is called on every child process that finished processing.
*
@@ -473,6 +488,12 @@ struct run_process_parallel_opts
*/
start_failure_fn start_failure;
/*
* feed_pipe: see feed_pipe_fn() above. This can be NULL to omit any
* special handling.
*/
feed_pipe_fn feed_pipe;
/**
* task_finished: See task_finished_fn() above. This can be
* NULL to omit any special handling.

View File

@@ -1292,32 +1292,40 @@ int update_head_with_reflog(const struct commit *old_head,
return ret;
}
static int pipe_from_strbuf(int hook_stdin_fd, void *pp_cb, void *pp_task_cb UNUSED)
{
struct hook_cb_data *hook_cb = pp_cb;
struct strbuf *to_pipe = hook_cb->options->feed_pipe_ctx;
int ret;
if (!to_pipe)
BUG("pipe_from_strbuf called without feed_pipe_ctx");
ret = write_in_full(hook_stdin_fd, to_pipe->buf, to_pipe->len);
if (ret < 0 && errno != EPIPE)
return ret;
return 1; /* done writing */
}
static int run_rewrite_hook(const struct object_id *oldoid,
const struct object_id *newoid)
{
struct child_process proc = CHILD_PROCESS_INIT;
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
int code;
struct strbuf sb = STRBUF_INIT;
const char *hook_path = find_hook(the_repository, "post-rewrite");
if (!hook_path)
return 0;
strvec_pushl(&proc.args, hook_path, "amend", NULL);
proc.in = -1;
proc.stdout_to_stderr = 1;
proc.trace2_hook_name = "post-rewrite";
code = start_command(&proc);
if (code)
return code;
strbuf_addf(&sb, "%s %s\n", oid_to_hex(oldoid), oid_to_hex(newoid));
sigchain_push(SIGPIPE, SIG_IGN);
write_in_full(proc.in, sb.buf, sb.len);
close(proc.in);
opt.feed_pipe_ctx = &sb;
opt.feed_pipe = pipe_from_strbuf;
strvec_push(&opt.args, "amend");
code = run_hooks_opt(the_repository, "post-rewrite", &opt);
strbuf_release(&sb);
sigchain_pop(SIGPIPE);
return finish_command(&proc);
return code;
}
void commit_post_rewrite(struct repository *r,

View File

@@ -23,19 +23,26 @@ static int number_callbacks;
static int parallel_next(struct child_process *cp,
struct strbuf *err,
void *cb,
void **task_cb UNUSED)
void **task_cb)
{
struct child_process *d = cb;
if (number_callbacks >= 4)
return 0;
strvec_pushv(&cp->args, d->args.v);
cp->in = d->in;
cp->no_stdin = d->no_stdin;
if (err)
strbuf_addstr(err, "preloaded output of a child\n");
else
fprintf(stderr, "preloaded output of a child\n");
number_callbacks++;
/* test_stdin callback will use this to count remaining lines */
*task_cb = xmalloc(sizeof(int));
*(int*)(*task_cb) = 2;
return 1;
}
@@ -54,15 +61,48 @@ static int no_job(struct child_process *cp UNUSED,
static int task_finished(int result UNUSED,
struct strbuf *err,
void *pp_cb UNUSED,
void *pp_task_cb UNUSED)
void *pp_task_cb)
{
if (err)
strbuf_addstr(err, "asking for a quick stop\n");
else
fprintf(stderr, "asking for a quick stop\n");
FREE_AND_NULL(pp_task_cb);
return 1;
}
static int task_finished_quiet(int result UNUSED,
struct strbuf *err UNUSED,
void *pp_cb UNUSED,
void *pp_task_cb)
{
FREE_AND_NULL(pp_task_cb);
return 0;
}
static int test_stdin_pipe_feed(int hook_stdin_fd, void *cb UNUSED, void *task_cb)
{
int *lines_remaining = task_cb;
if (*lines_remaining) {
struct strbuf buf = STRBUF_INIT;
strbuf_addf(&buf, "sample stdin %d\n", --(*lines_remaining));
if (write_in_full(hook_stdin_fd, buf.buf, buf.len) < 0) {
if (errno == EPIPE) {
/* child closed stdin, nothing more to do */
strbuf_release(&buf);
return 1;
}
die_errno("write");
}
strbuf_release(&buf);
}
return !(*lines_remaining);
}
struct testsuite {
struct string_list tests, failed;
int next;
@@ -157,6 +197,7 @@ static int testsuite(int argc, const char **argv)
struct run_process_parallel_opts opts = {
.get_next_task = next_test,
.start_failure = test_failed,
.feed_pipe = test_stdin_pipe_feed,
.task_finished = test_finished,
.data = &suite,
};
@@ -460,12 +501,19 @@ int cmd__run_command(int argc, const char **argv)
if (!strcmp(argv[1], "run-command-parallel")) {
opts.get_next_task = parallel_next;
opts.task_finished = task_finished_quiet;
} else if (!strcmp(argv[1], "run-command-abort")) {
opts.get_next_task = parallel_next;
opts.task_finished = task_finished;
} else if (!strcmp(argv[1], "run-command-no-jobs")) {
opts.get_next_task = no_job;
opts.task_finished = task_finished;
} else if (!strcmp(argv[1], "run-command-stdin")) {
proc.in = -1;
proc.no_stdin = 0;
opts.get_next_task = parallel_next;
opts.task_finished = task_finished_quiet;
opts.feed_pipe = test_stdin_pipe_feed;
} else {
ret = 1;
fprintf(stderr, "check usage\n");

View File

@@ -164,6 +164,37 @@ test_expect_success 'run_command runs ungrouped in parallel with more tasks than
test_line_count = 4 err
'
test_expect_success 'run_command listens to stdin' '
cat >expect <<-\EOF &&
preloaded output of a child
listening for stdin:
sample stdin 1
sample stdin 0
preloaded output of a child
listening for stdin:
sample stdin 1
sample stdin 0
preloaded output of a child
listening for stdin:
sample stdin 1
sample stdin 0
preloaded output of a child
listening for stdin:
sample stdin 1
sample stdin 0
EOF
write_script stdin-script <<-\EOF &&
echo "listening for stdin:"
while read line
do
echo "$line"
done
EOF
test-tool run-command run-command-stdin 2 ./stdin-script 2>actual &&
test_cmp expect actual
'
cat >expect <<-EOF
preloaded output of a child
asking for a quick stop

View File

@@ -1,18 +1,79 @@
#!/bin/sh
test_description='git-hook command'
test_description='git-hook command and config-managed multihooks'
. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-terminal.sh
setup_hooks () {
test_config hook.ghi.command "/path/ghi"
test_config hook.ghi.event pre-commit --add
test_config hook.ghi.event test-hook --add
test_config_global hook.def.command "/path/def"
test_config_global hook.def.event pre-commit --add
}
setup_hookdir () {
mkdir .git/hooks
write_script .git/hooks/pre-commit <<-EOF
echo \"Legacy Hook\"
EOF
test_when_finished rm -rf .git/hooks
}
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 list -h &&
test_expect_code 129 git hook run --unknown 2>err &&
test_expect_code 129 git hook list &&
test_expect_code 129 git hook list -h &&
grep "unknown option" err
'
test_expect_success 'git hook list: nonexistent hook' '
cat >stderr.expect <<-\EOF &&
warning: No hooks found for event '\''test-hook'\''
EOF
test_expect_code 1 git hook list test-hook 2>stderr.actual &&
test_cmp stderr.expect stderr.actual
'
test_expect_success 'git hook list: traditional hook from hookdir' '
test_hook test-hook <<-EOF &&
echo Test hook
EOF
cat >expect <<-\EOF &&
hook from hookdir
EOF
git hook list test-hook >actual &&
test_cmp expect actual
'
test_expect_success 'git hook list: configured hook' '
test_config hook.myhook.command "echo Hello" &&
test_config hook.myhook.event test-hook --add &&
echo "myhook" >expect &&
git hook list test-hook >actual &&
test_cmp expect actual
'
test_expect_success 'git hook list: -z shows NUL-terminated output' '
test_hook test-hook <<-EOF &&
echo Test hook
EOF
test_config hook.myhook.command "echo Hello" &&
test_config hook.myhook.event test-hook --add &&
printf "myhookQhook from hookdirQ" >expect &&
git hook list -z test-hook >actual.raw &&
nul_to_q <actual.raw >actual &&
test_cmp expect actual
'
test_expect_success 'git hook run: nonexistent hook' '
cat >stderr.expect <<-\EOF &&
error: cannot find a hook named test-hook
@@ -83,12 +144,18 @@ test_expect_success 'git hook run -- pass arguments' '
test_cmp expect actual
'
test_expect_success 'git hook run -- out-of-repo runs excluded' '
test_hook test-hook <<-EOF &&
echo Test hook
EOF
test_expect_success 'git hook run: out-of-repo runs execute global hooks' '
test_config_global hook.global-hook.event test-hook --add &&
test_config_global hook.global-hook.command "echo no repo no problems" --add &&
nongit test_must_fail git hook run test-hook
echo "global-hook" >expect &&
nongit git hook list test-hook >actual &&
test_cmp expect actual &&
echo "no repo no problems" >expect &&
nongit git hook run test-hook 2>actual &&
test_cmp expect actual
'
test_expect_success 'git -c core.hooksPath=<PATH> hook run' '
@@ -150,6 +217,170 @@ test_expect_success TTY 'git commit: stdout and stderr are connected to a TTY' '
test_hook_tty commit -m"B.new"
'
test_expect_success 'git hook list orders by config order' '
setup_hooks &&
cat >expected <<-\EOF &&
def
ghi
EOF
git hook list pre-commit >actual &&
test_cmp expected actual
'
test_expect_success 'git hook list reorders on duplicate event declarations' '
setup_hooks &&
# 'def' is usually configured globally; move it to the end by
# configuring it locally.
test_config hook.def.event "pre-commit" --add &&
cat >expected <<-\EOF &&
ghi
def
EOF
git hook list pre-commit >actual &&
test_cmp expected actual
'
test_expect_success 'git hook list: empty event value resets events' '
setup_hooks &&
# ghi is configured for pre-commit; reset it with an empty value
test_config hook.ghi.event "" --add &&
# only def should remain for pre-commit
echo "def" >expected &&
git hook list pre-commit >actual &&
test_cmp expected actual
'
test_expect_success 'hook can be configured for multiple events' '
setup_hooks &&
# 'ghi' should be included in both 'pre-commit' and 'test-hook'
git hook list pre-commit >actual &&
grep "ghi" actual &&
git hook list test-hook >actual &&
grep "ghi" actual
'
test_expect_success 'git hook list shows hooks from the hookdir' '
setup_hookdir &&
cat >expected <<-\EOF &&
hook from hookdir
EOF
git hook list pre-commit >actual &&
test_cmp expected actual
'
test_expect_success 'inline hook definitions execute oneliners' '
test_config hook.oneliner.event "pre-commit" &&
test_config hook.oneliner.command "echo \"Hello World\"" &&
echo "Hello World" >expected &&
# hooks are run with stdout_to_stderr = 1
git hook run pre-commit 2>actual &&
test_cmp expected actual
'
test_expect_success 'inline hook definitions resolve paths' '
write_script sample-hook.sh <<-\EOF &&
echo \"Sample Hook\"
EOF
test_when_finished "rm sample-hook.sh" &&
test_config hook.sample-hook.event pre-commit &&
test_config hook.sample-hook.command "\"$(pwd)/sample-hook.sh\"" &&
echo \"Sample Hook\" >expected &&
# hooks are run with stdout_to_stderr = 1
git hook run pre-commit 2>actual &&
test_cmp expected actual
'
test_expect_success 'hookdir hook included in git hook run' '
setup_hookdir &&
echo \"Legacy Hook\" >expected &&
# hooks are run with stdout_to_stderr = 1
git hook run pre-commit 2>actual &&
test_cmp expected actual
'
test_expect_success 'stdin to multiple hooks' '
test_config hook.stdin-a.event "test-hook" &&
test_config hook.stdin-a.command "xargs -P1 -I% echo a%" &&
test_config hook.stdin-b.event "test-hook" &&
test_config hook.stdin-b.command "xargs -P1 -I% echo b%" &&
cat >input <<-\EOF &&
1
2
3
EOF
cat >expected <<-\EOF &&
a1
a2
a3
b1
b2
b3
EOF
git hook run --to-stdin=input test-hook 2>actual &&
test_cmp expected actual
'
test_expect_success 'rejects hooks with no commands configured' '
test_config hook.broken.event "test-hook" &&
test_must_fail git hook list test-hook 2>actual &&
test_grep "hook.broken.command" actual &&
test_must_fail git hook run test-hook 2>actual &&
test_grep "hook.broken.command" actual
'
test_expect_success 'disabled hook is not run' '
test_config hook.skipped.event "test-hook" &&
test_config hook.skipped.command "echo \"Should not run\"" &&
test_config hook.skipped.enabled false &&
git hook run --ignore-missing test-hook 2>actual &&
test_must_be_empty actual
'
test_expect_success 'disabled hook does not appear in git hook list' '
test_config hook.active.event "pre-commit" &&
test_config hook.active.command "echo active" &&
test_config hook.inactive.event "pre-commit" &&
test_config hook.inactive.command "echo inactive" &&
test_config hook.inactive.enabled false &&
git hook list pre-commit >actual &&
test_grep "active" actual &&
test_grep ! "inactive" actual
'
test_expect_success 'globally disabled hook can be re-enabled locally' '
test_config_global hook.global-hook.event "test-hook" &&
test_config_global hook.global-hook.command "echo \"global-hook ran\"" &&
test_config_global hook.global-hook.enabled false &&
test_config hook.global-hook.enabled true &&
echo "global-hook ran" >expected &&
git hook run test-hook 2>actual &&
test_cmp expected actual
'
test_expect_success 'git hook run a hook with a bad shebang' '
test_when_finished "rm -rf bad-hooks" &&
mkdir bad-hooks &&
@@ -167,6 +398,7 @@ test_expect_success 'git hook run a hook with a bad shebang' '
'
test_expect_success 'stdin to hooks' '
mkdir -p .git/hooks &&
write_script .git/hooks/test-hook <<-\EOF &&
echo BEGIN stdin
cat
@@ -184,4 +416,141 @@ test_expect_success 'stdin to hooks' '
test_cmp expect actual
'
check_stdout_separate_from_stderr () {
for hook in "$@"
do
# Ensure hook's stdout is only in stdout, not stderr
test_grep "Hook $hook stdout" stdout.actual || return 1
test_grep ! "Hook $hook stdout" stderr.actual || return 1
# Ensure hook's stderr is only in stderr, not stdout
test_grep "Hook $hook stderr" stderr.actual || return 1
test_grep ! "Hook $hook stderr" stdout.actual || return 1
done
}
check_stdout_merged_to_stderr () {
for hook in "$@"
do
# Ensure hook's stdout is only in stderr, not stdout
test_grep "Hook $hook stdout" stderr.actual || return 1
test_grep ! "Hook $hook stdout" stdout.actual || return 1
# Ensure hook's stderr is only in stderr, not stdout
test_grep "Hook $hook stderr" stderr.actual || return 1
test_grep ! "Hook $hook stderr" stdout.actual || return 1
done
}
setup_hooks () {
for hook in "$@"
do
test_hook $hook <<-EOF
echo >&1 Hook $hook stdout
echo >&2 Hook $hook stderr
EOF
done
}
test_expect_success 'client hooks: pre-push expects separate stdout and stderr' '
test_when_finished "rm -f stdout.actual stderr.actual" &&
git init --bare remote &&
git remote add origin remote &&
test_commit A &&
setup_hooks pre-push &&
git push origin HEAD:main >stdout.actual 2>stderr.actual &&
check_stdout_separate_from_stderr pre-push
'
test_expect_success 'client hooks: commit hooks expect stdout redirected to stderr' '
hooks="pre-commit prepare-commit-msg \
commit-msg post-commit \
reference-transaction" &&
setup_hooks $hooks &&
test_when_finished "rm -f stdout.actual stderr.actual" &&
git checkout -B main &&
git checkout -b branch-a &&
test_commit commit-on-branch-a &&
git commit --allow-empty -m "Test" >stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr $hooks
'
test_expect_success 'client hooks: checkout hooks expect stdout redirected to stderr' '
setup_hooks post-checkout reference-transaction &&
test_when_finished "rm -f stdout.actual stderr.actual" &&
git checkout -b new-branch main >stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr post-checkout reference-transaction
'
test_expect_success 'client hooks: merge hooks expect stdout redirected to stderr' '
setup_hooks pre-merge-commit post-merge reference-transaction &&
test_when_finished "rm -f stdout.actual stderr.actual" &&
test_commit new-branch-commit &&
git merge --no-ff branch-a >stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr pre-merge-commit post-merge reference-transaction
'
test_expect_success 'client hooks: post-rewrite hooks expect stdout redirected to stderr' '
setup_hooks post-rewrite reference-transaction &&
test_when_finished "rm -f stdout.actual stderr.actual" &&
git commit --amend --allow-empty --no-edit >stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr post-rewrite reference-transaction
'
test_expect_success 'client hooks: applypatch hooks expect stdout redirected to stderr' '
setup_hooks applypatch-msg pre-applypatch post-applypatch &&
test_when_finished "rm -f stdout.actual stderr.actual" &&
git checkout -b branch-b main &&
test_commit branch-b &&
git format-patch -1 --stdout >patch &&
git checkout -b branch-c main &&
git am patch >stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr applypatch-msg pre-applypatch post-applypatch
'
test_expect_success 'client hooks: rebase hooks expect stdout redirected to stderr' '
setup_hooks pre-rebase &&
test_when_finished "rm -f stdout.actual stderr.actual" &&
git checkout -b branch-d main &&
test_commit branch-d &&
git checkout main &&
test_commit diverge-main &&
git checkout branch-d &&
git rebase main >stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr pre-rebase
'
test_expect_success 'client hooks: post-index-change expects stdout redirected to stderr' '
setup_hooks post-index-change &&
test_when_finished "rm -f stdout.actual stderr.actual" &&
oid=$(git hash-object -w --stdin </dev/null) &&
git update-index --add --cacheinfo 100644 $oid new-file \
>stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr post-index-change
'
test_expect_success 'server hooks expect stdout redirected to stderr' '
test_when_finished "rm -f stdout.actual stderr.actual" &&
git init --bare remote-server &&
git remote add origin-server remote-server &&
cd remote-server &&
setup_hooks pre-receive update post-receive post-update &&
cd .. &&
git push origin-server HEAD:new-branch >stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr pre-receive update post-receive post-update
'
test_expect_success 'server push-to-checkout hook expects stdout redirected to stderr' '
test_when_finished "rm -f stdout.actual stderr.actual" &&
git init server &&
git -C server checkout -b main &&
test_config -C server receive.denyCurrentBranch updateInstead &&
git remote add origin-server-2 server &&
cd server &&
setup_hooks push-to-checkout &&
cd .. &&
git push origin-server-2 HEAD:main >stdout.actual 2>stderr.actual &&
check_stdout_merged_to_stderr push-to-checkout
'
test_done

View File

@@ -1317,65 +1317,85 @@ static void die_with_unpushed_submodules(struct string_list *needs_pushing)
die(_("Aborting."));
}
struct feed_pre_push_hook_data {
struct strbuf buf;
const struct ref *refs;
};
static int pre_push_hook_feed_stdin(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_task_cb)
{
struct feed_pre_push_hook_data *data = pp_task_cb;
const struct ref *r = data->refs;
int ret = 0;
if (!r)
return 1; /* no more refs */
data->refs = r->next;
switch (r->status) {
case REF_STATUS_REJECT_NONFASTFORWARD:
case REF_STATUS_REJECT_REMOTE_UPDATED:
case REF_STATUS_REJECT_STALE:
case REF_STATUS_UPTODATE:
return 0; /* skip refs which won't be pushed */
default:
break;
}
if (!r->peer_ref)
return 0;
strbuf_reset(&data->buf);
strbuf_addf(&data->buf, "%s %s %s %s\n",
r->peer_ref->name, oid_to_hex(&r->new_oid),
r->name, oid_to_hex(&r->old_oid));
ret = write_in_full(hook_stdin_fd, data->buf.buf, data->buf.len);
if (ret < 0 && errno != EPIPE)
return ret; /* We do not mind if a hook does not read all refs. */
return 0;
}
static void *pre_push_hook_data_alloc(void *feed_pipe_ctx)
{
struct feed_pre_push_hook_data *data = xmalloc(sizeof(*data));
strbuf_init(&data->buf, 0);
data->refs = (struct ref *)feed_pipe_ctx;
return data;
}
static void pre_push_hook_data_free(void *data)
{
struct feed_pre_push_hook_data *d = data;
if (!d)
return;
strbuf_release(&d->buf);
free(d);
}
static int run_pre_push_hook(struct transport *transport,
struct ref *remote_refs)
{
int ret = 0, x;
struct ref *r;
struct child_process proc = CHILD_PROCESS_INIT;
struct strbuf buf;
const char *hook_path = find_hook(the_repository, "pre-push");
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
int ret = 0;
if (!hook_path)
return 0;
strvec_push(&opt.args, transport->remote->name);
strvec_push(&opt.args, transport->url);
strvec_push(&proc.args, hook_path);
strvec_push(&proc.args, transport->remote->name);
strvec_push(&proc.args, transport->url);
opt.feed_pipe = pre_push_hook_feed_stdin;
opt.feed_pipe_ctx = remote_refs;
opt.feed_pipe_cb_data_alloc = pre_push_hook_data_alloc;
opt.feed_pipe_cb_data_free = pre_push_hook_data_free;
proc.in = -1;
proc.trace2_hook_name = "pre-push";
/*
* pre-push hooks expect stdout & stderr to be separate, so don't merge
* them to keep backwards compatibility with existing hooks.
*/
opt.stdout_to_stderr = 0;
if (start_command(&proc)) {
finish_command(&proc);
return -1;
}
sigchain_push(SIGPIPE, SIG_IGN);
strbuf_init(&buf, 256);
for (r = remote_refs; r; r = r->next) {
if (!r->peer_ref) continue;
if (r->status == REF_STATUS_REJECT_NONFASTFORWARD) continue;
if (r->status == REF_STATUS_REJECT_STALE) continue;
if (r->status == REF_STATUS_REJECT_REMOTE_UPDATED) continue;
if (r->status == REF_STATUS_UPTODATE) continue;
strbuf_reset(&buf);
strbuf_addf( &buf, "%s %s %s %s\n",
r->peer_ref->name, oid_to_hex(&r->new_oid),
r->name, oid_to_hex(&r->old_oid));
if (write_in_full(proc.in, buf.buf, buf.len) < 0) {
/* We do not mind if a hook does not read all refs. */
if (errno != EPIPE)
ret = -1;
break;
}
}
strbuf_release(&buf);
x = close(proc.in);
if (!ret)
ret = x;
sigchain_pop(SIGPIPE);
x = finish_command(&proc);
if (!ret)
ret = x;
ret = run_hooks_opt(the_repository, "pre-push", &opt);
return ret;
}