[PATCH v1 2/3] worktree: add post-worktree-remove hook
From: Domen Kožar <hidden>
Date: 2026-07-09 23:39:13
Subsystem:
documentation, the rest · Maintainers:
Jonathan Corbet, Linus Torvalds
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žar <redacted> Co-Authored-By: Claude Fable 5 [off-list ref] --- Documentation/githooks.adoc | 18 +++++++++++++++ builtin/worktree.c | 10 +++++++++ t/t2403-worktree-move.sh | 44 +++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+)
diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 2778f73f30..22b3263ff7 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc@@ -235,6 +235,24 @@ runs after the `post-checkout` hook, and is skipped 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. +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 ~~~~~~~~~~
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 7b9d337234..01b62ed2fc 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c@@ -168,6 +168,14 @@ static void delete_worktrees_dir_if_empty(void) free(path); } +static int run_post_worktree_remove_hook(const char *path, const char *id) +{ + struct run_hooks_opt hook_opt = 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)
@@ -1437,6 +1445,8 @@ static int remove_worktree(int ac, const char **av, const char *prefix, ret |= delete_git_dir(wt->id); delete_worktrees_dir_if_empty(); + ret |= 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 initialized submodule' ' ) ' +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-remove 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", worktree 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 &&
--
2.54.0