Skip to content

Commit 7708fca

Browse files
nasamuffingitster
authored andcommitted
hook: include hooks from the config
Teach the hook.[hc] library to parse configs to populate the list of hooks to run for a given event. Multiple commands can be specified for a given hook by providing "hook.<friendly-name>.command = <path-to-hook>" and "hook.<friendly-name>.event = <hook-event>" lines. Hooks will be started in config order of the "hook.<name>.event" lines and will be run sequentially (.jobs == 1) like before. Running the hooks in parallel will be enabled in a future patch. Examples: $ git config --get-regexp "^hook\." hook.bar.command=~/bar.sh hook.bar.event=pre-commit # Will run ~/bar.sh, then .git/hooks/pre-commit $ git hook run pre-commit Signed-off-by: Emily Shaffer <emilyshaffer@google.com> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 2c62320 commit 7708fca

4 files changed

Lines changed: 353 additions & 8 deletions

File tree

Documentation/config/hook.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
hook.<name>.command::
2+
A command to execute whenever `hook.<name>` is invoked. `<name>` should
3+
be a unique "friendly" name which you can use to identify this hook
4+
command. (You can specify when to invoke this command with
5+
`hook.<name>.event`.) The value can be an executable on your device or a
6+
oneliner for your shell. If more than one value is specified for the
7+
same `<name>`, the last value parsed will be the only command executed.
8+
See linkgit:git-hook[1].
9+
10+
hook.<name>.event::
11+
The hook events which should invoke `hook.<name>`. `<name>` should be a
12+
unique "friendly" name which you can use to identify this hook. The
13+
value should be the name of a hook event, like "pre-commit" or "update".
14+
(See linkgit:githooks[5] for a complete list of hooks Git knows about.)
15+
On the specified event, the associated `hook.<name>.command` will be
16+
executed. More than one event can be specified if you wish for
17+
`hook.<name>` to execute on multiple events. See linkgit:git-hook[1].

Documentation/git-hook.adoc

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,94 @@ DESCRIPTION
1717
A command interface for running git hooks (see linkgit:githooks[5]),
1818
for use by other scripted git commands.
1919

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 hook.<some-name>.command <path-to-script>
91+
git config --add 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 hook.my-script.command ~/my-script.sh
99+
git config --add hook.my-script.event pre-commit
100+
----
101+
20102
SUBCOMMANDS
21103
-----------
22104
23105
run::
24-
Run the `<hook-name>` hook. See linkgit:githooks[5] for
25-
supported hook names.
106+
Runs hooks configured for `<hook-name>`, in the order they are
107+
discovered during the config parse.
26108
+
27109
28110
Any positional arguments to the hook should be passed after a
@@ -46,6 +128,46 @@ OPTIONS
46128
tools that want to do a blind one-shot run of a hook that may
47129
or may not be present.
48130

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

hook.c

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,74 @@ const char *find_hook(struct repository *r, const char *name)
4747
return path.buf;
4848
}
4949

50+
struct hook_config_cb
51+
{
52+
const char *hook_event;
53+
struct string_list *list;
54+
};
55+
56+
/*
57+
* Callback for git_config which adds configured hooks to a hook list. Hooks
58+
* can be configured by specifying both hook.<friendly-name>.command = <path>
59+
* and hook.<friendly-name>.event = <hook-event>.
60+
*/
61+
static int hook_config_lookup(const char *key, const char *value,
62+
const struct config_context *ctx UNUSED,
63+
void *cb_data)
64+
{
65+
struct hook_config_cb *data = cb_data;
66+
const char *name, *event_key;
67+
size_t name_len = 0;
68+
struct string_list_item *item;
69+
char *hook_name;
70+
71+
/*
72+
* Don't bother doing the expensive parse if there's no
73+
* chance that the config matches 'hook.myhook.event = hook_event'.
74+
*/
75+
if (!value || strcmp(value, data->hook_event))
76+
return 0;
77+
78+
/* Look for "hook.friendly-name.event = hook_event" */
79+
if (parse_config_key(key, "hook", &name, &name_len, &event_key) ||
80+
strcmp(event_key, "event"))
81+
return 0;
82+
83+
/* Extract the hook name */
84+
hook_name = xmemdupz(name, name_len);
85+
86+
/* Remove the hook if already in the list, so we append in config order. */
87+
if ((item = unsorted_string_list_lookup(data->list, hook_name)))
88+
unsorted_string_list_delete_item(data->list, item - data->list->items, 0);
89+
90+
/* The list takes ownership of hook_name, so append with nodup */
91+
string_list_append_nodup(data->list, hook_name);
92+
93+
return 0;
94+
}
95+
5096
struct string_list *list_hooks(struct repository *r, const char *hookname)
5197
{
52-
struct string_list *hook_head;
98+
struct hook_config_cb cb_data;
5399

54100
if (!hookname)
55101
BUG("null hookname was provided to hook_list()!");
56102

57-
hook_head = xmalloc(sizeof(struct string_list));
58-
string_list_init_dup(hook_head);
103+
cb_data.hook_event = hookname;
104+
cb_data.list = xmalloc(sizeof(struct string_list));
105+
string_list_init_dup(cb_data.list);
106+
107+
/* Add the hooks from the config, e.g. hook.myhook.event = pre-commit */
108+
repo_config(r, hook_config_lookup, &cb_data);
59109

60110
/*
61111
* Add the default hook from hookdir. It does not have a friendly name
62112
* like the hooks specified via configs, so add it with an empty name.
63113
*/
64114
if (r->gitdir && find_hook(r, hookname))
65-
string_list_append(hook_head, "");
115+
string_list_append(cb_data.list, "");
66116

67-
return hook_head;
117+
return cb_data.list;
68118
}
69119

70120
int hook_exists(struct repository *r, const char *name)
@@ -125,6 +175,24 @@ static int pick_next_hook(struct child_process *cp,
125175
hook_path = absolute_path(hook_path);
126176

127177
strvec_push(&cp->args, hook_path);
178+
} else {
179+
/* ...from config */
180+
struct strbuf cmd_key = STRBUF_INIT;
181+
char *command = NULL;
182+
183+
/* to enable oneliners, let config-specified hooks run in shell. */
184+
cp->use_shell = true;
185+
186+
strbuf_addf(&cmd_key, "hook.%s.command", to_run->string);
187+
if (repo_config_get_string(hook_cb->repository,
188+
cmd_key.buf, &command)) {
189+
die(_("'hook.%s.command' must be configured or"
190+
"'hook.%s.event' must be removed; aborting.\n"),
191+
to_run->string, to_run->string);
192+
}
193+
strbuf_release(&cmd_key);
194+
195+
strvec_push_nodup(&cp->args, command);
128196
}
129197

130198
if (!cp->args.nr)

0 commit comments

Comments
 (0)