Git development
 help / color / mirror / Atom feed
From: "Domen Kožar" <domen@cachix.org>
To: git@vger.kernel.org
Cc: "Phillip Wood" <phillip.wood123@gmail.com>,
	"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>
Subject: [PATCH v2 0/4] worktree: add lifecycle hooks
Date: Tue, 04 Aug 2026 18:14:00 +0000	[thread overview]
Message-ID: <371a01cf-2765-4cf5-b1fd-414d1b55a325@mtasv.net> (raw)
In-Reply-To: <7c8b4673-37ac-45fa-ad8c-a1dc09afe5fe@mtasv.net>

Hi everyone,

First, apologies that my earlier reply reached the list as a separate
message rather than as part of this thread. This is my first patch series
submitted by email, and I am still getting the threading details right. I
have made sure this reroll is plain text and correctly threaded.

I maintain devenv, a developer environment manager, and lately the
workflow we see most is people letting AI coding agents loose on a
repository, one linked worktree per task, created and discarded at a
pace no human would type. Each of those worktrees expects a working
environment: processes, sockets, and stateful services such as a
database seeded from a dump.

Today there is no reliable trigger to set that up when a worktree
appears: post-checkout does not fire for --no-checkout or --orphan
and cannot be told apart from a plain checkout. Nothing fires when a
worktree is moved or removed, so external registrations become stale
and databases and services can pile up after "git worktree remove" or
a manual rm followed by "git worktree prune". Wrapping the worktree
commands only helps when every tool, human or agent, uses the wrapper.

Patch 1 adds a post-worktree-add hook that fires after the working
tree is fully set up. Patch 2 adds post-worktree-remove for "git
worktree remove". Patch 3 extends the remove hook to "git worktree
prune" so that manually deleted worktrees are also observed. Patch 4
adds post-worktree-move so tools can update their path mapping.

Changes since v1:

 * Run post-worktree-add after post-checkout even if post-checkout
   fails, because the populated worktree remains present.

 * Make post-worktree-add take no arguments. Its working directory is
   the new worktree, so its path and identifier can be queried with
   git. This also lets a configured command shared across the hooks
   distinguish add, move, and remove by their argument counts.

 * Add post-worktree-move. It runs in the new location and receives
   the old absolute path as its sole argument.

 * Document the new hooks among those that always run serially.

Thanks to Phillip Wood for the review that prompted these changes.

Thanks,
Domen

Domen Kožar (4):
  worktree: add post-worktree-add hook
  worktree: add post-worktree-remove hook
  worktree: run post-worktree-remove hook when pruning
  worktree: add post-worktree-move hook

 Documentation/config/hook.adoc |   3 +
 Documentation/githooks.adoc    |  56 +++++++++++++++
 builtin/worktree.c             | 123 +++++++++++++++++++++++++--------
 t/t2400-worktree-add.sh        | 111 +++++++++++++++++++++++++++++
 t/t2401-worktree-prune.sh      |  88 +++++++++++++++++++++++
 t/t2403-worktree-move.sh       |  73 +++++++++++++++++++
 worktree.c                     |   1 -
 worktree.h                     |   6 +-
 8 files changed, 428 insertions(+), 33 deletions(-)

Range-diff against v1:
1:  98f06e55c8 ! 1:  73e36c179e worktree: add post-worktree-add hook
    @@ Commit message
         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.
    +    cleared, mirroring the existing post-checkout invocation, and takes no
    +    arguments. Details such as the absolute path, worktree identifier, and
    +    checked-out branch can be queried by running git from the hook's working
    +    directory. Taking no arguments also lets a configured command shared
    +    with post-worktree-remove distinguish the events by argument count.
     
         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.
    +    hook runs after post-checkout, even when post-checkout fails, because
    +    the worktree has still been populated and remains present.
     
         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
    @@ Commit message
         Signed-off-by: Domen Kožar <domen@cachix.org>
         Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
     
    + ## Documentation/config/hook.adoc ##
    +@@ Documentation/config/hook.adoc: hook.jobs::
    + 	Receive a commit message file and may rewrite it in place.
    + `pre-commit`;;
    + `post-checkout`;;
    ++`post-worktree-add`;;
    + `push-to-checkout`;;
    + `post-commit`;;
    + 	Access the working tree, index, or repository state.
    +
      ## Documentation/githooks.adoc ##
     @@ Documentation/githooks.adoc: This hook can be used to perform repository validity checks, auto-display
      differences from the previous HEAD if different, or set working dir metadata
    @@ Documentation/githooks.adoc: This hook can be used to perform repository validit
     +~~~~~~~~~~~~~~~~~
     +
     +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/`).
    ++has created and set up a new working tree. It takes no parameters.
     +
    -+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.
    ++The hook's current working directory is the new working tree, so further
    ++details, such as its absolute path, identifier, and checked-out branch,
    ++can be queried by running `git`. 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.
    ++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.
    @@ Documentation/githooks.adoc: This hook can be used to perform repository validit
      
     
      ## builtin/worktree.c ##
    +@@ builtin/worktree.c: static void delete_worktrees_dir_if_empty(void)
    + 	free(path);
    + }
    + 
    ++static int run_post_worktree_add_hook(const char *path)
    ++{
    ++	struct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
    ++
    ++	strvec_pushl(&hook_opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
    ++	hook_opt.dir = path;
    ++	return run_hooks_opt(the_repository, "post-worktree-add", &hook_opt);
    ++}
    ++
    + static void prune_worktree(const char *id, const char *reason)
    + {
    + 	if (show_only || verbose)
     @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refname,
      	}
      
    @@ builtin/worktree.c: static int add_worktree(const char *path, const char *refnam
     +	 * 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;
    -@@ builtin/worktree.c: static int add_worktree(const char *path, const char *refname,
    - 		ret = run_hooks_opt(the_repository, "post-checkout", &opt);
    - 	}
    - 
    +-	if (!ret && opts->checkout && !opts->orphan) {
    +-		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,
    +-			     oid_to_hex(null_oid(the_hash_algo)),
    +-			     oid_to_hex(&commit->object.oid),
    +-			     "1",
    +-			     NULL);
    +-		opt.dir = path;
    +-
    +-		ret = run_hooks_opt(the_repository, "post-checkout", &opt);
     +	if (!ret) {
    -+		struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT_FORCE_SERIAL;
    ++		int hook_ret;
    ++
    ++		if (opts->checkout && !opts->orphan) {
    ++			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;
    ++			strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL);
    ++			strvec_pushl(&opt.args,
    ++				     oid_to_hex(null_oid(the_hash_algo)),
    ++				     oid_to_hex(&commit->object.oid),
    ++				     "1",
    ++				     NULL);
    ++			opt.dir = path;
     +
    -+		ret = run_hooks_opt(the_repository, "post-worktree-add", &opt);
    -+	}
    ++			ret = run_hooks_opt(the_repository, "post-checkout", &opt);
    ++		}
     +
    ++		hook_ret = run_post_worktree_add_hook(wt->path);
    ++		if (!ret)
    ++			ret = hook_ret;
    + 	}
    + 
      	strvec_clear(&child_env);
    - 	strbuf_release(&sb);
    - 	strbuf_release(&symref);
     
      ## t/t2400-worktree-add.sh ##
     @@ t/t2400-worktree-add.sh: test_expect_success '"add" in bare repo invokes post-checkout hook' '
    @@ t/t2400-worktree-add.sh: test_expect_success '"add" in bare repo invokes post-ch
     +	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
    ++	test "$#" = 0 &&
    ++	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
    @@ t/t2400-worktree-add.sh: test_expect_success '"add" in bare repo invokes post-ch
     +	test_cmp hooks.expect wobble/hooks.actual
     +'
     +
    -+test_expect_success 'failing post-checkout hook suppresses post-worktree-add hook' '
    ++test_expect_success 'failing post-checkout hook does not suppress post-worktree-add hook' '
     +	test_when_finished "rm -rf .git/hooks" &&
     +	mkdir .git/hooks &&
     +	test_hook post-checkout <<-\EOF &&
    @@ t/t2400-worktree-add.sh: test_expect_success '"add" in bare repo invokes post-ch
     +	>post-worktree-add.ran
     +	EOF
     +	test_must_fail git worktree add wozzle &&
    -+	test_path_is_missing wozzle/post-worktree-add.ran
    ++	test_path_is_file wozzle/post-worktree-add.ran
     +'
     +
     +test_expect_success 'failing post-worktree-add hook leaves worktree in place' '
    @@ t/t2400-worktree-add.sh: test_expect_success '"add" in bare repo invokes post-ch
     +	test_path_is_missing hook.ran
     +'
     +
    -+test_expect_success 'post-worktree-add hook gets absolute path with relative worktrees' '
    ++test_expect_success 'post-worktree-add hook can derive 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
    ++	test "$#" = 0 &&
    ++	git rev-parse --show-toplevel >hook.actual
     +	EOF
     +	git -C relhook worktree add --relative-paths --detach wt &&
    -+	echo $(pwd)/relhook/wt wt >hook.expect &&
    ++	echo $(pwd)/relhook/wt >hook.expect &&
     +	test_cmp hook.expect relhook/wt/hook.actual
     +'
     +
2:  7e109ece23 ! 2:  3de87064c0 worktree: add post-worktree-remove hook
    @@ Commit message
         Signed-off-by: Domen Kožar <domen@cachix.org>
         Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
     
    + ## Documentation/config/hook.adoc ##
    +@@ Documentation/config/hook.adoc: hook.jobs::
    + `pre-commit`;;
    + `post-checkout`;;
    + `post-worktree-add`;;
    ++`post-worktree-remove`;;
    + `push-to-checkout`;;
    + `post-commit`;;
    + 	Access the working tree, index, or repository state.
    +
      ## Documentation/githooks.adoc ##
    -@@ Documentation/githooks.adoc: runs after the `post-checkout` hook, and is skipped if that hook fails.
    +@@ Documentation/githooks.adoc: 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.
      
    @@ Documentation/githooks.adoc: runs after the `post-checkout` hook, and is skipped
      
     
      ## builtin/worktree.c ##
    -@@ builtin/worktree.c: static void delete_worktrees_dir_if_empty(void)
    - 	free(path);
    +@@ builtin/worktree.c: static int run_post_worktree_add_hook(const char *path)
    + 	return run_hooks_opt(the_repository, "post-worktree-add", &hook_opt);
      }
      
     +static int run_post_worktree_remove_hook(const char *path, const char *id)
3:  143da548e4 = 3:  7989a1d6a2 worktree: run post-worktree-remove hook when pruning
-:  ---------- > 4:  95ab61e377 worktree: add post-worktree-move hook
-- 
2.54.0

  parent reply	other threads:[~2026-08-04 18:17 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-09 23:36 [PATCH v1 0/3] worktree: add post-worktree-add and post-worktree-remove hooks Domen Kožar
2026-07-10  9:34 ` Phillip Wood
     [not found]   ` <CAMvcdZS=ZYbLmjKaGJvjQ_fWYhVbOzwMvYq+MMENWPYi_RiqvQ@mail.gmail.com>
2026-07-13 13:19     ` Phillip Wood
2026-08-04 18:14 ` Domen Kožar [this message]
2026-08-04 19:03   ` [PATCH v2 0/4] worktree: add lifecycle hooks Caleb White
2026-08-04 20:28     ` Junio C Hamano
     [not found] ` <20260804181358.532970-1-domen@cachix.org>
2026-08-04 18:14   ` [PATCH v2 1/4] worktree: add post-worktree-add hook Domen Kožar
2026-08-04 20:03     ` Caleb White
2026-08-04 18:14   ` [PATCH v2 2/4] worktree: add post-worktree-remove hook Domen Kožar
2026-08-04 18:14   ` [PATCH v2 3/4] worktree: run post-worktree-remove hook when pruning Domen Kožar
2026-08-04 18:14   ` [PATCH v2 4/4] worktree: add post-worktree-move hook 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=371a01cf-2765-4cf5-b1fd-414d1b55a325@mtasv.net \
    --to=domen@cachix.org \
    --cc=avarab@gmail.com \
    --cc=cdwhite3@pm.me \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=phillip.wood123@gmail.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