Skip to content

Commit dcfb5af

Browse files
10ne1gitster
authored andcommitted
hook: add hook.<event>.enabled switch
Add a hook.<event>.enabled config key that disables all hooks for a given event, when set to false, acting as a high-level switch above the existing per-hook hook.<friendly-name>.enabled. Event-disabled hooks are shown in "git hook list" with an "event-disabled" tab-separated prefix before the name: $ git hook list test-hook event-disabled hook-1 event-disabled hook-2 With --show-scope: $ git hook list --show-scope test-hook local event-disabled hook-1 When a hook is both per-hook disabled and event-disabled, only "event-disabled" is shown: the event-level switch is the more relevant piece of information, and the per-hook "disabled" status will surface once the event is re-enabled. Using an event name as a friendly-name (e.g. hook.<event>.enabled) can cause ambiguity, so a fatal error is issued when using a known event name and a warning is issued for unknown event name, since a collision cannot be detected with certainty for unknown events. Suggested-by: Patrick Steinhardt <ps@pks.im> Suggested-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 2eb541e commit dcfb5af

File tree

7 files changed

+165
-11
lines changed

7 files changed

+165
-11
lines changed

Documentation/config/hook.adoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ hook.<friendly-name>.event::
1515
events, specify the key more than once. An empty value resets
1616
the list of events, clearing any previously defined events for
1717
`hook.<friendly-name>`. See linkgit:git-hook[1].
18+
+
19+
The `<friendly-name>` must not be the same as a known hook event name
20+
(e.g. do not use `hook.pre-commit.event`). Using a known event name as
21+
a friendly-name is a fatal error because it creates an ambiguity with
22+
`hook.<event>.enabled` and `hook.<event>.jobs`. For unknown event names,
23+
a warning is issued when `<friendly-name>` matches the event value.
1824

1925
hook.<friendly-name>.enabled::
2026
Whether the hook `hook.<friendly-name>` is enabled. Defaults to `true`.
@@ -33,6 +39,20 @@ hook.<friendly-name>.parallel::
3339
found in the hooks directory do not need to, and run in parallel when
3440
the effective job count is greater than 1. See linkgit:git-hook[1].
3541

42+
hook.<event>.enabled::
43+
Switch to enable or disable all hooks for the `<event>` hook event.
44+
When set to `false`, no hooks fire for that event, regardless of any
45+
per-hook `hook.<friendly-name>.enabled` settings. Defaults to `true`.
46+
See linkgit:git-hook[1].
47+
+
48+
Note on naming: `<event>` must be the event name (e.g. `pre-commit`),
49+
not a hook friendly-name. Since using a known event name as a
50+
friendly-name is disallowed (see `hook.<friendly-name>.event` above),
51+
there is no ambiguity between event-level and per-hook `.enabled`
52+
settings for known events. For unknown events, if a friendly-name
53+
matches the event name despite the warning, `.enabled` is treated
54+
as per-hook only.
55+
3656
hook.<event>.jobs::
3757
Specifies how many hooks can be run simultaneously for the `<event>`
3858
hook event (e.g. `hook.post-receive.jobs = 4`). Overrides `hook.jobs`

builtin/hook.c

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,22 @@ static int list(int argc, const char **argv, const char *prefix,
8787
const char *name = h->u.configured.friendly_name;
8888
const char *scope = show_scope ?
8989
config_scope_name(h->u.configured.scope) : NULL;
90+
/*
91+
* Show the most relevant disable reason. Event-level
92+
* takes precedence: if the whole event is off, that
93+
* is what the user needs to know. The per-hook
94+
* "disabled" surfaces once the event is re-enabled.
95+
*/
96+
const char *disability =
97+
h->u.configured.event_disabled ? "event-disabled\t" :
98+
h->u.configured.disabled ? "disabled\t" :
99+
"";
90100
if (scope)
91-
printf("%s\t%s%s%c", scope,
92-
h->u.configured.disabled ? "disabled\t" : "",
93-
name, line_terminator);
101+
printf("%s\t%s%s%c", scope, disability, name,
102+
line_terminator);
94103
else
95-
printf("%s%s%c",
96-
h->u.configured.disabled ? "disabled\t" : "",
97-
name, line_terminator);
104+
printf("%s%s%c", disability, name,
105+
line_terminator);
98106
break;
99107
}
100108
default:

hook.c

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ struct hook_config_cache_entry {
133133
* Callback struct to collect all hook.* keys in a single config pass.
134134
* commands: friendly-name to command map.
135135
* event_hooks: event-name to list of friendly-names map.
136-
* disabled_hooks: set of friendly-names with hook.<friendly-name>.enabled = false.
136+
* disabled_hooks: set of all names with hook.<name>.enabled = false; after
137+
* parsing, names that are not friendly-names become event-level
138+
* disables stored in r->disabled_events. This collects all.
137139
* parallel_hooks: friendly-name to parallel flag.
138140
* event_jobs: event-name to per-event jobs count (stored as uintptr_t, NULL == unset).
139141
* jobs: value of the global hook.jobs key. Defaults to 0 if unset (stored in r->hook_jobs).
@@ -189,8 +191,21 @@ static int hook_config_lookup_all(const char *key, const char *value,
189191
strmap_for_each_entry(&data->event_hooks, &iter, e)
190192
unsorted_string_list_remove(e->value, hook_name, 0);
191193
} else {
192-
struct string_list *hooks =
193-
strmap_get(&data->event_hooks, value);
194+
struct string_list *hooks;
195+
196+
if (is_known_hook(hook_name))
197+
die(_("hook friendly-name '%s' collides with "
198+
"a known event name; please choose a "
199+
"different friendly-name"),
200+
hook_name);
201+
202+
if (!strcmp(hook_name, value))
203+
warning(_("hook friendly-name '%s' is the "
204+
"same as its event; this may cause "
205+
"ambiguity with hook.%s.enabled"),
206+
hook_name, hook_name);
207+
208+
hooks = strmap_get(&data->event_hooks, value);
194209

195210
if (!hooks) {
196211
CALLOC_ARRAY(hooks, 1);
@@ -345,6 +360,22 @@ static void build_hook_config_map(struct repository *r, struct strmap *cache)
345360

346361
warn_jobs_on_friendly_names(&cb_data);
347362

363+
/*
364+
* Populate disabled_events: names in disabled_hooks that are not
365+
* friendly-names are event-level switches (hook.<event>.enabled = false).
366+
* Names that are friendly-names are already handled per-hook via the
367+
* hook_config_cache_entry.disabled flag below.
368+
*/
369+
if (r) {
370+
string_list_clear(&r->disabled_events, 0);
371+
string_list_init_dup(&r->disabled_events);
372+
for (size_t i = 0; i < cb_data.disabled_hooks.nr; i++) {
373+
const char *n = cb_data.disabled_hooks.items[i].string;
374+
if (!is_friendly_name(&cb_data, n))
375+
string_list_append(&r->disabled_events, n);
376+
}
377+
}
378+
348379
/* Construct the cache from parsed configs. */
349380
strmap_for_each_entry(&cb_data.event_hooks, &iter, e) {
350381
struct string_list *hook_names = e->value;
@@ -446,6 +477,8 @@ static void list_hooks_add_configured(struct repository *r,
446477
{
447478
struct strmap *cache = get_hook_config_cache(r);
448479
struct string_list *configured_hooks = strmap_get(cache, hookname);
480+
bool event_is_disabled = r ? !!unsorted_string_list_lookup(&r->disabled_events,
481+
hookname) : 0;
449482

450483
/* Iterate through configured hooks and initialize internal states */
451484
for (size_t i = 0; configured_hooks && i < configured_hooks->nr; i++) {
@@ -472,6 +505,7 @@ static void list_hooks_add_configured(struct repository *r,
472505
entry->command ? xstrdup(entry->command) : NULL;
473506
hook->u.configured.scope = entry->scope;
474507
hook->u.configured.disabled = entry->disabled;
508+
hook->u.configured.event_disabled = event_is_disabled;
475509
hook->parallel = entry->parallel;
476510

477511
string_list_append(list, friendly_name)->util = hook;
@@ -484,6 +518,8 @@ static void list_hooks_add_configured(struct repository *r,
484518
if (!r || !r->gitdir) {
485519
hook_cache_clear(cache);
486520
free(cache);
521+
if (r)
522+
string_list_clear(&r->disabled_events, 0);
487523
}
488524
}
489525

@@ -515,7 +551,7 @@ int hook_exists(struct repository *r, const char *name)
515551
for (size_t i = 0; i < hooks->nr; i++) {
516552
struct hook *h = hooks->items[i].util;
517553
if (h->kind == HOOK_TRADITIONAL ||
518-
!h->u.configured.disabled) {
554+
(!h->u.configured.disabled && !h->u.configured.event_disabled)) {
519555
exists = 1;
520556
break;
521557
}
@@ -538,7 +574,8 @@ static int pick_next_hook(struct child_process *cp,
538574
if (hook_cb->hook_to_run_index >= hook_list->nr)
539575
return 0;
540576
h = hook_list->items[hook_cb->hook_to_run_index++].util;
541-
} while (h->kind == HOOK_CONFIGURED && h->u.configured.disabled);
577+
} while (h->kind == HOOK_CONFIGURED &&
578+
(h->u.configured.disabled || h->u.configured.event_disabled));
542579

543580
cp->no_stdin = 1;
544581
strvec_pushv(&cp->env, hook_cb->options->env.v);

hook.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ struct hook {
3232
const char *command;
3333
enum config_scope scope;
3434
bool disabled;
35+
bool event_disabled;
3536
} configured;
3637
} u;
3738

repository.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ void repo_clear(struct repository *repo)
427427
FREE_AND_NULL(repo->hook_config_cache);
428428
}
429429
strmap_clear(&repo->event_jobs, 0); /* values are uintptr_t, not heap ptrs */
430+
string_list_clear(&repo->disabled_events, 0);
430431

431432
if (repo->promisor_remote_config) {
432433
promisor_remote_clear(repo->promisor_remote_config);

repository.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#define REPOSITORY_H
33

44
#include "strmap.h"
5+
#include "string-list.h"
56
#include "repo-settings.h"
67
#include "environment.h"
78

@@ -178,6 +179,9 @@ struct repository {
178179
/* Cached map of event-name -> jobs count (as uintptr_t) from hook.<event>.jobs. */
179180
struct strmap event_jobs;
180181

182+
/* Cached list of event names with hook.<event>.enabled = false. */
183+
struct string_list disabled_events;
184+
181185
/* Configurations related to promisor remotes. */
182186
char *repository_format_partial_clone;
183187
struct promisor_remote_config *promisor_remote_config;

t/t1800-hook.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,4 +1058,87 @@ test_expect_success 'hook.<event>.jobs does not warn for a real event name' '
10581058
test_grep ! "friendly-name" err
10591059
'
10601060

1061+
test_expect_success 'hook.<event>.enabled=false skips all hooks for event' '
1062+
test_config hook.hook-1.event test-hook &&
1063+
test_config hook.hook-1.command "echo ran" &&
1064+
test_config hook.test-hook.enabled false &&
1065+
git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1066+
test_must_be_empty out
1067+
'
1068+
1069+
test_expect_success 'hook.<event>.enabled=true does not suppress hooks' '
1070+
test_config hook.hook-1.event test-hook &&
1071+
test_config hook.hook-1.command "echo ran" &&
1072+
test_config hook.test-hook.enabled true &&
1073+
git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1074+
test_grep "ran" err
1075+
'
1076+
1077+
test_expect_success 'hook.<event>.enabled=false does not affect other events' '
1078+
test_config hook.hook-1.event test-hook &&
1079+
test_config hook.hook-1.command "echo ran" &&
1080+
test_config hook.other-event.enabled false &&
1081+
git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1082+
test_grep "ran" err
1083+
'
1084+
1085+
test_expect_success 'hook.<friendly-name>.enabled=false still disables that hook' '
1086+
test_config hook.hook-1.event test-hook &&
1087+
test_config hook.hook-1.command "echo hook-1" &&
1088+
test_config hook.hook-2.event test-hook &&
1089+
test_config hook.hook-2.command "echo hook-2" &&
1090+
test_config hook.hook-1.enabled false &&
1091+
git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1092+
test_grep ! "hook-1" err &&
1093+
test_grep "hook-2" err
1094+
'
1095+
1096+
test_expect_success 'git hook list shows event-disabled hooks as event-disabled' '
1097+
test_config hook.hook-1.event test-hook &&
1098+
test_config hook.hook-1.command "echo ran" &&
1099+
test_config hook.hook-2.event test-hook &&
1100+
test_config hook.hook-2.command "echo ran" &&
1101+
test_config hook.test-hook.enabled false &&
1102+
git hook list --allow-unknown-hook-name test-hook >actual &&
1103+
test_grep "^event-disabled hook-1$" actual &&
1104+
test_grep "^event-disabled hook-2$" actual
1105+
'
1106+
1107+
test_expect_success 'git hook list shows scope with event-disabled' '
1108+
test_config hook.hook-1.event test-hook &&
1109+
test_config hook.hook-1.command "echo ran" &&
1110+
test_config hook.test-hook.enabled false &&
1111+
git hook list --allow-unknown-hook-name --show-scope test-hook >actual &&
1112+
test_grep "^local event-disabled hook-1$" actual
1113+
'
1114+
1115+
test_expect_success 'git hook list still shows hooks when event is disabled' '
1116+
test_config hook.hook-1.event test-hook &&
1117+
test_config hook.hook-1.command "echo ran" &&
1118+
test_config hook.test-hook.enabled false &&
1119+
git hook list --allow-unknown-hook-name test-hook >actual &&
1120+
test_grep "event-disabled" actual
1121+
'
1122+
1123+
test_expect_success 'friendly-name matching known event name is rejected' '
1124+
test_config hook.pre-commit.event pre-commit &&
1125+
test_config hook.pre-commit.command "echo oops" &&
1126+
test_must_fail git hook run pre-commit 2>err &&
1127+
test_grep "collides with a known event name" err
1128+
'
1129+
1130+
test_expect_success 'friendly-name matching known event name is rejected even for different event' '
1131+
test_config hook.pre-commit.event post-commit &&
1132+
test_config hook.pre-commit.command "echo oops" &&
1133+
test_must_fail git hook run post-commit 2>err &&
1134+
test_grep "collides with a known event name" err
1135+
'
1136+
1137+
test_expect_success 'friendly-name matching unknown event warns' '
1138+
test_config hook.test-hook.event test-hook &&
1139+
test_config hook.test-hook.command "echo ran" &&
1140+
git hook run --allow-unknown-hook-name test-hook >out 2>err &&
1141+
test_grep "same as its event" err
1142+
'
1143+
10611144
test_done

0 commit comments

Comments
 (0)