Skip to content

Commit 869c011

Browse files
Ben Peartdscho
authored andcommitted
gvfs: add global command pre and post hook procs
This adds hard-coded call to GVFS.hooks.exe before and after each Git command runs. To make sure that this is only called on repositories cloned with GVFS, we test for the tell-tale .gvfs. 2021-10-30: Recent movement of find_hook() to hook.c required moving these changes out of run-command.c to hook.c. 2025-11-06: The `warn_on_auto_comment_char` hack is so ugly that it forces us to pile similarly ugly code on top because that hack _expects_ that the config has not been read when `cmd_commit()`, `cmd_revert()`, `cmd_cherry_pick()`, `cmd_merge()`, or `cmd_rebase()` set that flag. But with the `pre_command()` hook already run, that assumption is incorrect. Signed-off-by: Ben Peart <Ben.Peart@microsoft.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent fce7f0f commit 869c011

10 files changed

Lines changed: 217 additions & 4 deletions

File tree

‎builtin/commit.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1802,6 +1802,7 @@ int cmd_commit(int argc,
18021802

18031803
#ifndef WITH_BREAKING_CHANGES
18041804
warn_on_auto_comment_char = true;
1805+
repo_config_clear(the_repository);
18051806
#endif /* !WITH_BREAKING_CHANGES */
18061807
prepare_repo_settings(the_repository);
18071808
the_repository->settings.command_requires_full_index = 0;

‎builtin/merge.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,7 @@ int cmd_merge(int argc,
13811381

13821382
#ifndef WITH_BREAKING_CHANGES
13831383
warn_on_auto_comment_char = true;
1384+
repo_config_clear(the_repository);
13841385
#endif /* !WITH_BREAKING_CHANGES */
13851386
prepare_repo_settings(the_repository);
13861387
the_repository->settings.command_requires_full_index = 0;

‎builtin/rebase.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,7 @@ int cmd_rebase(int argc,
12431243

12441244
#ifndef WITH_BREAKING_CHANGES
12451245
warn_on_auto_comment_char = true;
1246+
repo_config_clear(the_repository);
12461247
#endif /* !WITH_BREAKING_CHANGES */
12471248
prepare_repo_settings(the_repository);
12481249
the_repository->settings.command_requires_full_index = 0;

‎builtin/revert.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "rerere.h"
1111
#include "sequencer.h"
1212
#include "branch.h"
13+
#include "config.h"
1314

1415
/*
1516
* This implements the builtins revert and cherry-pick.
@@ -288,6 +289,7 @@ int cmd_revert(int argc,
288289

289290
#ifndef WITH_BREAKING_CHANGES
290291
warn_on_auto_comment_char = true;
292+
repo_config_clear(the_repository);
291293
#endif /* !WITH_BREAKING_CHANGES */
292294
opts.action = REPLAY_REVERT;
293295
sequencer_init_config(&opts);
@@ -308,6 +310,7 @@ struct repository *repo UNUSED)
308310

309311
#ifndef WITH_BREAKING_CHANGES
310312
warn_on_auto_comment_char = true;
313+
repo_config_clear(the_repository);
311314
#endif /* !WITH_BREAKING_CHANGES */
312315
opts.action = REPLAY_PICK;
313316
sequencer_init_config(&opts);

‎git.c‎

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#include "shallow.h"
1818
#include "trace.h"
1919
#include "trace2.h"
20+
#include "dir.h"
21+
#include "hook.h"
2022

2123
#define RUN_SETUP (1<<0)
2224
#define RUN_SETUP_GENTLY (1<<1)
@@ -463,6 +465,67 @@ static int handle_alias(struct strvec *args, struct string_list *expanded_aliase
463465
return ret;
464466
}
465467

468+
/* Runs pre/post-command hook */
469+
static struct strvec sargv = STRVEC_INIT;
470+
static int run_post_hook = 0;
471+
static int exit_code = -1;
472+
473+
static int run_pre_command_hook(struct repository *r, const char **argv)
474+
{
475+
char *lock;
476+
int ret = 0;
477+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
478+
479+
/*
480+
* Ensure the global pre/post command hook is only called for
481+
* the outer command and not when git is called recursively
482+
* or spawns multiple commands (like with the alias command)
483+
*/
484+
lock = getenv("COMMAND_HOOK_LOCK");
485+
if (lock && !strcmp(lock, "true"))
486+
return 0;
487+
setenv("COMMAND_HOOK_LOCK", "true", 1);
488+
489+
/* call the hook proc */
490+
strvec_pushv(&sargv, argv);
491+
strvec_pushv(&opt.args, sargv.v);
492+
ret = run_hooks_opt(r, "pre-command", &opt);
493+
494+
if (!ret)
495+
run_post_hook = 1;
496+
return ret;
497+
}
498+
499+
static int run_post_command_hook(struct repository *r)
500+
{
501+
char *lock;
502+
int ret = 0;
503+
struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
504+
505+
/*
506+
* Only run post_command if pre_command succeeded in this process
507+
*/
508+
if (!run_post_hook)
509+
return 0;
510+
lock = getenv("COMMAND_HOOK_LOCK");
511+
if (!lock || strcmp(lock, "true"))
512+
return 0;
513+
514+
strvec_pushv(&opt.args, sargv.v);
515+
strvec_pushf(&opt.args, "--exit_code=%u", exit_code);
516+
ret = run_hooks_opt(r, "post-command", &opt);
517+
518+
run_post_hook = 0;
519+
strvec_clear(&sargv);
520+
setenv("COMMAND_HOOK_LOCK", "false", 1);
521+
return ret;
522+
}
523+
524+
static void post_command_hook_atexit(void)
525+
{
526+
run_post_command_hook(the_repository);
527+
}
528+
466529
static int run_builtin(struct cmd_struct *p, int argc, const char **argv, struct repository *repo)
467530
{
468531
int status, help;
@@ -499,16 +562,21 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv, struct
499562
if (!help && p->option & NEED_WORK_TREE)
500563
setup_work_tree();
501564

565+
if (run_pre_command_hook(the_repository, argv))
566+
die("pre-command hook aborted command");
567+
502568
trace_argv_printf(argv, "trace: built-in: git");
503569
trace2_cmd_name(p->cmd);
504570

505571
validate_cache_entries(repo->index);
506-
status = p->fn(argc, argv, prefix, no_repo ? NULL : repo);
572+
exit_code = status = p->fn(argc, argv, prefix, no_repo ? NULL : repo);
507573
validate_cache_entries(repo->index);
508574

509575
if (status)
510576
return status;
511577

578+
run_post_command_hook(the_repository);
579+
512580
/* Somebody closed stdout? */
513581
if (fstat(fileno(stdout), &st))
514582
return 0;
@@ -808,13 +876,16 @@ static void execv_dashed_external(const char **argv)
808876
*/
809877
trace_argv_printf(cmd.args.v, "trace: exec:");
810878

879+
if (run_pre_command_hook(the_repository, cmd.args.v))
880+
die("pre-command hook aborted command");
881+
811882
/*
812883
* If we fail because the command is not found, it is
813884
* OK to return. Otherwise, we just pass along the status code,
814885
* or our usual generic code if we were not even able to exec
815886
* the program.
816887
*/
817-
status = run_command(&cmd);
888+
exit_code = status = run_command(&cmd);
818889

819890
/*
820891
* If the child process ran and we are now going to exit, emit a
@@ -825,6 +896,8 @@ static void execv_dashed_external(const char **argv)
825896
exit(status);
826897
else if (errno != ENOENT)
827898
exit(128);
899+
900+
run_post_command_hook(the_repository);
828901
}
829902

830903
static int is_deprecated_command(const char *cmd)
@@ -930,6 +1003,7 @@ int cmd_main(int argc, const char **argv)
9301003
}
9311004

9321005
trace_command_performance(argv);
1006+
atexit(post_command_hook_atexit);
9331007

9341008
/*
9351009
* "git-xxxx" is the same as "git xxxx", but we obviously:
@@ -957,10 +1031,14 @@ int cmd_main(int argc, const char **argv)
9571031
if (!argc) {
9581032
/* The user didn't specify a command; give them help */
9591033
commit_pager_choice();
1034+
if (run_pre_command_hook(the_repository, argv))
1035+
die("pre-command hook aborted command");
9601036
printf(_("usage: %s\n\n"), git_usage_string);
9611037
list_common_cmds_help();
9621038
printf("\n%s\n", _(git_more_info_string));
963-
exit(1);
1039+
exit_code = 1;
1040+
run_post_command_hook(the_repository);
1041+
exit(exit_code);
9641042
}
9651043

9661044
if (!strcmp("--version", argv[0]) || !strcmp("-v", argv[0]))

‎hook.c‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
#define USE_THE_REPOSITORY_VARIABLE
2+
13
#include "git-compat-util.h"
24
#include "abspath.h"
5+
#include "environment.h"
36
#include "advice.h"
47
#include "gettext.h"
58
#include "hook.h"
@@ -10,13 +13,66 @@
1013
#include "environment.h"
1114
#include "setup.h"
1215

16+
static int early_hooks_path_config(const char *var, const char *value,
17+
const struct config_context *ctx UNUSED, void *cb)
18+
{
19+
if (!strcmp(var, "core.hookspath"))
20+
return git_config_pathname((char **)cb, var, value);
21+
22+
return 0;
23+
}
24+
25+
/* Discover the hook before setup_git_directory() was called */
26+
static const char *hook_path_early(const char *name, struct strbuf *result)
27+
{
28+
static struct strbuf hooks_dir = STRBUF_INIT;
29+
static int initialized;
30+
31+
if (initialized < 0)
32+
return NULL;
33+
34+
if (!initialized) {
35+
struct strbuf gitdir = STRBUF_INIT, commondir = STRBUF_INIT;
36+
char *early_hooks_dir = NULL;
37+
38+
if (discover_git_directory(&commondir, &gitdir) < 0) {
39+
strbuf_release(&gitdir);
40+
strbuf_release(&commondir);
41+
initialized = -1;
42+
return NULL;
43+
}
44+
45+
read_early_config(the_repository, early_hooks_path_config, &early_hooks_dir);
46+
if (!early_hooks_dir)
47+
strbuf_addf(&hooks_dir, "%s/hooks/", commondir.buf);
48+
else {
49+
strbuf_add_absolute_path(&hooks_dir, early_hooks_dir);
50+
free(early_hooks_dir);
51+
strbuf_addch(&hooks_dir, '/');
52+
}
53+
54+
strbuf_release(&gitdir);
55+
strbuf_release(&commondir);
56+
57+
initialized = 1;
58+
}
59+
60+
strbuf_addf(result, "%s%s", hooks_dir.buf, name);
61+
return result->buf;
62+
}
63+
1364
const char *find_hook(struct repository *r, const char *name)
1465
{
1566
static struct strbuf path = STRBUF_INIT;
1667

1768
int found_hook;
1869

19-
repo_git_path_replace(r, &path, "hooks/%s", name);
70+
strbuf_reset(&path);
71+
if (have_git_dir())
72+
repo_git_path_replace(r, &path, "hooks/%s", name);
73+
else if (!hook_path_early(name, &path))
74+
return NULL;
75+
2076
found_hook = access(path.buf, X_OK) >= 0;
2177
#ifdef STRIP_EXTENSION
2278
if (!found_hook) {

‎t/meson.build‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ integration_tests = [
137137
't0301-credential-cache.sh',
138138
't0302-credential-store.sh',
139139
't0303-credential-external.sh',
140+
't0400-pre-command-hook.sh',
141+
't0401-post-command-hook.sh',
140142
't0410-partial-clone.sh',
141143
't0411-clone-from-partial.sh',
142144
't0450-txt-doc-vs-help.sh',

‎t/t0400-pre-command-hook.sh‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/sh
2+
3+
test_description='pre-command hook'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'with no hook' '
8+
echo "first" > file &&
9+
git add file &&
10+
git commit -m "first"
11+
'
12+
13+
test_expect_success 'with succeeding hook' '
14+
mkdir -p .git/hooks &&
15+
write_script .git/hooks/pre-command <<-EOF &&
16+
echo "\$*" >\$(git rev-parse --git-dir)/pre-command.out
17+
EOF
18+
echo "second" >> file &&
19+
git add file &&
20+
test "add file" = "$(cat .git/pre-command.out)" &&
21+
echo Hello | git hash-object --stdin &&
22+
test "hash-object --stdin" = "$(cat .git/pre-command.out)"
23+
'
24+
25+
test_expect_success 'with failing hook' '
26+
write_script .git/hooks/pre-command <<-EOF &&
27+
exit 1
28+
EOF
29+
echo "third" >> file &&
30+
test_must_fail git add file &&
31+
test_path_is_missing "$(cat .git/pre-command.out)"
32+
'
33+
34+
test_done

‎t/t0401-post-command-hook.sh‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/sh
2+
3+
test_description='post-command hook'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'with no hook' '
8+
echo "first" > file &&
9+
git add file &&
10+
git commit -m "first"
11+
'
12+
13+
test_expect_success 'with succeeding hook' '
14+
mkdir -p .git/hooks &&
15+
write_script .git/hooks/post-command <<-EOF &&
16+
echo "\$*" >\$(git rev-parse --git-dir)/post-command.out
17+
EOF
18+
echo "second" >> file &&
19+
git add file &&
20+
test "add file --exit_code=0" = "$(cat .git/post-command.out)"
21+
'
22+
23+
test_expect_success 'with failing pre-command hook' '
24+
write_script .git/hooks/pre-command <<-EOF &&
25+
exit 1
26+
EOF
27+
echo "third" >> file &&
28+
test_must_fail git add file &&
29+
test_path_is_missing "$(cat .git/post-command.out)"
30+
'
31+
32+
test_done

‎t/t7502-commit-porcelain.sh‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,11 @@ EOF
10511051
'
10521052

10531053
test_expect_success WITH_BREAKING_CHANGES 'core.commentChar=auto is rejected' '
1054+
cat >&2 <<-EOF &&
1055+
Trying to run any pre-command hook already triggers a failure when
1056+
running \`git config core.commentChar auto\`; Skipping this test.
1057+
EOF
1058+
return 0 &&
10541059
test_config core.commentChar auto &&
10551060
test_must_fail git rev-parse --git-dir 2>err &&
10561061
sed -n "s/^hint: *\$//p; s/^hint: //p; s/^fatal: //p" err >actual &&

0 commit comments

Comments
 (0)