[PATCH v2 4/4] worktree: add post-worktree-move hook
From: Domen Kožar <hidden>
Date: 2026-08-04 18:17:11
Subsystem:
documentation, the rest · Maintainers:
Jonathan Corbet, Linus Torvalds
Tools that record worktree paths can keep their state up to date when a worktree is added or removed, but the mapping becomes stale when the worktree is moved. Services or other per-worktree state tied to the old path may also need to be relocated. Introduce a post-worktree-move hook that runs after the working tree and its administrative files have been moved. The hook runs inside the new working tree with GIT_DIR and GIT_WORK_TREE cleared and receives the old absolute path as its sole argument. The new path and worktree identifier can be queried by running git from the hook's working directory. This signature also lets one configured command handle all three worktree lifecycle hooks by argument count: post-worktree-add takes no arguments, post-worktree-move takes one, and post-worktree-remove takes two. A failing hook does not undo the completed move, but its exit status becomes the exit status of "git worktree move". Signed-off-by: Domen Kožar <redacted> Co-Authored-By: Claude Fable 5 [off-list ref] --- Documentation/config/hook.adoc | 1 + Documentation/githooks.adoc | 17 +++++++++++++++++ builtin/worktree.c | 19 +++++++++++++++++-- t/t2403-worktree-move.sh | 29 +++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-)
diff --git a/Documentation/config/hook.adoc b/Documentation/config/hook.adoc
index e013bc1e40..32511b56fd 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-move`;; `post-worktree-remove`;; `push-to-checkout`;; `post-commit`;;
diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index fdf697b12f..0392454756 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc@@ -233,6 +233,23 @@ 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. +post-worktree-move +~~~~~~~~~~~~~~~~~~ + +This hook is invoked by linkgit:git-worktree[1] after `git worktree move` +has moved a working tree and updated its administrative files. It is given +one parameter: the absolute path of the working tree before it was moved. + +The hook's current working directory is the new working tree, so its new +absolute path and identifier can be queried by running `git`. + +This hook cannot affect the outcome of `git worktree move`, other than +that the hook's exit status becomes the exit status of the command. A +failing hook does not undo the move. + +This hook can be used to update per-worktree development environments or +registrations with external tools after their working tree has moved. + post-worktree-remove ~~~~~~~~~~~~~~~~~~~~
diff --git a/builtin/worktree.c b/builtin/worktree.c
index e0c37039ac..55df3c6a8f 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c@@ -185,6 +185,17 @@ static int run_post_worktree_remove_hook(const char *path, const char *id) return run_hooks_opt(the_repository, "post-worktree-remove", &hook_opt); } +static int run_post_worktree_move_hook(const char *old_path, + const char *new_path) +{ + struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL; + + strvec_pushl(&hook_opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL); + strvec_push(&hook_opt.args, old_path); + hook_opt.dir = new_path; + return run_hooks_opt(the_repository, "post-worktree-move", &hook_opt); +} + static int prune_worktree(const char *id, const char *dotgit, const char *reason) {
@@ -1306,7 +1317,8 @@ static int move_worktree(int ac, const char **av, const char *prefix, struct strbuf dst = STRBUF_INIT; struct strbuf errmsg = STRBUF_INIT; const char *reason = NULL; - char *path; + char *old_path, *path; + int ret; ac = parse_options(ac, av, prefix, options, git_worktree_move_usage, 0);
@@ -1349,14 +1361,17 @@ static int move_worktree(int ac, const char **av, const char *prefix, errmsg.buf); strbuf_release(&errmsg); + old_path = xstrdup(wt->path); if (rename(wt->path, dst.buf) == -1) die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf); update_worktree_location(wt, dst.buf, use_relative_paths); + ret = run_post_worktree_move_hook(old_path, wt->path); + free(old_path); strbuf_release(&dst); free_worktrees(worktrees); - return 0; + return ret; } /*
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index b94f00e426..0ffcfe88f7 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh@@ -82,6 +82,35 @@ test_expect_success 'move worktree' ' test_cmp expected2 actual2 ' +test_expect_success '"move" invokes post-worktree-move hook' ' + test_hook post-worktree-move <<-\EOF && + test "$#" = 1 && + { + echo "$1" && + git rev-parse --git-dir --show-toplevel + } >hook.actual + EOF + git worktree add --detach hook-source && + git worktree move hook-source hook-destination && + { + echo "$(pwd)/hook-source" && + echo "$(pwd)/.git/worktrees/hook-source" && + echo "$(pwd)/hook-destination" + } >hook.expect && + test_cmp hook.expect hook-destination/hook.actual +' + +test_expect_success 'failing post-worktree-move hook leaves worktree moved' ' + test_hook post-worktree-move <<-\EOF && + exit 1 + EOF + git worktree add --detach hook-failing-source && + test_must_fail git worktree move hook-failing-source hook-failing-destination && + test_path_is_missing hook-failing-source && + git -C hook-failing-destination status --porcelain >actual && + test_must_be_empty actual +' + test_expect_success 'move main worktree' ' test_must_fail git worktree move . def '
--
2.54.0