Skip to content

Commit 90dd793

Browse files
derrickstoleedscho
authored andcommitted
hooks: add custom post-command hook config
The microsoft/git fork includes pre- and post-command hooks, with the initial intention of using these for VFS for Git. In that environment, these are important hooks to avoid concurrent issues when the virtualization is incomplete. However, in the Office monorepo the post-command hook is used in a different way. A custom hook is used to update the sparse-checkout, if necessary. To avoid this hook from being incredibly slow on every Git command, this hook checks for the existence of a "sentinel file" that is written by a custom post-index-change hook and no-ops if that file does not exist. However, even this "no-op" is 200ms due to the use of two scripts (one simple script in .git/hooks/ does some environment checking and then calls a script from the working directory which actually contains the logic). Add a new config option, 'postCommand.strategy', that will allow for multiple possible strategies in the future. For now, the one we are adding is 'worktree-change' which states that we should write a sentinel file instead of running the 'post-index-change' hook and then skip the 'post-command' hook if the proper sentinel file doesn't exist. If it does exist, then delete it and run the hook. This behavior is _only_ triggered, however, if a part of the index changes that is within the sparse checkout; If only parts of the index change that are not even checked out on disk, the hook is still skipped. I originally planned to put this into the repo-settings, but this caused the repo settings to load in more cases than they did previously. When there is an invalid boolean config option, this causes failure in new places. This was caught by t3007. This behavior is tested in t0401-post-command-hook.sh. Signed-off-by: Derrick Stolee <[email protected]>
1 parent 65ef2d6 commit 90dd793

File tree

4 files changed

+167
-1
lines changed

4 files changed

+167
-1
lines changed

Documentation/config.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,8 @@ include::config/pack.adoc[]
490490

491491
include::config/pager.adoc[]
492492

493+
include::config/postcommand.adoc[]
494+
493495
include::config/pretty.adoc[]
494496

495497
include::config/promisor.adoc[]

Documentation/config/postcommand.adoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
postCommand.strategy::
2+
The `post-command` hook is run on every Git process by default. This
3+
config option allows running the hook only conditionally, according
4+
to these values:
5+
+
6+
----
7+
`always`;;
8+
run the `post-command` hook on every process (default).
9+
10+
`worktree-change`;;
11+
run the `post-command` hook only if the current process wrote to
12+
the index and updated the worktree.
13+
----

hook.c

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#define USE_THE_REPOSITORY_VARIABLE
22

33
#include "git-compat-util.h"
4+
#include "trace2/tr2_sid.h"
45
#include "abspath.h"
56
#include "environment.h"
67
#include "advice.h"
@@ -176,6 +177,93 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
176177
strvec_clear(&options->args);
177178
}
178179

180+
static char *get_post_index_change_sentinel_name(struct repository *r)
181+
{
182+
struct strbuf path = STRBUF_INIT;
183+
const char *sid = tr2_sid_get();
184+
char *slash = strchr(sid, '/');
185+
186+
/*
187+
* Name is based on top-level SID, so children can indicate that
188+
* the top-level process should run the post-command hook.
189+
*/
190+
if (slash)
191+
*slash = 0;
192+
193+
/*
194+
* Do not write to hooks directory, as it could be redirected
195+
* somewhere like the source tree.
196+
*/
197+
repo_git_path_replace(r, &path, "info/index-change-%s.snt", sid);
198+
199+
return strbuf_detach(&path, NULL);
200+
}
201+
202+
static int write_post_index_change_sentinel(struct repository *r)
203+
{
204+
char *path = get_post_index_change_sentinel_name(r);
205+
FILE *fp = xfopen(path, "w");
206+
207+
if (fp) {
208+
fprintf(fp, "run post-command hook");
209+
fclose(fp);
210+
}
211+
212+
free(path);
213+
return fp ? 0 : -1;
214+
}
215+
216+
/**
217+
* Try to delete the sentinel file for this repository. If that succeeds, then
218+
* return 1.
219+
*/
220+
static int post_index_change_sentinel_exists(struct repository *r)
221+
{
222+
char *path = get_post_index_change_sentinel_name(r);
223+
int res = 1;
224+
225+
if (unlink(path)) {
226+
if (is_missing_file_error(errno))
227+
res = 0;
228+
else
229+
warning_errno("failed to remove index-change sentinel file '%s'", path);
230+
}
231+
232+
free(path);
233+
return res;
234+
}
235+
236+
/**
237+
* See if we can replace the requested hook with an internal behavior.
238+
* Returns 0 if the real hook should run. Returns nonzero if we instead
239+
* executed custom internal behavior and the real hook should not run.
240+
*/
241+
static int handle_hook_replacement(struct repository *r,
242+
const char *hook_name,
243+
struct strvec *args)
244+
{
245+
const char *strval;
246+
if (repo_config_get_string_tmp(r, "postcommand.strategy", &strval) ||
247+
strcasecmp(strval, "worktree-change"))
248+
return 0;
249+
250+
if (!strcmp(hook_name, "post-index-change")) {
251+
/* Create a sentinel file only if the worktree changed. */
252+
if (!strcmp(args->v[0], "1"))
253+
write_post_index_change_sentinel(r);
254+
255+
/* We don't skip post-index-change hooks that exist. */
256+
return 0;
257+
}
258+
if (!strcmp(hook_name, "post-command") &&
259+
!post_index_change_sentinel_exists(r)) {
260+
/* We skip the post-command hook in this case. */
261+
return 1;
262+
}
263+
264+
return 0;
265+
}
266+
179267
int run_hooks_opt(struct repository *r, const char *hook_name,
180268
struct run_hooks_opt *options)
181269
{
@@ -185,7 +273,7 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
185273
.hook_name = hook_name,
186274
.options = options,
187275
};
188-
const char *hook_path = find_hook(r, hook_name);
276+
const char *hook_path;
189277
int ret = 0;
190278
const struct run_process_parallel_opts opts = {
191279
.tr2_category = "hook",
@@ -201,6 +289,13 @@ int run_hooks_opt(struct repository *r, const char *hook_name,
201289
.data = &cb_data,
202290
};
203291

292+
/* Interject hook behavior depending on strategy. */
293+
if (r && r->gitdir &&
294+
handle_hook_replacement(r, hook_name, &options->args))
295+
return 0;
296+
297+
hook_path = find_hook(r, hook_name);
298+
204299
/*
205300
* Backwards compatibility hack in VFS for Git: when originally
206301
* introduced (and used!), it was called `post-indexchanged`, but this

t/t0401-post-command-hook.sh

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ test_expect_success 'with succeeding hook' '
2222
'
2323

2424
test_expect_success 'with failing pre-command hook' '
25+
test_when_finished rm -f .git/hooks/pre-command &&
2526
write_script .git/hooks/pre-command <<-EOF &&
2627
exit 1
2728
EOF
@@ -30,4 +31,59 @@ test_expect_success 'with failing pre-command hook' '
3031
test_path_is_missing "$(cat .git/post-command.out)"
3132
'
3233

34+
test_expect_success 'with post-index-change config' '
35+
mkdir -p internal-hooks &&
36+
write_script internal-hooks/post-command <<-EOF &&
37+
echo ran >post-command.out
38+
EOF
39+
write_script internal-hooks/post-index-change <<-EOF &&
40+
echo ran >post-index-change.out
41+
EOF
42+
43+
# prevent writing of sentinel files to this directory.
44+
test_when_finished chmod 775 internal-hooks &&
45+
chmod a-w internal-hooks &&
46+
47+
git config core.hooksPath internal-hooks &&
48+
49+
# First, show expected behavior.
50+
echo ran >expect &&
51+
rm -f post-command.out post-index-change.out &&
52+
53+
# rev-parse leaves index intact, but runs post-command.
54+
git rev-parse HEAD &&
55+
test_path_is_missing post-index-change.out &&
56+
test_cmp expect post-command.out &&
57+
rm -f post-command.out &&
58+
59+
echo stuff >>file &&
60+
# add updates the index and runs post-command.
61+
git add file &&
62+
test_cmp expect post-index-change.out &&
63+
test_cmp expect post-command.out &&
64+
65+
# Now, show configured behavior
66+
git config postCommand.strategy worktree-change &&
67+
68+
# rev-parse leaves index intact and thus skips post-command.
69+
rm -f post-command.out post-index-change.out &&
70+
git rev-parse HEAD &&
71+
test_path_is_missing post-index-change.out &&
72+
test_path_is_missing post-command.out &&
73+
74+
echo stuff >>file &&
75+
# add keeps the worktree the same, so does not run post-command.
76+
rm -f post-command.out post-index-change.out &&
77+
git add file &&
78+
test_cmp expect post-index-change.out &&
79+
test_path_is_missing post-command.out &&
80+
81+
echo stuff >>file &&
82+
# reset --hard updates the worktree.
83+
rm -f post-command.out post-index-change.out &&
84+
git reset --hard &&
85+
test_cmp expect post-index-change.out &&
86+
test_cmp expect post-command.out
87+
'
88+
3389
test_done

0 commit comments

Comments
 (0)