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 1/3] worktree: add post-worktree-add hook
Date: Thu, 09 Jul 2026 23:36:09 +0000 [thread overview]
Message-ID: <4864bcf9-1018-4b36-9239-47df247b8418@mtasv.net> (raw)
In-Reply-To: <20260709233542.628628-1-domen@cachix.org>
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(-)
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
next 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 ` Domen Kožar [this message]
2026-07-09 23:36 ` [PATCH v1 2/3] worktree: add post-worktree-remove hook Domen Kožar
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=4864bcf9-1018-4b36-9239-47df247b8418@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