Volume XXII, number 279Tuesday, October 6, 2026Latest message 47 minutes ago

The Git List

News and archive of git@vger.kernel.org, since April 2005

patch, 3 partsworktree: add post-worktree-remove hook

3 messages between Jul 9, 2026 and Jul 9, 2026, from Domen Kožar.

Plain Markdown or JSON for tools and agents. Diffs are folded; open one to read it.

Domen KožarJul 9, 2026, 23:36 UTC on lore

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(+)
Show changes to 3 files +72 −0

Documentation/githooks.adoc, builtin/worktree.c, t/t2403-worktree-move.sh

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
Domen KožarJul 9, 2026, 23:36 UTC on lore

[PATCH v1 1/3] worktree: add post-worktree-add hook

Tools that manage per-worktree state, such as development environment managers or IDEs, have no way to react when a new working tree is created. The only hook that fires during "git worktree add" is post-checkout, which is skipped when --no-checkout or --orphan is used and cannot be distinguished from a plain checkout.

Introduce a post-worktree-add hook that runs after the working tree has been fully set up, including with --no-checkout and --orphan. The hook runs inside the new working tree with GIT_DIR and GIT_WORK_TREE cleared, mirroring the existing post-checkout invocation, and is given the absolute path of the new working tree and its identifier as arguments. Anything else, such as the checked-out branch, can be queried by running git from the hook's working directory.

Like post-checkout, the hook cannot affect the outcome of the command: a failing hook does not delete the already-created working tree, but its exit status becomes the exit status of "git worktree add". The hook runs after post-checkout and is skipped if that hook fails.

Documenting the new hook in githooks(5) also registers its name in the generated hook-list.h, so "git hook run" and hook.*.event recognize it without further changes.

Signed-off-by: Domen Kožar <domen@cachix.org>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 Documentation/githooks.adoc |  20 +++++++
 builtin/worktree.c          |  15 ++++-
 t/t2400-worktree-add.sh     | 113 ++++++++++++++++++++++++++++++++++++
 3 files changed, 146 insertions(+), 2 deletions(-)
Show changes to 3 files +146 −2

Documentation/githooks.adoc, builtin/worktree.c, t/t2400-worktree-add.sh

diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index ed045940d1..2778f73f30 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -215,6 +215,26 @@ This hook can be used to perform repository validity checks, auto-display
 differences from the previous HEAD if different, or set working dir metadata
 properties.
 
+post-worktree-add
+~~~~~~~~~~~~~~~~~
+
+This hook is invoked by linkgit:git-worktree[1] after `git worktree add`
+has created and set up a new working tree. The hook is given two
+parameters: the absolute path of the new working tree and its identifier
+(the name of its administrative directory in `$GIT_DIR/worktrees/`).
+
+The hook runs inside the new working tree, so further details, such as
+the checked-out branch, can be queried by running `git` from the hook's
+current directory. Unlike the `post-checkout` hook, it is also run when
+`--no-checkout` or `--orphan` is used.
+
+This hook cannot affect the outcome of `git worktree add`, other than
+that the hook's exit status becomes the exit status of the command. It
+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-merge
 ~~~~~~~~~~
 
diff --git a/builtin/worktree.c b/builtin/worktree.c
index d21c43fde3..7b9d337234 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -605,8 +605,9 @@ static int add_worktree(const char *path, const char *refname,
 	}
 
 	/*
-	 * Hook failure does not warrant worktree deletion, so run hook after
-	 * is_junk is cleared, but do return appropriate code when hook fails.
+	 * Hook failures do not warrant worktree deletion, so run hooks after
+	 * is_junk is cleared, but do return appropriate code when a hook
+	 * fails.
 	 */
 	if (!ret && opts->checkout && !opts->orphan) {
 		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
@@ -622,6 +623,16 @@ static int add_worktree(const char *path, const char *refname,
 		ret = run_hooks_opt(the_repository, "post-checkout", &opt);
 	}
 
+	if (!ret) {
+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
+
+		strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
+		strvec_pushl(&opt.args, wt->path, wt->id, NULL);
+		opt.dir = path;
+
+		ret = run_hooks_opt(the_repository, "post-worktree-add", &opt);
+	}
+
 	strvec_clear(&child_env);
 	strbuf_release(&sb);
 	strbuf_release(&symref);
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index 58b4445cc4..3754559a98 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -1132,6 +1132,119 @@ test_expect_success '"add" in bare repo invokes post-checkout hook' '
 	test_cmp hook.expect goozy/hook.actual
 '
 
+# Install a post-worktree-add hook and write the output expected for
+# adding worktree $1; the hook is installed in repo $2 (default ".git").
+post_worktree_add_hook () {
+	test_when_finished "rm -rf .git/hooks" &&
+	mkdir .git/hooks &&
+	test_hook -C "$2" post-worktree-add <<-\EOF &&
+	{
+		echo $*
+		git rev-parse --git-dir --show-toplevel
+	} >hook.actual
+	EOF
+	{
+		echo $(pwd)/$1 $1 &&
+		echo $(pwd)/${2:-.git}/worktrees/$1 &&
+		echo $(pwd)/$1
+	} >hook.expect
+}
+
+test_expect_success '"add" invokes post-worktree-add hook' '
+	post_worktree_add_hook wanda &&
+	git worktree add wanda &&
+	test_cmp hook.expect wanda/hook.actual
+'
+
+test_expect_success '"add" in other worktree invokes post-worktree-add hook' '
+	post_worktree_add_hook wilbur &&
+	git -C wanda worktree add ../wilbur &&
+	test_cmp hook.expect wilbur/hook.actual
+'
+
+test_expect_success '"add --no-checkout" still invokes post-worktree-add hook' '
+	post_worktree_add_hook wendy &&
+	git worktree add --no-checkout wendy &&
+	test_cmp hook.expect wendy/hook.actual
+'
+
+test_expect_success '"add --orphan" invokes post-worktree-add hook' '
+	post_worktree_add_hook winnie &&
+	git worktree add --orphan winnie &&
+	test_cmp hook.expect winnie/hook.actual
+'
+
+test_expect_success '"add" in bare repo invokes post-worktree-add hook' '
+	rm -rf bare2 &&
+	git clone --bare . bare2 &&
+	post_worktree_add_hook willow bare2 &&
+	git -C bare2 worktree add --detach ../willow &&
+	test_cmp hook.expect willow/hook.actual
+'
+
+test_expect_success '"add" runs post-worktree-add after post-checkout' '
+	test_when_finished "rm -rf .git/hooks" &&
+	mkdir .git/hooks &&
+	test_hook post-checkout <<-\EOF &&
+	echo post-checkout >>hooks.actual
+	EOF
+	test_hook post-worktree-add <<-\EOF &&
+	echo post-worktree-add >>hooks.actual
+	EOF
+	test_write_lines post-checkout post-worktree-add >hooks.expect &&
+	git worktree add wobble &&
+	test_cmp hooks.expect wobble/hooks.actual
+'
+
+test_expect_success 'failing post-checkout hook suppresses post-worktree-add hook' '
+	test_when_finished "rm -rf .git/hooks" &&
+	mkdir .git/hooks &&
+	test_hook post-checkout <<-\EOF &&
+	exit 1
+	EOF
+	test_hook post-worktree-add <<-\EOF &&
+	>post-worktree-add.ran
+	EOF
+	test_must_fail git worktree add wozzle &&
+	test_path_is_missing wozzle/post-worktree-add.ran
+'
+
+test_expect_success 'failing post-worktree-add hook leaves worktree in place' '
+	test_when_finished "rm -rf .git/hooks" &&
+	mkdir .git/hooks &&
+	test_hook post-worktree-add <<-\EOF &&
+	exit 1
+	EOF
+	test_must_fail git worktree add wilma &&
+	git worktree list --porcelain >out &&
+	grep -F "worktree $(pwd)/wilma" out
+'
+
+test_expect_success 'failed "add" does not invoke post-worktree-add hook' '
+	test_when_finished "rm -rf .git/hooks occupied" &&
+	mkdir .git/hooks &&
+	test_hook post-worktree-add <<-\EOF &&
+	>hook.ran
+	EOF
+	mkdir occupied &&
+	: >occupied/blocker &&
+	test_must_fail git worktree add occupied &&
+	test_path_is_missing occupied/hook.ran &&
+	test_path_is_missing hook.ran
+'
+
+test_expect_success 'post-worktree-add hook gets absolute path with relative worktrees' '
+	test_when_finished "rm -rf relhook" &&
+	git init relhook &&
+	test_commit -C relhook base &&
+	test_hook -C relhook post-worktree-add <<-\EOF &&
+	echo $* >hook.actual
+	EOF
+	git -C relhook worktree add --relative-paths --detach wt &&
+	echo $(pwd)/relhook/wt wt >hook.expect &&
+	test_cmp hook.expect relhook/wt/hook.actual
+'
+
 test_expect_success '"add" an existing but missing worktree' '
 	git worktree add --detach pneu &&
 	test_must_fail git worktree add --detach pneu &&
-- 
2.54.0
Domen KožarJul 9, 2026, 23:36 UTC on lore

[PATCH v1 3/3] worktree: run post-worktree-remove hook when pruning

A working tree can also disappear via "git worktree prune", e.g. after the user deleted the working tree directory manually. Tooling that tears down per-worktree state wants to observe those deletions the same way as an explicit "git worktree remove".

Run the post-worktree-remove hook once for each administrative entry that "git worktree prune" removes, including duplicate entries pruned during deduplication. The hook is not run with --dry-run, and a failing hook is reflected in the exit status of the command.

should_prune_worktree() so far returned the path of the worktree's .git file only for entries that are kept. Also return it when pruning an entry whose gitdir file points to a location that no longer exists, which is the common case of a manually deleted working tree, so that the hook can be given the path. For entries whose path cannot be determined at all (missing or corrupt gitdir file), the hook receives an empty string instead. The one other caller of should_prune_worktree() already frees the path unconditionally.

Signed-off-by: Domen Kožar <domen@cachix.org>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 Documentation/githooks.adoc | 23 +++++-----
 builtin/worktree.c          | 48 ++++++++++++++------
 t/t2401-worktree-prune.sh   | 88 +++++++++++++++++++++++++++++++++++++
 worktree.c                  |  1 -
 worktree.h                  |  6 +--
 5 files changed, 139 insertions(+), 27 deletions(-)
Show changes to 5 files +139 −27

Documentation/githooks.adoc, builtin/worktree.c, t/t2401-worktree-prune.sh, worktree.c, worktree.h

diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 22b3263ff7..28fab7ccbe 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -239,16 +239,19 @@ 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.
+has been deleted by `git worktree remove`, and once for each working
+tree pruned by `git worktree prune`. 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. For working
+trees pruned by `git worktree prune`, the first parameter may be the
+empty string if the path could not be determined from the leftover
+administrative files.
+
+This hook cannot affect the outcome of `git worktree remove` or
+`git worktree prune`, 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.
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 01b62ed2fc..e2cdbef8bb 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -176,12 +176,27 @@ 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 void prune_worktree(const char *id, const char *reason)
+static int prune_worktree(const char *id, const char *dotgit,
+			  const char *reason)
 {
+	struct strbuf path = STRBUF_INIT;
+	int ret;
+
 	if (show_only || verbose)
 		fprintf_ln(stderr, _("Removing %s/%s: %s"), "worktrees", id, reason);
-	if (!show_only)
-		delete_git_dir(id);
+	if (show_only)
+		return 0;
+
+	delete_git_dir(id);
+
+	/* path stays empty when the worktree path cannot be determined */
+	if (dotgit) {
+		strbuf_addstr(&path, dotgit);
+		strbuf_strip_suffix(&path, "/.git");
+	}
+	ret = run_post_worktree_remove_hook(path.buf, id);
+	strbuf_release(&path);
+	return ret;
 }
 
 static int prune_cmp(const void *a, const void *b)
@@ -206,18 +221,22 @@ static int prune_cmp(const void *a, const void *b)
 	return strcmp(x->util, y->util);
 }
 
-static void prune_dups(struct string_list *l)
+static int prune_dups(struct string_list *l)
 {
 	int i;
+	int ret = 0;
 
 	QSORT(l->items, l->nr, prune_cmp);
 	for (i = 1; i < l->nr; i++) {
 		if (!fspathcmp(l->items[i].string, l->items[i - 1].string))
-			prune_worktree(l->items[i].util, "duplicate entry");
+			ret |= prune_worktree(l->items[i].util,
+					      l->items[i].string,
+					      "duplicate entry");
 	}
+	return ret;
 }
 
-static void prune_worktrees(void)
+static int prune_worktrees(void)
 {
 	struct strbuf reason = STRBUF_INIT;
 	struct strbuf main_path = STRBUF_INIT;
@@ -225,19 +244,22 @@ static void prune_worktrees(void)
 	char *path;
 	DIR *dir;
 	struct dirent *d;
+	int ret = 0;
 
 	path = repo_git_path(the_repository, "worktrees");
 	dir = opendir(path);
 	free(path);
 	if (!dir)
-		return;
+		return 0;
 	while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) {
 		char *path;
 		strbuf_reset(&reason);
-		if (should_prune_worktree(d->d_name, &reason, &path, expire))
-			prune_worktree(d->d_name, reason.buf);
-		else if (path)
+		if (should_prune_worktree(d->d_name, &reason, &path, expire)) {
+			ret |= prune_worktree(d->d_name, path, reason.buf);
+			free(path);
+		} else if (path) {
 			string_list_append_nodup(&kept, path)->util = xstrdup(d->d_name);
+		}
 	}
 	closedir(dir);
 
@@ -245,12 +267,13 @@ static void prune_worktrees(void)
 	/* massage main worktree absolute path to match 'gitdir' content */
 	strbuf_strip_suffix(&main_path, "/.");
 	string_list_append_nodup(&kept, strbuf_detach(&main_path, NULL));
-	prune_dups(&kept);
+	ret |= prune_dups(&kept);
 	string_list_clear(&kept, 1);
 
 	if (!show_only)
 		delete_worktrees_dir_if_empty();
 	strbuf_release(&reason);
+	return ret;
 }
 
 static int prune(int ac, const char **av, const char *prefix,
@@ -269,8 +292,7 @@ static int prune(int ac, const char **av, const char *prefix,
 			   0);
 	if (ac)
 		usage_with_options(git_worktree_prune_usage, options);
-	prune_worktrees();
-	return 0;
+	return prune_worktrees();
 }
 
 static char *junk_work_tree;
diff --git a/t/t2401-worktree-prune.sh b/t/t2401-worktree-prune.sh
index f8f28c76ee..74a80c1a8d 100755
--- a/t/t2401-worktree-prune.sh
+++ b/t/t2401-worktree-prune.sh
@@ -119,6 +119,94 @@ test_expect_success 'prune duplicate (main/linked)' '
 	test_path_is_missing .git/worktrees/wt
 '
 
+test_expect_success 'prune invokes post-worktree-remove hook' '
+	test_hook post-worktree-remove <<-\EOF &&
+	echo $* >hook.actual
+	EOF
+	git worktree add --detach flushed &&
+	rm -rf flushed &&
+	git worktree prune &&
+	echo $(pwd)/flushed flushed >hook.expect &&
+	test_cmp hook.expect hook.actual
+'
+
+test_expect_success 'prune invokes post-worktree-remove hook once per worktree' '
+	test_hook post-worktree-remove <<-\EOF &&
+	echo $* >>hook.actual
+	EOF
+	git worktree add --detach first &&
+	git worktree add --detach second &&
+	rm -rf first second hook.actual &&
+	git worktree prune &&
+	{
+		echo $(pwd)/first first &&
+		echo $(pwd)/second second
+	} >hook.expect &&
+	sort hook.actual >hook.sorted &&
+	test_cmp hook.expect hook.sorted
+'
+
+test_expect_success 'prune --dry-run does not invoke post-worktree-remove hook' '
+	git worktree add --detach dry &&
+	rm -rf dry &&
+	test_when_finished "git worktree prune" &&
+	test_hook post-worktree-remove <<-\EOF &&
+	>hook.ran
+	EOF
+	git worktree prune --dry-run &&
+	test_path_is_missing hook.ran
+'
+
+test_expect_success 'pruned entry with unknown path gives empty hook argument' '
+	test_hook post-worktree-remove <<-\EOF &&
+	echo "[$1][$2]" >hook.actual
+	EOF
+	mkdir -p .git/worktrees/broken &&
+	: >.git/worktrees/broken/gitdir &&
+	git worktree prune &&
+	echo "[][broken]" >hook.expect &&
+	test_cmp hook.expect hook.actual
+'
+
+test_expect_success 'failing post-worktree-remove hook fails prune' '
+	test_hook post-worktree-remove <<-\EOF &&
+	exit 1
+	EOF
+	git worktree add --detach doomed &&
+	rm -rf doomed &&
+	test_must_fail git worktree prune &&
+	test_path_is_missing .git/worktrees/doomed
+'
+
+test_expect_success 'prune duplicate invokes post-worktree-remove hook' '
+	test_when_finished rm -fr .git/worktrees w1 w2 &&
+	test_hook post-worktree-remove <<-\EOF &&
+	echo $* >>hook.actual
+	EOF
+	rm -f hook.actual &&
+	git worktree add --detach w1 &&
+	git worktree add --detach w2 &&
+	sed "s/w2/w1/" .git/worktrees/w2/gitdir >.git/worktrees/w2/gitdir.new &&
+	mv .git/worktrees/w2/gitdir.new .git/worktrees/w2/gitdir &&
+	git worktree prune &&
+	echo $(pwd)/w1 w2 >hook.expect &&
+	test_cmp hook.expect hook.actual
+'
+
+test_expect_success 'post-worktree-remove hook gets absolute path with relative worktrees' '
+	test_when_finished "rm -rf relhook" &&
+	git init relhook &&
+	test_commit -C relhook base &&
+	test_hook -C relhook post-worktree-remove <<-\EOF &&
+	echo $* >hook.actual
+	EOF
+	git -C relhook worktree add --relative-paths --detach wt &&
+	rm -rf relhook/wt &&
+	git -C relhook worktree prune &&
+	echo $(pwd)/relhook/wt wt >hook.expect &&
+	test_cmp hook.expect relhook/hook.actual
+'
+
 test_expect_success 'not prune proper worktrees inside linked worktree with relative paths' '
 	test_when_finished rm -rf repo wt_ext &&
 	git init repo &&
diff --git a/worktree.c b/worktree.c
index 30125827fd..6a9d943874 100644
--- a/worktree.c
+++ b/worktree.c
@@ -1004,7 +1004,6 @@ int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath,
 		if (stat(file.buf, &st) || st.st_mtime <= expire) {
 			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
 			rc = 1;
-			goto done;
 		}
 	}
 	*wtpath = strbuf_detach(&dotgit, NULL);
diff --git a/worktree.h b/worktree.h
index 1075409f9a..dde8fc2be4 100644
--- a/worktree.h
+++ b/worktree.h
@@ -105,9 +105,9 @@ const char *worktree_prune_reason(struct worktree *wt, timestamp_t expire);
 
 /*
  * Return true if worktree entry should be pruned, along with the reason for
- * pruning. Otherwise, return false and the worktree's path in `wtpath`, or
- * NULL if it cannot be determined. Caller is responsible for freeing
- * returned path.
+ * pruning. Otherwise, return false. In both cases the path of the
+ * worktree's `.git` file is returned in `wtpath`, or NULL if it cannot
+ * be determined. Caller is responsible for freeing returned path.
  *
  * `expire` defines a grace period to prune the worktree when its path
  * does not exist.
-- 
2.54.0

Back to recent threads