From: "Domen Kožar" <domen@cachix.org>
To: git@vger.kernel.org
Cc: "Eric Sunshine" <sunshine@sunshineco.com>,
"Patrick Steinhardt" <ps@pks.im>,
"Ævar Arnfjörð Bjarmason" <avarab@gmail.com>,
"Caleb White" <cdwhite3@pm.me>,
"Junio C Hamano" <gitster@pobox.com>,
"Domen Kožar" <domen@cachix.org>,
"Claude Fable 5" <noreply@anthropic.com>
Subject: [PATCH v1 2/3] worktree: add post-worktree-remove hook
Date: Thu, 09 Jul 2026 23:36:10 +0000 [thread overview]
Message-ID: <746fccc3-8700-4413-96a6-d98da7cf678a@mtasv.net> (raw)
In-Reply-To: <20260709233542.628628-1-domen@cachix.org>
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 <domen@cachix.org>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
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
next prev parent reply other threads:[~2026-07-09 23:39 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
[not found] <20260709233542.628628-1-domen@cachix.org>
2026-07-09 23:36 ` [PATCH v1 1/3] worktree: add post-worktree-add hook Domen Kožar
2026-07-09 23:36 ` Domen Kožar [this message]
2026-07-09 23:36 ` [PATCH v1 3/3] worktree: run post-worktree-remove hook when pruning Domen Kožar
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=746fccc3-8700-4413-96a6-d98da7cf678a@mtasv.net \
--to=domen@cachix.org \
--cc=avarab@gmail.com \
--cc=cdwhite3@pm.me \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=noreply@anthropic.com \
--cc=ps@pks.im \
--cc=sunshine@sunshineco.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox