[PATCH v2 2/4] worktree: add post-worktree-remove hook
Domen Kožar <[email protected]> Tue, 04 Aug 2026 18:14:02 +0000
| Newsgroups | org.kernel.vger.git |
|---|---|
| Message-ID | <[email protected]> |
External tooling has no way to learn that a working tree is gone: "git worktree remove" deletes both the working tree and its administrative directory without running any hook. Introduce a post-worktree-remove hook that runs after "git worktree remove" has deleted a working tree. It is given the former absolute path of the working tree and its identifier as arguments. The hook also runs when only the administrative entry is deleted because the working tree directory itself had already disappeared, since the worktree is deregistered either way. Because the working tree no longer exists at that point, no special working directory or environment is set up; the hook runs wherever the command ran, like other post-command hooks. The hook runs once deletion is underway even if parts of it fail, since there is no going back at that point, but it does not run when the removal is refused (locked or dirty working tree, failed validation). It cannot affect the outcome of the command other than its exit status being reflected in the exit status of "git worktree remove". Signed-off-by: Domen Ko=C5=BEar <[email protected]> Co-Authored-By: Claude Fable 5 <[email protected]> --- Documentation/config/hook.adoc | 1 + Documentation/githooks.adoc | 18 ++++++++++++++ builtin/worktree.c | 10 ++++++++ t/t2403-worktree-move.sh | 44 ++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.ado= c index 81afb4a919..e013bc1e40 100644 --- a/Documentation/config/hook.adoc +++ b/Documentation/config/hook.adoc @@ -95,6 +95,7 @@ hook.jobs:: `pre-commit`;; `post-checkout`;; `post-worktree-add`;; +`post-worktree-remove`;; `push-to-checkout`;; `post-commit`;; Access the working tree, index, or repository state. diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc index 5a2955ee2f..9573b8c1f5 100644 --- a/Documentation/githooks.adoc +++ b/Documentation/githooks.adoc @@ -233,6 +233,24 @@ runs after the `post-checkout` hook, even if that hook= fails. This hook can be used to set up per-worktree development environments or to register the new working tree with external tools. =20 +post-worktree-remove +~~~~~~~~~~~~~~~~~~~~ + +This hook is invoked by linkgit:git-worktree[1] after a working tree +has been deleted by `git worktree remove`. The hook is given two +parameters: the absolute path of the removed working tree and its +identifier (the name of its former administrative directory in +`$GIT_DIR/worktrees/`). + +The working tree no longer exists when the hook runs. + +This hook cannot affect the outcome of `git worktree remove`, other +than that the hook's exit status becomes the exit status of the +command. + +This hook can be used to tear down per-worktree development +environments or to unregister the working tree from external tools. + post-merge ~~~~~~~~~~ =20 diff --git a/builtin/worktree.c b/builtin/worktree.c index cc3299bca9..dc456fcac7 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -177,6 +177,14 @@ static int run_post_worktree_add_hook(const char *path= ) return run_hooks_opt(the_repository, "post-worktree-add", &hook_opt); } =20 +static int run_post_worktree_remove_hook(const char *path, const char *id) +{ + struct run_hooks_opt hook_opt =3D RUN_HOOKS_OPT_INIT_FORCE_SERIAL; + + strvec_pushl(&hook_opt.args, path, id, NULL); + return run_hooks_opt(the_repository, "post-worktree-remove", &hook_opt); +} + static void prune_worktree(const char *id, const char *reason) { if (show_only || verbose) @@ -1444,6 +1452,8 @@ static int remove_worktree(int ac, const char **av, c= onst char *prefix, ret |=3D delete_git_dir(wt->id); delete_worktrees_dir_if_empty(); =20 + ret |=3D run_post_worktree_remove_hook(wt->path, wt->id); + free_worktrees(worktrees); return ret; } diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh index 0bb33e8b1b..b94f00e426 100755 --- a/t/t2403-worktree-move.sh +++ b/t/t2403-worktree-move.sh @@ -246,6 +246,50 @@ test_expect_success 'not remove a repo with initialize= d submodule' ' ) ' =20 +test_expect_success '"remove" invokes post-worktree-remove hook' ' + test_hook post-worktree-remove <<-\EOF && + echo $* >hook.actual + EOF + git worktree add --detach wt-hooked && + git worktree remove wt-hooked && + echo $(pwd)/wt-hooked wt-hooked >hook.expect && + test_cmp hook.expect hook.actual +' + +test_expect_success '"remove" of missing worktree invokes post-worktree-re= move hook' ' + test_when_finished "rm -rf wt-moved-away" && + test_hook post-worktree-remove <<-\EOF && + echo $* >hook.actual + EOF + rm -f hook.actual && + git worktree add --detach wt-elsewhere && + mv wt-elsewhere wt-moved-away && + git worktree remove wt-elsewhere && + echo $(pwd)/wt-elsewhere wt-elsewhere >hook.expect && + test_cmp hook.expect hook.actual +' + +test_expect_success 'refused "remove" does not invoke post-worktree-remove= hook' ' + git worktree add --detach wt-kept && + test_when_finished "git worktree remove --force --force wt-kept || :" && + test_hook post-worktree-remove <<-\EOF && + >hook.ran + EOF + git worktree lock wt-kept && + test_must_fail git worktree remove wt-kept && + test_path_is_missing hook.ran +' + +test_expect_success 'failing post-worktree-remove hook fails "remove", wor= ktree is gone' ' + test_hook post-worktree-remove <<-\EOF && + exit 1 + EOF + git worktree add --detach wt-doomed && + test_must_fail git worktree remove wt-doomed && + test_path_is_missing wt-doomed && + test_path_is_missing .git/worktrees/wt-doomed +' + test_expect_success 'move worktree with absolute path to relative path' ' test_config worktree.useRelativePaths false && git worktree add ./absolute && --=20 2.54.0