Skip to content

Commit 86cf051

Browse files
committed
Merge branch 'ar/config-hooks' into seen
* ar/config-hooks: 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
2 parents 3b8cd88 + 4b12cd3 commit 86cf051

File tree

12 files changed

+990
-62
lines changed

12 files changed

+990
-62
lines changed

Documentation/config/hook.adoc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
hook.<name>.command::
2+
The command to execute for `hook.<name>`. `<name>` is a unique
3+
"friendly" name that identifies this hook. (The hook events that
4+
trigger the command are configured with `hook.<name>.event`.) The
5+
value can be an executable path or a shell oneliner. If more than
6+
one value is specified for the same `<name>`, only the last value
7+
parsed is used. See linkgit:git-hook[1].
8+
9+
hook.<name>.event::
10+
The hook events that trigger `hook.<name>`. The value is the name
11+
of a hook event, like "pre-commit" or "update". (See
12+
linkgit:githooks[5] for a complete list of hook events.) On the
13+
specified event, the associated `hook.<name>.command` is executed.
14+
This is a multi-valued key. To run `hook.<name>` on multiple
15+
events, specify the key more than once. An empty value resets
16+
the list of events, clearing any previously defined events for
17+
`hook.<name>`. See linkgit:git-hook[1].
18+
19+
hook.<name>.enabled::
20+
Whether the hook `hook.<name>` is enabled. Defaults to `true`.
21+
Set to `false` to disable the hook without removing its
22+
configuration. This is particularly useful when a hook is defined
23+
in a system or global config file and needs to be disabled for a
24+
specific repository. See linkgit:git-hook[1].

Documentation/git-hook.adoc

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,115 @@ SYNOPSIS
99
--------
1010
[verse]
1111
'git hook' run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]
12+
'git hook' list [-z] <hook-name>
1213

1314
DESCRIPTION
1415
-----------
1516

1617
A command interface for running git hooks (see linkgit:githooks[5]),
1718
for use by other scripted git commands.
1819

20+
This command parses the default configuration files for sets of configs like
21+
so:
22+
23+
[hook "linter"]
24+
event = pre-commit
25+
command = ~/bin/linter --cpp20
26+
27+
In this example, `[hook "linter"]` represents one script - `~/bin/linter
28+
--cpp20` - which can be shared by many repos, and even by many hook events, if
29+
appropriate.
30+
31+
To add an unrelated hook which runs on a different event, for example a
32+
spell-checker for your commit messages, you would write a configuration like so:
33+
34+
[hook "linter"]
35+
event = pre-commit
36+
command = ~/bin/linter --cpp20
37+
[hook "spellcheck"]
38+
event = commit-msg
39+
command = ~/bin/spellchecker
40+
41+
With this config, when you run 'git commit', first `~/bin/linter --cpp20` will
42+
have a chance to check your files to be committed (during the `pre-commit` hook
43+
event`), and then `~/bin/spellchecker` will have a chance to check your commit
44+
message (during the `commit-msg` hook event).
45+
46+
Commands are run in the order Git encounters their associated
47+
`hook.<name>.event` configs during the configuration parse (see
48+
linkgit:git-config[1]). Although multiple `hook.linter.event` configs can be
49+
added, only one `hook.linter.command` event is valid - Git uses "last-one-wins"
50+
to determine which command to run.
51+
52+
So if you wanted your linter to run when you commit as well as when you push,
53+
you would configure it like so:
54+
55+
[hook "linter"]
56+
event = pre-commit
57+
event = pre-push
58+
command = ~/bin/linter --cpp20
59+
60+
With this config, `~/bin/linter --cpp20` would be run by Git before a commit is
61+
generated (during `pre-commit`) as well as before a push is performed (during
62+
`pre-push`).
63+
64+
And if you wanted to run your linter as well as a secret-leak detector during
65+
only the "pre-commit" hook event, you would configure it instead like so:
66+
67+
[hook "linter"]
68+
event = pre-commit
69+
command = ~/bin/linter --cpp20
70+
[hook "no-leaks"]
71+
event = pre-commit
72+
command = ~/bin/leak-detector
73+
74+
With this config, before a commit is generated (during `pre-commit`), Git would
75+
first start `~/bin/linter --cpp20` and second start `~/bin/leak-detector`. It
76+
would evaluate the output of each when deciding whether to proceed with the
77+
commit.
78+
79+
For a full list of hook events which you can set your `hook.<name>.event` to,
80+
and how hooks are invoked during those events, see linkgit:githooks[5].
81+
82+
Git will ignore any `hook.<name>.event` that specifies an event it doesn't
83+
recognize. This is intended so that tools which wrap Git can use the hook
84+
infrastructure to run their own hooks; see "WRAPPERS" for more guidance.
85+
86+
In general, when instructions suggest adding a script to
87+
`.git/hooks/<hook-event>`, you can specify it in the config instead by running:
88+
89+
----
90+
git config set hook.<some-name>.command <path-to-script>
91+
git config set --append hook.<some-name>.event <hook-event>
92+
----
93+
94+
This way you can share the script between multiple repos. That is, `cp
95+
~/my-script.sh ~/project/.git/hooks/pre-commit` would become:
96+
97+
----
98+
git config set hook.my-script.command ~/my-script.sh
99+
git config set --append hook.my-script.event pre-commit
100+
----
101+
19102
SUBCOMMANDS
20103
-----------
21104
22105
run::
23-
Run the `<hook-name>` hook. See linkgit:githooks[5] for
24-
supported hook names.
106+
Runs hooks configured for `<hook-name>`, in the order they are
107+
discovered during the config parse. The default `<hook-name>` from
108+
the hookdir is run last. See linkgit:githooks[5] for supported
109+
hook names.
25110
+
26111
27112
Any positional arguments to the hook should be passed after a
28113
mandatory `--` (or `--end-of-options`, see linkgit:gitcli[7]). See
29114
linkgit:githooks[5] for arguments hooks might expect (if any).
30115
116+
list [-z]::
117+
Print a list of hooks which will be run on `<hook-name>` event. If no
118+
hooks are configured for that event, print a warning and return 1.
119+
Use `-z` to terminate output lines with NUL instead of newlines.
120+
31121
OPTIONS
32122
-------
33123

@@ -41,6 +131,49 @@ OPTIONS
41131
tools that want to do a blind one-shot run of a hook that may
42132
or may not be present.
43133

134+
-z::
135+
Terminate "list" output lines with NUL instead of newlines.
136+
137+
WRAPPERS
138+
--------
139+
140+
`git hook run` has been designed to make it easy for tools which wrap Git to
141+
configure and execute hooks using the Git hook infrastructure. It is possible to
142+
provide arguments and stdin via the command line, as well as specifying parallel
143+
or series execution if the user has provided multiple hooks.
144+
145+
Assuming your wrapper wants to support a hook named "mywrapper-start-tests", you
146+
can have your users specify their hooks like so:
147+
148+
[hook "setup-test-dashboard"]
149+
event = mywrapper-start-tests
150+
command = ~/mywrapper/setup-dashboard.py --tap
151+
152+
Then, in your 'mywrapper' tool, you can invoke any users' configured hooks by
153+
running:
154+
155+
----
156+
git hook run mywrapper-start-tests \
157+
# providing something to stdin
158+
--stdin some-tempfile-123 \
159+
# execute hooks in serial
160+
# plus some arguments of your own...
161+
-- \
162+
--testname bar \
163+
baz
164+
----
165+
166+
Take care to name your wrapper's hook events in a way which is unlikely to
167+
overlap with Git's native hooks (see linkgit:githooks[5]) - a hook event named
168+
`mywrappertool-validate-commit` is much less likely to be added to native Git
169+
than a hook event named `validate-commit`. If Git begins to use a hook event
170+
named the same thing as your wrapper hook, it may invoke your users' hooks in
171+
unintended and unsupported ways.
172+
173+
CONFIGURATION
174+
-------------
175+
include::config/hook.adoc[]
176+
44177
SEE ALSO
45178
--------
46179
linkgit:githooks[5]

builtin/hook.c

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
#include "hook.h"
77
#include "parse-options.h"
88
#include "strvec.h"
9+
#include "abspath.h"
910

1011
#define BUILTIN_HOOK_RUN_USAGE \
1112
N_("git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]")
13+
#define BUILTIN_HOOK_LIST_USAGE \
14+
N_("git hook list [-z] <hook-name>")
1215

1316
static const char * const builtin_hook_usage[] = {
1417
BUILTIN_HOOK_RUN_USAGE,
18+
BUILTIN_HOOK_LIST_USAGE,
1519
NULL
1620
};
1721

@@ -20,6 +24,67 @@ static const char * const builtin_hook_run_usage[] = {
2024
NULL
2125
};
2226

27+
static int list(int argc, const char **argv, const char *prefix,
28+
struct repository *repo)
29+
{
30+
static const char *const builtin_hook_list_usage[] = {
31+
BUILTIN_HOOK_LIST_USAGE,
32+
NULL
33+
};
34+
struct string_list *head;
35+
struct string_list_item *item;
36+
const char *hookname = NULL;
37+
int line_terminator = '\n';
38+
int ret = 0;
39+
40+
struct option list_options[] = {
41+
OPT_SET_INT('z', NULL, &line_terminator,
42+
N_("use NUL as line terminator"), '\0'),
43+
OPT_END(),
44+
};
45+
46+
argc = parse_options(argc, argv, prefix, list_options,
47+
builtin_hook_list_usage, 0);
48+
49+
/*
50+
* The only unnamed argument provided should be the hook-name; if we add
51+
* arguments later they probably should be caught by parse_options.
52+
*/
53+
if (argc != 1)
54+
usage_msg_opt(_("You must specify a hook event name to list."),
55+
builtin_hook_list_usage, list_options);
56+
57+
hookname = argv[0];
58+
59+
head = list_hooks(repo, hookname, NULL);
60+
61+
if (!head->nr) {
62+
warning(_("No hooks found for event '%s'"), hookname);
63+
ret = 1; /* no hooks found */
64+
goto cleanup;
65+
}
66+
67+
for_each_string_list_item(item, head) {
68+
struct hook *h = item->util;
69+
70+
switch (h->kind) {
71+
case HOOK_TRADITIONAL:
72+
printf("%s%c", _("hook from hookdir"), line_terminator);
73+
break;
74+
case HOOK_CONFIGURED:
75+
printf("%s%c", h->u.configured.friendly_name, line_terminator);
76+
break;
77+
default:
78+
BUG("unknown hook kind");
79+
}
80+
}
81+
82+
cleanup:
83+
hook_list_clear(head, NULL);
84+
free(head);
85+
return ret;
86+
}
87+
2388
static int run(int argc, const char **argv, const char *prefix,
2489
struct repository *repo UNUSED)
2590
{
@@ -77,6 +142,7 @@ int cmd_hook(int argc,
77142
parse_opt_subcommand_fn *fn = NULL;
78143
struct option builtin_hook_options[] = {
79144
OPT_SUBCOMMAND("run", &fn, run),
145+
OPT_SUBCOMMAND("list", &fn, list),
80146
OPT_END(),
81147
};
82148

builtin/receive-pack.c

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -901,14 +901,34 @@ static int feed_receive_hook_cb(int hook_stdin_fd, void *pp_cb UNUSED, void *pp_
901901
return state->cmd ? 0 : 1; /* 0 = more to come, 1 = EOF */
902902
}
903903

904+
static void *receive_hook_feed_state_alloc(void *feed_pipe_ctx)
905+
{
906+
struct receive_hook_feed_state *init_state = feed_pipe_ctx;
907+
struct receive_hook_feed_state *data = xcalloc(1, sizeof(*data));
908+
data->report = init_state->report;
909+
data->cmd = init_state->cmd;
910+
data->skip_broken = init_state->skip_broken;
911+
strbuf_init(&data->buf, 0);
912+
return data;
913+
}
914+
915+
static void receive_hook_feed_state_free(void *data)
916+
{
917+
struct receive_hook_feed_state *d = data;
918+
if (!d)
919+
return;
920+
strbuf_release(&d->buf);
921+
free(d);
922+
}
923+
904924
static int run_receive_hook(struct command *commands,
905925
const char *hook_name,
906926
int skip_broken,
907927
const struct string_list *push_options)
908928
{
909929
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
910930
struct command *iter = commands;
911-
struct receive_hook_feed_state feed_state;
931+
struct receive_hook_feed_state feed_init_state = { 0 };
912932
struct async sideband_async;
913933
int sideband_async_started = 0;
914934
int saved_stderr = -1;
@@ -938,16 +958,15 @@ static int run_receive_hook(struct command *commands,
938958
prepare_sideband_async(&sideband_async, &saved_stderr, &sideband_async_started);
939959

940960
/* set up stdin callback */
941-
feed_state.cmd = commands;
942-
feed_state.skip_broken = skip_broken;
943-
feed_state.report = NULL;
944-
strbuf_init(&feed_state.buf, 0);
945-
opt.feed_pipe_cb_data = &feed_state;
961+
feed_init_state.cmd = commands;
962+
feed_init_state.skip_broken = skip_broken;
963+
opt.feed_pipe_ctx = &feed_init_state;
946964
opt.feed_pipe = feed_receive_hook_cb;
965+
opt.feed_pipe_cb_data_alloc = receive_hook_feed_state_alloc;
966+
opt.feed_pipe_cb_data_free = receive_hook_feed_state_free;
947967

948968
ret = run_hooks_opt(the_repository, hook_name, &opt);
949969

950-
strbuf_release(&feed_state.buf);
951970
finish_sideband_async(&sideband_async, saved_stderr, sideband_async_started);
952971

953972
return ret;

git.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ static struct cmd_struct commands[] = {
587587
{ "hash-object", cmd_hash_object },
588588
{ "help", cmd_help },
589589
{ "history", cmd_history, RUN_SETUP },
590-
{ "hook", cmd_hook, RUN_SETUP },
590+
{ "hook", cmd_hook, RUN_SETUP_GENTLY },
591591
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
592592
{ "init", cmd_init_db },
593593
{ "init-db", cmd_init_db },

0 commit comments

Comments
 (0)