worktree: copy-on-write creation and shared-branch worktrees
cover worktree: copy-on-write creation and shared-branch worktrees
When many worktrees share one repository -- e .g. a fleet of agents each needing an isolated checkout -- "git worktree add" is costly at scale. Objects are shared via the common dir, but the working tree is not: each add rewrites every tracked file, so N worktrees cost N full checkouts of disk and I/O. And a branch can only be checked out in one worktree.
Patch 1 adds "git worktree add --reflink": on a copy-on-write filesystem it populates the new worktree by reflinking the current worktree's files and index, then "git reset --hard" rewrites only the paths that differ from . A reflink_file() helper in copy.c uses FICLONE (Linux) and clonefile() (macOS); elsewhere (other filesystems, Windows) it is probed up front and falls back to a normal checkout. Defaulting is via the worktree.reflink config (true/false/auto); --no-reflink overrides.
Patch 2 lets a branch be checked out in several worktrees, for parallel work on one checkout. A branch mid-rebase or mid-bisect elsewhere is still refused.
Benchmark (Linux-kernel fork, 93k files, ~33 GB tree incl. build output, btrfs): a normal add allocates ~0.9 GB of real disk per worktree (~5.3 GB for four, linear); --reflink allocates ~0 at any count and also carries the untracked build tree. ("Real disk" = btrfs exclusive bytes.)
worktree-reflink-bench
[https://github.com/user-attachments/assets/e3e721c8-2206-4b78-ad08-21677ef30753]
Note: patch 2 changes a default (same-branch checkout now allowed); two t2400 assertions were updated accordingly.
Jason Newton (2):
worktree: add --reflink for copy-on-write worktree creation
worktree: allow sharing a checked-out branch across worktrees
Documentation/config/worktree.adoc | 10 ++ Documentation/git-worktree.adoc | 47 +++++- builtin/worktree.c | 257 ++++++++++++++++++++++++++++- copy.c | 65 ++++++++ copy.h | 13 ++ t/t2400-worktree-add.sh | 119 ++++++++++++- 6 files changed, 493 insertions(+), 18 deletions(-)
base-commit: c69baaf57ba26cf117c2b6793802877f19738b0d
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2317%2Fnevion%2Fworktree-reflink-cow-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2317/nevion/worktree-reflink-cow-v1
Pull-Request: https://github.com/git/git/pull/2317
Signature · 1 line
gitgitgadget
PATCH 1/2 worktree: add --reflink for copy-on-write worktree creation
From: Jason Newton <[email protected]>
Creating many worktrees from the same base -- for example to run a fleet of automated agents in parallel -- is expensive today: every "git worktree add" materializes the entire working tree by writing each tracked file out from the object store. The objects are shared via the common directory, but the working tree is not: N worktrees mean N full checkouts on disk and N times the file I/O.
Add a "--reflink" option that, on copy-on-write filesystems, populates the new worktree by reflinking the current worktree's files and index instead. The subsequent "git reset --hard" then only rewrites the paths that actually differ between the current worktree and <commit-ish>; everything else (including untracked files such as build outputs) keeps sharing storage with the source until modified. Because the cloned index still carries the source files' stat data, it is refreshed against the reflinked files first so that reset recognizes the unchanged paths as up to date and leaves them sharing extents rather than rewriting them.
The clones are made by a new reflink_file() helper in copy.c, which uses the FICLONE ioctl on Linux and clonefile() on macOS and reports an error otherwise so callers fall back to a normal copy. Support is probed up front; when unavailable -- including on filesystems without copy-on-write and on platforms such as Windows that lack a reflink primitive -- "--reflink" transparently falls back to an ordinary checkout, so the worst case is no slower than today rather than a byte-for-byte copy of the source tree. The directory walk skips the new worktree itself when it lives inside the source one, and preserves symlinks and modes.
The behavior can be made the default with the worktree.reflink configuration ("true", "false" or "auto", the last suppressing the unsupported-filesystem warning), and turned off per-invocation with --no-reflink. A configured default degrades quietly in modes that cannot reflink (--orphan, --no-checkout) instead of erroring, so enabling it never breaks those commands. The checkout step continues to honor checkout.workers, so parallel checkout composes with --reflink for the paths that do need rewriting.
--- Documentation/config/worktree.adoc | 10 ++ Documentation/git-worktree.adoc | 30 +++- builtin/worktree.c | 227 ++++++++++++++++++++++++++++- copy.c | 65 +++++++++ copy.h | 13 ++ t/t2400-worktree-add.sh | 88 +++++++++++ 6 files changed, 431 insertions(+), 2 deletions(-)
Documentation/config/worktree.adoc +10 -0
@@ -17,3 +17,13 @@ Note that setting `worktree.useRelativePaths` to "`true`" implies enabling the `extensions.relativeWorktrees` config (see linkgit:git-config[1]), thus making it incompatible with older versions of Git. + +`worktree.reflink`:: + Controls whether `git worktree add` populates new worktrees with + copy-on-write (reflink) clones, as if `--reflink` had been given + (see linkgit:git-worktree[1]). May be set to "`true`", "`false`" + (the default), or "`auto`". With "`true`", a filesystem that does + not support reflinks produces a warning before falling back to an + ordinary checkout; with "`auto`", the fallback is silent. An + explicit `--reflink` or `--no-reflink` on the command line + overrides this setting.
Documentation/git-worktree.adoc +29 -1
@@ -10,7 +10,7 @@ SYNOPSIS -------- [synopsis] git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]] - [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>] + [--orphan] [--reflink] [(-b | -B) <new-branch>] <path> [<commit-ish>] git worktree list [-v | --porcelain [-z]] git worktree lock [--reason <string>] <worktree> git worktree move <worktree> <new-path>
@@ -213,6 +213,34 @@ To remove a locked worktree, specify `--force` twice. such as configuring sparse-checkout. See "Sparse checkout" in linkgit:git-read-tree[1]. +`--reflink`:: +`--no-reflink`:: + Populate the new worktree by creating copy-on-write (reflink) + clones of the current worktree's files and index instead of + writing every file out from the object store. The checkout that + follows then only has to rewrite the paths that actually differ + between the current worktree and _<commit-ish>_; everything else + (including untracked files such as build outputs) keeps sharing + storage with the source worktree until modified. This makes + `add` much faster and far cheaper on disk when creating many + worktrees from the same base. `--no-reflink` forces an ordinary + checkout, overriding the `worktree.reflink` configuration. ++ +Reflinks require a copy-on-write filesystem (for example btrfs, XFS, +bcachefs or ZFS on Linux, or APFS on macOS). On filesystems or platforms +that do not support reflinks, `--reflink` transparently falls back to an +ordinary checkout. Because the source worktree's untracked and ignored +files are cloned as well, only use `--reflink` when that is acceptable. ++ +This option cannot be combined with `--no-checkout` or `--orphan`. It can +be enabled by default with the `worktree.reflink` configuration; see +linkgit:git-config[1]. ++ +The checkout that populates a new worktree also honors the +`checkout.workers` configuration (see linkgit:git-config[1]), so setting it +parallelizes the file writes and can further speed up `add`, with or +without `--reflink`. + `--guess-remote`:: `--no-guess-remote`:: With `worktree add <path>`, without _<commit-ish>_, instead
builtin/worktree.c +226 -1
287 lines not on this page · raw message
copy.c +65 -0
74 lines not on this page · raw message
copy.h +13 -0
@@ -7,4 +7,17 @@ int copy_fd(int ifd, int ofd);int copy_file (const char *dst,const char *src,int mode);int copy_file_with_time (const char *dst,const char *src,int mode); +/* +* Create "dst" as a copy-on-write (reflink) clone of the regular file +* "src", so that the two files share their data blocks until one of +* them is modified. "dst" must not already exist. +* +* This only succeeds on filesystems that support block cloning (e.g. +* btrfs, XFS or bcachefs on Linux, APFS on macOS). When the platform or +* filesystem does not support reflinks, -1 is returned with errno set to +* ENOSYS (or the underlying error). Callers are expected to fall back to +* copy_file() in that case. Returns 0 on success. +*/ +int reflink_file (const char *dst,const char *src,int mode); +#endif /* COPY_H */
t/t2400-worktree-add.sh +88 -0
92 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH 2/2 worktree: allow sharing a checked-out branch across worktrees
In reply to Jason Newton via GitGitGadgetFrom: Jason Newton <[email protected]>
When spinning up several worktrees on the same checkout for parallel work (for example a fleet of agents working from one branch), git's refusal to check out a branch that is already checked out elsewhere is just in the way. The restriction exists to stop two worktrees from moving the same branch underneath each other, but plain parallel checkouts do not need that protection.
Drop the restriction: "git worktree add <branch>" now checks out a branch even if it is in use by another worktree. The genuinely dangerous case is kept -- a branch that another worktree is in the middle of rebasing or bisecting is still refused, because a second checkout could corrupt that operation. die_if_branch_busy() performs that narrower check in place of the old die_if_checked_out(). The separate guard against force-updating (e.g. with -B) a branch in use elsewhere is left untouched.
--- Documentation/git-worktree.adoc | 17 +++++++++-------- builtin/worktree.c | 30 +++++++++++++++++++++++++++++- t/t2400-worktree-add.sh | 31 ++++++++++++++++++++++++------- 3 files changed, 62 insertions(+), 16 deletions(-)
Documentation/git-worktree.adoc +9 -8
@@ -93,8 +93,9 @@ then, as a convenience, the new worktree is associated with a branch (call it _<branch>_) named after `$(basename <path>)`. If _<branch>_ doesn't exist, a new branch based on `HEAD` is automatically created as if `-b <branch>` was given. If _<branch>_ does exist, it will be checked out -in the new worktree, if it's not checked out anywhere else, otherwise the -command will refuse to create the worktree (unless `--force` is used). +in the new worktree, even if it is already checked out in another worktree. +(A branch that another worktree is in the middle of rebasing or bisecting is +refused unless `--force` is used.) + If _<commit-ish>_ is omitted, neither `--detach`, or `--orphan` is used, and there are no valid local branches (or remote branches if
@@ -177,12 +178,12 @@ OPTIONS `-f`:: `--force`:: - By default, `add` refuses to create a new worktree when - _<commit-ish>_ is a branch name and is already checked out by - another worktree, or if _<path>_ is already assigned to some - worktree but is missing (for instance, if _<path>_ was deleted - manually). This option overrides these safeguards. To add a missing but - locked worktree path, specify `--force` twice. + `add` refuses to create a new worktree when _<commit-ish>_ is a + branch that another worktree is in the middle of rebasing or + bisecting, or if _<path>_ is already assigned to some worktree but + is missing (for instance, if _<path>_ was deleted manually). This + option overrides these safeguards. To add a missing but locked + worktree path, specify `--force` twice. + `move` refuses to move a locked worktree unless `--force` is specified twice. If the destination is already assigned to some other worktree but is
builtin/worktree.c +29 -1
@@ -648,6 +648,34 @@ static void setup_alternate_ref_dir(struct worktree *wt, const char *wt_git_pathstrbuf_release (&sb); } +/* +* Checking out a branch that is already checked out in another worktree is +* fine -- it is exactly what you want when spinning up several worktrees on +* the same checkout for parallel work. The one case that is still unsafe is a +* branch that another worktree is in the middle of rebasing or bisecting, +* since a second checkout could corrupt that operation, so refuse only that. +*/ +static void die_if_branch_busy (const char *branch) +{ +struct worktree **worktrees =get_worktrees (); +int i; + +for (i =0 ; worktrees[i]; i++) { +const struct worktree *wt = worktrees[i]; + +if (is_worktree_being_rebased (wt, branch) || +is_worktree_being_bisected (wt, branch)) { +const char *shortname = branch; + +skip_prefix (branch,"refs/heads/" , &shortname); +die (_ ("'%s' is already used by worktree at '%s'" ), + shortname, wt->path); + } + } + +free_worktrees (worktrees); +} +static int add_worktree (const char *path,const char *refname,const struct add_opts *opts) {
@@ -675,7 +703,7 @@ static int add_worktree(const char *path, const char *refname, refs_ref_exists(get_main_ref_store(the_repository), symref.buf)) { is_branch =1 ;if (!opts->force) -die_if_checked_out (symref.buf,0 ); +die_if_branch_busy (symref.buf); } commit =lookup_commit_reference_by_name (refname);if (!commit && !opts->orphan)
t/t2400-worktree-add.sh +24 -7
@@ -40,10 +40,24 @@ test_expect_success '"add" using - shorthand' ' test_cmp expect actual ' -test_expect_success '"add" refuses to checkout locked branch' ' - test_must_fail git worktree add zere main && - test_path_is_missing zere && - test_path_is_missing .git/worktrees/zere +test_expect_success '"add" can check out a branch in use by another worktree' ' + test_when_finished "git worktree remove -f zere || :" && + git worktree add zere main && + echo refs/heads/main >expect && + git -C zere symbolic-ref HEAD >actual && + test_cmp expect actual +' + +test_expect_success 'the same branch can be checked out in several worktrees' ' + test_when_finished "git worktree remove -f shared1 || :; git worktree remove -f shared2 || :" && + git branch -f sharedbr main && + git worktree add shared1 sharedbr && + git worktree add shared2 sharedbr && + echo refs/heads/sharedbr >expect && + git -C shared1 symbolic-ref HEAD >actual && + test_cmp expect actual && + git -C shared2 symbolic-ref HEAD >actual && + test_cmp expect actual ' test_expect_success 'checking out paths not complaining about linked checkouts' '
@@ -304,10 +318,13 @@ test_expect_success '"add" checks out existing branch of dwimd name' ' ) ' -test_expect_success '"add <path>" dwim fails with checked out branch' ' +test_expect_success '"add <path>" dwim shares a checked out branch' ' git checkout -b test-branch && - test_must_fail git worktree add test-branch && - test_path_is_missing test-branch + git worktree add test-branch && + echo refs/heads/test-branch >expect && + git -C test-branch symbolic-ref HEAD >actual && + test_cmp expect actual && + git worktree remove test-branch ' test_expect_success '"add --force" with existing dwimd name doesnt die' '
Signature · 1 line
gitgitgadget
Quoted 6 lines · Jason Newton via GitGitGadget
When many worktrees share one repository -- e .g. a fleet of agents each needing an isolated checkout -- "git worktree add" is costly at scale. Objects are shared via the common dir, but the working tree is not: each add rewrites every tracked file, so N worktrees cost N full checkouts of disk and I/O. And a branch can only be checked out in one worktree.
Quoted 7 lines
Patch 1 adds "git worktree add --reflink": on a copy-on-write filesystem it populates the new worktree by reflinking the current worktree's files and index, then "git reset --hard" rewrites only the paths that differ from . A
reflink_file()helper incopy.cuses FICLONE (Linux) andclonefile()(macOS); elsewhere (other filesystems, Windows) it is probed up front and falls back to a normal checkout. Defaulting is via the worktree.reflink config (true/false/auto); --no-reflink overrides.
Windows apparently has CoW functionality if you use ReFS. I believe Git LFS has code to do this and you may be interested in checking it out.
Also, how does this work if worktree A is dirty (but git update-index and git status have not been run) when the reflink occurs? Does B have stale files from the working tree? If not, how do we plan on detecting that? (While I'm curious, this should also be explained in your commit message because we want to know that you have thought about this problem and have a good answer for it.)
I was curious as to how this would work with containers, which typically use overlayfs, but some searching reveals that overlayfs does indeed support reflinks. Thanks for the opportunity to learn something new today.
Quoted 3 lines
Patch 2 lets a branch be checked out in several worktrees, for parallel work on one checkout. A branch mid-rebase or mid-bisect elsewhere is still refused.
So how does this work if you have two worktrees for the same branch, A and B, and A commits, and then B does? What we don't want to happen is that because B's worktree is not up to date, it effectively reverts the changes that A made when adding objects to the index to commit. (Again, this is a good thing to explain in your commit message, since reviewers will be curious.)
My personal approach, if I needed many worktrees of the same commit, would be to create many refs pointing to the same object ID and check those out. git update-ref can perform a single ref transaction with many refs, which is especially efficient with reftable. That would avoid the need for multiple checkout support, although I could still see the utility of reflinking if it can be done safely. If that's a solution that you think would be valuable, you could propose it as a FAQ entry or an edit to the manual page, since I'm sure there are other people with your use case.
Signature · 2 lines
brian m. carlson (they/them) Toronto, Ontario, CA
Quoted 5 lines · Jason Newton via GitGitGadget
When many worktrees share one repository -- e .g. a fleet of agents each needing an isolated checkout -- "git worktree add" is costly at scale. Objects are shared via the common dir, but the working tree is not: each add rewrites every tracked file, so N worktrees cost N full checkouts of disk and I/O.
Are the "CoW" semantics offered by these underlying mechanisms, which may differ per operating system and possibly filesystem type, all meant as mere storage-space optimization, or do some of them trade potential space saving with some limitation of the features, i.e., what you can do in the CoW copy and original, or increased runtime cost, either at the clone time or the time of first modification?
What I am trying to get at is why should this be even an opt-in feature. If "cp treeA treeB" at the shell level would make all the files in treeA under identical names and contents in treeB, and let you edit/update/delete copies in either tree without affecting the other tree, then in practice you would not even be able to _tell_ if CoW is in use, no?
It may tilt the scale if there is a downside associated with the use of CoW, like at the first modification of one copy, the system may need to do real copies of other copies, but even such a cost should not be outrageously worse than the cost of copying everything once at the worktree creation time.
So I would understand "whenever we say git_copy_file(A, B), we always use CoW facility under the hood if available, regardless of the purpose of the operation to copy one file to another location--- it may include, but does not have to be limited to, populating working file trees in a new worktree", and I think it is a welcome change.
But I do not quite get "... only if the user gives --reflink option". Why is it even necessary to offer a choice? Especially since you seem to have auto-probe, we should be able to implement a low-level operation to materialize contents identified by a_hash at a_path in the working tree in two different ways, switching on the availablity of CoW, e.g.,
if (CoW available && we can find existing path with a_hash) {
copy-cow the found path to a_path;
} else {
write object identified by a_hash to a_path;
}Quoted 1 line
And a branch can only be checked out in one worktree.
This is a safety feature that has nothing to do with shared files across worktrees, no? Your two worktrees may think they have a checkout of the same branch (thus the same commit), one worktree makes changes and commits, the other worktree suddenly starts seeing a totally different output from its "git diff HEAD" that mixes what it did relative to where it started (which is what we want) plus the reversion of what was done in the other worktree (which is definitely not what we want).
v2 cover worktree: copy-on-write creation and ephemeral branches
In reply to Jason Newton via GitGitGadgetWhen many worktrees share one repository -- for example a fleet of agents each needing an isolated checkout -- "git worktree add" is costly at scale. Objects are shared via the common directory, but the working tree is not:
each add rewrites every tracked file, so N worktrees cost N full checkouts of disk and I/O. A branch created only to back one of those worktrees is also left behind when the worktree is removed.
Patch 1 adds "git worktree add --reflink". On a copy-on-write filesystem it populates the new worktree by reflinking the current worktree's files and index, then "git reset --hard" rewrites only the paths that differ from . reflink_file() in copy.c uses FICLONE (Linux), clonefile() (macOS), and ReFS block cloning (FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does). Elsewhere it is probed up front and falls back to a normal checkout. worktree.reflink (true/false/auto) sets the default; --no-reflink overrides.
Uncommitted edits to tracked files in the source do not leak into the new worktree. The copied index is refreshed before reset, and because every reflinked file is a new inode the refresh re-validates by content. Only untracked files are carried over. The option stays opt-in for that reason:
it copies the source's untracked and ignored files, which a plain checkout does not.
For many worktrees at one commit, the series does not share a branch. git-worktree(1) shows a single update-ref transaction that creates a ref per worktree (or a detached HEAD). A shared branch still lets a stale index in one worktree silently revert a commit made in another.
Patch 2 treats a branch that "git worktree add " created itself as owned by that worktree. "git worktree remove" deletes it. Branches named with -b/-B, or that already existed, are left alone. --keep-branch keeps an owned branch.
Changes since v1:
* Dropped the patch that allowed one branch to be checked out in several worktrees. Documented the per-ref / detached-HEAD pattern instead (brian m. carlson, Junio).
* Explained, in the commit message and the manual, why a dirty source worktree does not leak tracked content (brian m. carlson).
* Added the ReFS block-cloning arm (brian m. carlson).
* Noted overlayfs as a supported setup when the lower filesystem supports reflinks (brian m. carlson).
* Kept --reflink opt-in. A whole-tree reflink is not invisible: it carries untracked files, and cloning every file can be slower than checkout when the files are small (Junio; qub1n on the ReFS work).
* Rebased onto 47fdf2208c ("The 7th batch"). reflink_file() now takes a repository argument, matching copy_file().
* Added ephemeral-branch reaping for the auto-named branch that "worktree add " creates.
Jason Newton (2):
worktree: add --reflink for copy-on-write worktree creation
worktree: reap ephemeral branches created by "worktree add"
Documentation/config/worktree.adoc | 10 + Documentation/git-worktree.adoc | 71 ++++++- builtin/branch.c | 28 +++ builtin/worktree.c | 301 ++++++++++++++++++++++++++++- copy.c | 190 ++++++++++++++++++ copy.h | 14 ++ t/t2400-worktree-add.sh | 143 ++++++++++++++ t/t3200-branch.sh | 2 +- t/t3400-rebase.sh | 2 +- t/t5516-fetch-push.sh | 2 +- 10 files changed, 754 insertions(+), 9 deletions(-)
base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2317%2Fnevion%2Fworktree-reflink-cow-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2317/nevion/worktree-reflink-cow-v2
Pull-Request: https://github.com/git/git/pull/2317
Range-diff vs v1:
1: 7dbd9ca94f ! 1: 83841020f4 worktree: add --reflink for copy-on-write worktree creation
@@ Commit message
via the common directory, but the working tree is not: N worktrees
mean N full checkouts on disk and N times the file I/O.
- Add a "--reflink" option that, on copy-on-write filesystems, populates
+ Add a "--reflink" option that, on a copy-on-write filesystem, populates
the new worktree by reflinking the current worktree's files and index
instead. The subsequent "git reset --hard" then only rewrites the
paths that actually differ between the current worktree and
<commit-ish>; everything else (including untracked files such as build
- outputs) keeps sharing storage with the source until modified. Because
- the cloned index still carries the source files' stat data, it is
- refreshed against the reflinked files first so that reset recognizes
- the unchanged paths as up to date and leaves them sharing extents
- rather than rewriting them.
+ outputs) keeps sharing storage with the source until modified.
- The clones are made by a new reflink_file() helper in copy.c, which
- uses the FICLONE ioctl on Linux and clonefile() on macOS and reports
- an error otherwise so callers fall back to a normal copy. Support is
- probed up front; when unavailable -- including on filesystems without
- copy-on-write and on platforms such as Windows that lack a reflink
- primitive -- "--reflink" transparently falls back to an ordinary
- checkout, so the worst case is no slower than today rather than a
- byte-for-byte copy of the source tree. The directory walk skips the
- new worktree itself when it lives inside the source one, and preserves
- symlinks and modes.
+ A new reflink_file() helper in copy.c clones every regular file. There
+ is no size cutoff.
+
+ On Linux it uses the FICLONE ioctl. That is the primitive btrfs, XFS
+ and bcachefs implement, and the one overlayfs forwards when the lower
+ filesystem can clone. This path has been tested on btrfs.
+
+ On macOS it uses clonefile(), the APFS primitive. That path is
+ implemented and has not been run here.
+
+ On Windows it uses ReFS block cloning,
+ FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does. The destination is
+ sized to the source, extents are cloned in chunks of at most 1GiB, and
+ the tail is rounded up to a cluster (64K, then 4K) and cloned; Windows
+ ignores the range past EOF. The tail is not byte-copied, and small
+ files are not skipped. That path is implemented and has not been run
+ on ReFS. A volume that rejects the control code, including NTFS, fails
+ the probe.
+
+ Anywhere else there is no reflink primitive. Support is probed up front
+ by cloning a one-byte file, and where that fails "--reflink" falls back
+ to an ordinary checkout instead of byte-copying the source tree. The
+ directory walk skips the new worktree itself when it lives inside the
+ source one, and preserves symlinks and modes.
+
+ Uncommitted changes in the source do not leak into the new worktree.
+ After reflinking the source's files we copy its index and run
+ "update-index --refresh" before "reset --hard": because every reflinked
+ file is a fresh inode, the cached stat never matches, so the refresh is
+ forced to re-validate by content rather than by stat. A tracked path is
+ therefore only left sharing extents when its content equals the indexed
+ object; any path whose content differs from <commit-ish> -- which
+ includes every uncommitted modification in the source -- is rewritten
+ by the reset. The new worktree is thus always a faithful checkout of
+ <commit-ish> for tracked paths, and only untracked files are carried
+ over.
The behavior can be made the default with the worktree.reflink
configuration ("true", "false" or "auto", the last suppressing the
unsupported-filesystem warning), and turned off per-invocation with
--no-reflink. A configured default degrades quietly in modes that
cannot reflink (--orphan, --no-checkout) instead of erroring, so
- enabling it never breaks those commands. The checkout step continues
- to honor checkout.workers, so parallel checkout composes with
- --reflink for the paths that do need rewriting.
+ enabling it never breaks those commands. The checkout step continues to
+ honor checkout.workers, so parallel checkout composes with --reflink for
+ the paths that do need rewriting.
+
+ To check out many worktrees at the same commit safely, give each its
+ own ref (or a detached HEAD) rather than sharing one branch; a
+ git-worktree(1) example shows the one-shot "update-ref" transaction
+ that pairs with --reflink.
Signed-off-by: Jason Newton <[email protected]>
@@ Documentation/git-worktree.adoc: To remove a locked worktree, specify `--force`
+ worktrees from the same base. `--no-reflink` forces an ordinary
+ checkout, overriding the `worktree.reflink` configuration.
++
-+Reflinks require a copy-on-write filesystem (for example btrfs, XFS,
-+bcachefs or ZFS on Linux, or APFS on macOS). On filesystems or platforms
-+that do not support reflinks, `--reflink` transparently falls back to an
-+ordinary checkout. Because the source worktree's untracked and ignored
-+files are cloned as well, only use `--reflink` when that is acceptable.
++Reflinks require a copy-on-write filesystem: for example btrfs, XFS,
++bcachefs or ZFS on Linux (and overlayfs on top of one of these, as used by
++containers), APFS on macOS, or ReFS on Windows. On a filesystem that does
++not support reflinks, `--reflink` transparently falls back to an ordinary
++checkout. Because the source worktree's untracked and ignored files are
++cloned as well, only use `--reflink` when that is acceptable.
++
+This option cannot be combined with `--no-checkout` or `--orphan`. It can
+be enabled by default with the `worktree.reflink` configuration; see
@@ Documentation/git-worktree.adoc: To remove a locked worktree, specify `--force`
`--guess-remote`::
`--no-guess-remote`::
With `worktree add <path>`, without _<commit-ish>_, instead
+@@ Documentation/git-worktree.adoc: $ popd
+ $ git worktree remove ../temp
+ ------------
+
++To run several independent worktrees from the same starting commit -- for
++example a pool of automated agents -- give each one its own branch rather than
++sharing a single branch between them. A shared branch is unsafe here: if one
++worktree commits and another then commits from its now-stale index, the second
++commit silently drops the first one's changes. Create the branches in a single
++ref transaction and check each out in its own (optionally reflinked) worktree:
++
++------------
++$ oid=$(git rev-parse HEAD)
++$ printf 'create refs/heads/agent-%s %s\n' 1 "$oid" 2 "$oid" 3 "$oid" |
++ git update-ref --stdin
++$ for n in 1 2 3; do git worktree add --reflink ../agent-$n agent-$n; done
++------------
++
++The single linkgit:git-update-ref[1] transaction is cheap and is especially
++efficient with the `reftable` backend. If a worktree does not need to advance a
++branch at all, use a detached `HEAD` (`git worktree add --detach`) instead.
++
+ CONFIGURATION
+ -------------
+
## builtin/worktree.c ##
@@
@@ builtin/worktree.c: worktree_copy_cleanup:
+/*
+ * Probe whether the filesystem backing "dir" supports reflinks. We do this
-+ * once up front so that, on filesystems without copy-on-write support (or on
-+ * platforms such as Windows that lack a reflink primitive entirely), we can
++ * once up front so that, on filesystems without copy-on-write support (and on
++ * platforms that lack a reflink primitive entirely), we can
+ * fall back to a normal checkout instead of byte-copying the whole source
+ * working tree -- which would include untracked files and be slower than the
+ * checkout we are trying to avoid.
@@ builtin/worktree.c: worktree_copy_cleanup:
+ if (fd >= 0) {
+ write_in_full(fd, "x", 1);
+ close(fd);
-+ if (!reflink_file(dst.buf, src.buf, 0600))
++ if (!reflink_file(the_repository, dst.buf, src.buf, 0600))
+ ok = 1;
+ unlink(dst.buf);
+ }
@@ builtin/worktree.c: worktree_copy_cleanup:
+ */
+static int reflink_or_copy(const char *dst, const char *src, int mode)
+{
-+ if (!reflink_file(dst, src, mode))
++ if (!reflink_file(the_repository, dst, src, mode))
+ return 0;
-+ return copy_file(dst, src, mode);
++ return copy_file(the_repository, dst, src, mode);
+}
+
+/*
@@ copy.c
+#elif defined(__APPLE__)
+#include <sys/attr.h>
+#include <sys/clonefile.h>
++#elif defined(GIT_WINDOWS_NATIVE)
++#include <winioctl.h>
++/*
++ * Block cloning on ReFS uses FSCTL_DUPLICATE_EXTENTS_TO_FILE. Older Windows
++ * SDK headers may lack the control code and its payload struct, so define
++ * them ourselves when missing (the values are part of the stable Win32 API).
++ */
++#ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE
++#define FSCTL_DUPLICATE_EXTENTS_TO_FILE \
++ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 209, METHOD_BUFFERED, FILE_WRITE_DATA)
++typedef struct _DUPLICATE_EXTENTS_DATA {
++ HANDLE FileHandle;
++ LARGE_INTEGER SourceFileOffset;
++ LARGE_INTEGER TargetFileOffset;
++ LARGE_INTEGER ByteCount;
++} DUPLICATE_EXTENTS_DATA, *PDUPLICATE_EXTENTS_DATA;
++#endif
+#endif
+
int copy_fd(int ifd, int ofd)
{
while (1) {
-@@ copy.c: int copy_file_with_time(const char *dst, const char *src, int mode)
+@@ copy.c: int copy_file_with_time(struct repository *repo,
return copy_times(dst, src);
return status;
}
+
-+int reflink_file(const char *dst, const char *src, int mode)
++#ifdef GIT_WINDOWS_NATIVE
++/*
++ * Clone [offset, offset+count) of "src" onto "dst" via ReFS block cloning.
++ * "count" is rounded up to a cluster boundary by the caller; a range that
++ * runs past the source's end of file is harmless and ignored by Windows.
++ */
++static int clone_extent_windows(HANDLE dst, HANDLE src,
++ uint64_t offset, uint64_t count)
++{
++ DUPLICATE_EXTENTS_DATA dup;
++ DWORD bytes;
++
++ dup.FileHandle = src;
++ dup.SourceFileOffset.QuadPart = offset;
++ dup.TargetFileOffset.QuadPart = offset;
++ dup.ByteCount.QuadPart = count;
++ if (!DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE,
++ &dup, sizeof(dup), NULL, 0, &bytes, NULL))
++ return -1;
++ return 0;
++}
++#endif
++
++int reflink_file(struct repository *repo,
++ const char *dst, const char *src, int mode)
+{
+#if defined(__APPLE__)
+ /*
@@ copy.c: int copy_file_with_time(const char *dst, const char *src, int mode)
+ (void)mode;
+ if (clonefile(src, dst, 0) < 0)
+ return -1;
-+ if (adjust_shared_perm(the_repository, dst))
++ if (adjust_shared_perm(repo, dst))
+ return -1;
+ return 0;
+#elif defined(__linux__)
@@ copy.c: int copy_file_with_time(const char *dst, const char *src, int mode)
+ }
+ if (close(fdo) != 0)
+ return -1;
-+ if (adjust_shared_perm(the_repository, dst))
++ if (adjust_shared_perm(repo, dst))
++ return -1;
++ return 0;
++#elif defined(GIT_WINDOWS_NATIVE)
++ /*
++ * ReFS supports copy-on-write block cloning via
++ * FSCTL_DUPLICATE_EXTENTS_TO_FILE, the same mechanism Git LFS uses.
++ * Cloned ranges must start and end on a cluster boundary (ReFS uses
++ * 64K or 4K clusters) and span less than 4GiB, so clone in <=1GiB
++ * chunks and round the final chunk up to a cluster; the destination is
++ * sized to the source's exact length and an over-long final range past
++ * the source's EOF is ignored.
++ */
++ const uint64_t GiB = (uint64_t)1 << 30;
++ const uint64_t clusters[] = { 64 * 1024, 4 * 1024 };
++ int fdi, fdo, ret = -1;
++ HANDLE hi, ho;
++ LARGE_INTEGER size;
++ uint64_t off;
++ size_t i;
++
++ mode = (mode & 0111) ? 0777 : 0666;
++ if ((fdi = open(src, O_RDONLY | O_BINARY)) < 0)
++ return -1;
++ if ((fdo = open(dst, O_RDWR | O_CREAT | O_EXCL | O_BINARY, mode)) < 0) {
++ int saved = errno;
++ close(fdi);
++ errno = saved;
++ return -1;
++ }
++ hi = (HANDLE)_get_osfhandle(fdi);
++ ho = (HANDLE)_get_osfhandle(fdo);
++ if (hi == INVALID_HANDLE_VALUE || ho == INVALID_HANDLE_VALUE ||
++ !GetFileSizeEx(hi, &size)) {
++ errno = EINVAL;
++ goto cleanup;
++ }
++
++ /* the destination must be the source's exact size before cloning */
++ if (!SetFilePointerEx(ho, size, NULL, FILE_BEGIN) || !SetEndOfFile(ho)) {
++ errno = EIO;
++ goto cleanup;
++ }
++
++ ret = 0;
++ /* clone whole 1GiB regions (already cluster aligned) */
++ for (off = 0; (uint64_t)size.QuadPart > off &&
++ (uint64_t)size.QuadPart - off > GiB; off += GiB) {
++ if (clone_extent_windows(ho, hi, off, GiB)) {
++ errno = ENOSYS;
++ ret = -1;
++ goto cleanup;
++ }
++ }
++ /* clone the tail, rounding its length up to a cluster (64K then 4K) */
++ if (!ret && (uint64_t)size.QuadPart > off) {
++ uint64_t rem = (uint64_t)size.QuadPart - off;
++
++ ret = -1;
++ for (i = 0; i < ARRAY_SIZE(clusters); i++) {
++ uint64_t count = (rem + clusters[i] - 1) &
++ ~(clusters[i] - 1);
++ if (!clone_extent_windows(ho, hi, off, count)) {
++ ret = 0;
++ break;
++ }
++ }
++ if (ret)
++ errno = ENOSYS; /* not a CoW volume */
++ }
++
++cleanup:
++ close(fdi);
++ if (close(fdo) && !ret) {
++ errno = EIO;
++ ret = -1;
++ }
++ if (ret) {
++ int saved = errno;
++ unlink(dst);
++ errno = saved;
++ return -1;
++ }
++ if (adjust_shared_perm(repo, dst))
+ return -1;
+ return 0;
+#else
-+ /* No reflink support on this platform (e.g. Windows). */
++ /* No reflink primitive on this platform. */
++ (void)repo;
+ (void)dst;
+ (void)src;
+ (void)mode;
@@ copy.c: int copy_file_with_time(const char *dst, const char *src, int mode)
+}
## copy.h ##
-@@ copy.h: int copy_fd(int ifd, int ofd);
- int copy_file(const char *dst, const char *src, int mode);
- int copy_file_with_time(const char *dst, const char *src, int mode);
+@@ copy.h: int copy_file(struct repository *repo,
+ int copy_file_with_time(struct repository *repo,
+ const char *dst, const char *src, int mode);
+/*
+ * Create "dst" as a copy-on-write (reflink) clone of the regular file
@@ copy.h: int copy_fd(int ifd, int ofd);
+ * ENOSYS (or the underlying error). Callers are expected to fall back to
+ * copy_file() in that case. Returns 0 on success.
+ */
-+int reflink_file(const char *dst, const char *src, int mode);
++int reflink_file(struct repository *repo,
++ const char *dst, const char *src, int mode);
+
#endif /* COPY_H */
@@ t/t2400-worktree-add.sh: test_expect_success 'relative worktree sets extension c
+ echo artifact >expect &&
+ test_cmp expect wt/untracked.bin &&
+ git -C wt status --porcelain >status &&
-+ grep "?? untracked.bin" status
++ test_grep "?? untracked.bin" status
+ )
+'
+
2: 97f0fdad9b < -: ---------- worktree: allow sharing a checked-out branch across worktrees
-: ---------- > 2: 6e2acdc5e2 worktree: reap ephemeral branches created by "worktree add"Signature · 1 line
gitgitgadget
PATCH v2 1/2 worktree: add --reflink for copy-on-write worktree creation
From: Jason Newton <[email protected]>
Creating many worktrees from the same base -- for example to run a fleet of automated agents in parallel -- is expensive today: every "git worktree add" materializes the entire working tree by writing each tracked file out from the object store. The objects are shared via the common directory, but the working tree is not: N worktrees mean N full checkouts on disk and N times the file I/O.
Add a "--reflink" option that, on a copy-on-write filesystem, populates the new worktree by reflinking the current worktree's files and index instead. The subsequent "git reset --hard" then only rewrites the paths that actually differ between the current worktree and <commit-ish>; everything else (including untracked files such as build outputs) keeps sharing storage with the source until modified.
A new reflink_file() helper in copy.c clones every regular file. There is no size cutoff.
On Linux it uses the FICLONE ioctl. That is the primitive btrfs, XFS and bcachefs implement, and the one overlayfs forwards when the lower filesystem can clone. This path has been tested on btrfs.
On macOS it uses clonefile(), the APFS primitive. That path is implemented and has not been run here.
On Windows it uses ReFS block cloning, FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does. The destination is sized to the source, extents are cloned in chunks of at most 1GiB, and the tail is rounded up to a cluster (64K, then 4K) and cloned; Windows ignores the range past EOF. The tail is not byte-copied, and small files are not skipped. That path is implemented and has not been run on ReFS. A volume that rejects the control code, including NTFS, fails the probe.
Anywhere else there is no reflink primitive. Support is probed up front by cloning a one-byte file, and where that fails "--reflink" falls back to an ordinary checkout instead of byte-copying the source tree. The directory walk skips the new worktree itself when it lives inside the source one, and preserves symlinks and modes.
Uncommitted changes in the source do not leak into the new worktree. After reflinking the source's files we copy its index and run "update-index --refresh" before "reset --hard": because every reflinked file is a fresh inode, the cached stat never matches, so the refresh is forced to re-validate by content rather than by stat. A tracked path is therefore only left sharing extents when its content equals the indexed object; any path whose content differs from <commit-ish> -- which includes every uncommitted modification in the source -- is rewritten by the reset. The new worktree is thus always a faithful checkout of <commit-ish> for tracked paths, and only untracked files are carried over.
The behavior can be made the default with the worktree.reflink configuration ("true", "false" or "auto", the last suppressing the unsupported-filesystem warning), and turned off per-invocation with --no-reflink. A configured default degrades quietly in modes that cannot reflink (--orphan, --no-checkout) instead of erroring, so enabling it never breaks those commands. The checkout step continues to honor checkout.workers, so parallel checkout composes with --reflink for the paths that do need rewriting.
To check out many worktrees at the same commit safely, give each its own ref (or a detached HEAD) rather than sharing one branch; a git-worktree(1) example shows the one-shot "update-ref" transaction that pairs with --reflink.
--- Documentation/config/worktree.adoc | 10 ++ Documentation/git-worktree.adoc | 49 ++++++- builtin/worktree.c | 227 ++++++++++++++++++++++++++++- copy.c | 190 ++++++++++++++++++++++++ copy.h | 14 ++ t/t2400-worktree-add.sh | 88 +++++++++++ 6 files changed, 576 insertions(+), 2 deletions(-)
Documentation/config/worktree.adoc +10 -0
@@ -18,3 +18,13 @@ Note that setting `worktree.useRelativePaths` to "`true`" implies enabling the `extensions.relativeWorktrees` config (see linkgit:git-config[1]), thus making it incompatible with older versions of Git. + +`worktree.reflink`:: + Controls whether `git worktree add` populates new worktrees with + copy-on-write (reflink) clones, as if `--reflink` had been given + (see linkgit:git-worktree[1]). May be set to "`true`", "`false`" + (the default), or "`auto`". With "`true`", a filesystem that does + not support reflinks produces a warning before falling back to an + ordinary checkout; with "`auto`", the fallback is silent. An + explicit `--reflink` or `--no-reflink` on the command line + overrides this setting.
Documentation/git-worktree.adoc +48 -1
67 lines not on this page · raw message
builtin/worktree.c +226 -1
287 lines not on this page · raw message
copy.c +190 -0
199 lines not on this page · raw message
copy.h +14 -0
@@ -11,4 +11,18 @@ int copy_file(struct repository *repo,int copy_file_with_time (struct repository *repo,const char *dst,const char *src,int mode); +/* +* Create "dst" as a copy-on-write (reflink) clone of the regular file +* "src", so that the two files share their data blocks until one of +* them is modified. "dst" must not already exist. +* +* This only succeeds on filesystems that support block cloning (e.g. +* btrfs, XFS or bcachefs on Linux, APFS on macOS). When the platform or +* filesystem does not support reflinks, -1 is returned with errno set to +* ENOSYS (or the underlying error). Callers are expected to fall back to +* copy_file() in that case. Returns 0 on success. +*/ +int reflink_file (struct repository *repo, +const char *dst,const char *src,int mode); +#endif /* COPY_H */
t/t2400-worktree-add.sh +88 -0
92 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v2 2/2 worktree: reap ephemeral branches created by "worktree add"
In reply to Jason Newton via GitGitGadgetFrom: Jason Newton <[email protected]>
When "git worktree add <path>" is given no branch to check out, it auto-creates one named after the path. Such a branch exists only to back the new worktree, yet it outlives it: removing the worktree leaves the branch behind, so a fleet of throwaway worktrees fills refs/heads/ with dead branches that have to be cleaned up by hand.
Treat an auto-created branch as owned by the worktree. Record it in the worktree's administrative directory (worktrees/<id>/owned-branch) at creation time -- the one moment we can know for certain the branch is worktree-private, since we are the one that created it. "git worktree remove" then deletes the owned branch once the worktree is gone and it is no longer checked out.
Ownership is recorded only when "worktree add" itself creates the branch via DWIM auto-naming; a branch named explicitly with -b/-B, or one that already existed, is never owned and never reaped. Deletion is unconditional (even for unmerged commits) because an ephemeral branch is disposable by contract: to keep the work, pass --keep-branch, or anchor the commits with an ordinary branch beforehand -- a normal branch can point at the same commits, after which reaping the ephemeral ref loses nothing. "git branch -m" updates the marker so ownership follows a rename, and the branch stays ephemeral.
The marker is just another per-worktree administrative file, so older versions of Git simply ignore it (they keep the branch on removal, as today).
--- Documentation/git-worktree.adoc | 22 ++++++++-- builtin/branch.c | 28 +++++++++++++ builtin/worktree.c | 74 ++++++++++++++++++++++++++++++++- t/t2400-worktree-add.sh | 55 ++++++++++++++++++++++++ t/t3200-branch.sh | 2 +- t/t3400-rebase.sh | 2 +- t/t5516-fetch-push.sh | 2 +- 7 files changed, 178 insertions(+), 7 deletions(-)
Documentation/git-worktree.adoc +19 -3
@@ -15,7 +15,7 @@ git worktree list [-v | --porcelain [-z]] git worktree lock [--reason <string>] <worktree> git worktree move <worktree> <new-path> git worktree prune [-n] [-v] [--expire <expire>] -git worktree remove [-f] <worktree> +git worktree remove [-f] [--keep-branch] <worktree> git worktree repair [<path>...] git worktree unlock <worktree>
@@ -40,8 +40,11 @@ In its simplest form, `git worktree add <path>` automatically creates a new branch whose name is the final component of _<path>_, which is convenient if you plan to work on a new topic. For instance, `git worktree add ../hotfix` creates new branch `hotfix` and checks it out at -path `../hotfix`. To instead work on an existing branch in a new worktree, -use `git worktree add <path> <branch>`. On the other hand, if you just +path `../hotfix`. Such an automatically-created branch is _ephemeral_: it +is owned by the worktree and is deleted again when the worktree is removed +with `git worktree remove` (see that command and `--keep-branch`). To +instead work on an existing branch in a new worktree, use +`git worktree add <path> <branch>`. On the other hand, if you just plan to make some experimental changes or do testing without disturbing existing development, it is often convenient to create a 'throwaway' worktree not associated with any branch. For instance,
@@ -145,6 +148,14 @@ Remove a worktree. Only clean worktrees (no untracked files and no modification in tracked files) can be removed. Unclean worktrees or ones with submodules can be removed with `--force`. The main worktree cannot be removed. ++ +If the worktree owns an _ephemeral_ branch -- one that `git worktree add` +created itself because no branch was named (the auto-naming case above) -- +that branch is deleted along with the worktree, even if it has unmerged +commits, since it was created solely to back this worktree. To keep it, use +`--keep-branch`, or anchor the commits with an ordinary branch +(`git branch <name>`) beforehand. A branch you named explicitly (`-b`/`-B`) +or one that existed before the worktree is never deleted. `repair [<path>...]`::
@@ -192,6 +203,11 @@ allows the move to proceed; use `--force` twice if the destination is locked. `remove` refuses to remove an unclean worktree unless `--force` is used. To remove a locked worktree, specify `--force` twice. +`--keep-branch`:: + With `remove`, do not delete the worktree's ephemeral (owned) branch; + keep it as an ordinary branch. Has no effect on worktrees that do not + own a branch. + `-b <new-branch>`:: `-B <new-branch>`:: With `add`, create a new branch named _<new-branch>_ starting at
builtin/branch.c +28 -0
@@ -607,6 +607,32 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,#define IS_HEAD 1#define IS_ORPHAN 2 +/* +* When a branch that a worktree owns (its ephemeral branch, recorded in +* worktrees/<id>/owned-branch) is renamed, update the marker so ownership +* follows the new name -- it stays ephemeral. +*/ +static void rename_owned_branch_markers (struct worktree **worktrees, +const char *oldref,const char *newref) +{ +for (int i =0 ; worktrees[i]; i++) { +struct strbuf path =STRBUF_INIT , buf =STRBUF_INIT ; + +if (!worktrees[i]->id) +continue ;/* the main worktree keeps no marker */ +repo_common_path_append (the_repository, &path, +"worktrees/%s/owned-branch" , + worktrees[i]->id); +if (strbuf_read_file (&buf, path.buf,64 ) >0 ) { +strbuf_trim (&buf); +if (!strcmp (buf.buf, oldref)) +write_file (path.buf,"%s" , newref); + } +strbuf_release (&path); +strbuf_release (&buf); + } +} +static void copy_or_rename_branch (const char *oldname,const char *newname,int copy,int force) {struct strbuf oldref =STRBUF_INIT , newref =STRBUF_INIT , logmsg =STRBUF_INIT ;
@@ -675,6 +701,8 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, intif (!copy && !(oldref_usage &IS_ORPHAN ) &&refs_rename_ref (get_main_ref_store (the_repository), oldref.buf, newref.buf, logmsg.buf))die (_ ("branch rename failed" )); +if (!copy && !(oldref_usage &IS_ORPHAN )) +rename_owned_branch_markers (worktrees, oldref.buf, newref.buf);if (copy &&refs_copy_existing_ref (get_main_ref_store (the_repository), oldref.buf, newref.buf, logmsg.buf))die (_ ("branch copy failed" ));
builtin/worktree.c +73 -1
127 lines not on this page · raw message
t/t2400-worktree-add.sh +55 -0
59 lines not on this page · raw message
t/t3200-branch.sh +1 -1
@@ -190,7 +190,7 @@ test_expect_success 'git branch -M foo bar should fail when bar is checked out' test_expect_success 'git branch -M foo bar should fail when bar is checked out in worktree' ' git branch -f bar && - test_when_finished "git worktree remove wt && git branch -D wt" && + test_when_finished "git worktree remove --keep-branch wt && git branch -D wt" && git worktree add wt && test_must_fail git branch -M bar wt '
t/t3400-rebase.sh +1 -1
@@ -471,7 +471,7 @@ test_expect_success 'git rebase --update-ref with core.commentChar and branch on test_when_finished git branch -D base topic2 && test_when_finished git checkout main && test_when_finished git branch -D wt-topic && - test_when_finished git worktree remove wt-topic && + test_when_finished git worktree remove --keep-branch wt-topic && git checkout main && git checkout -b base && git checkout -b topic2 &&
t/t5516-fetch-push.sh +1 -1
@@ -1873,7 +1873,7 @@ test_expect_success 'updateInstead with bare repository worktree and unborn bare ' test_expect_success 'refuse fetch to current branch of worktree' ' - test_when_finished "git worktree remove --force wt && git branch -D wt" && + test_when_finished "git worktree remove --force --keep-branch wt && git branch -D wt" && git worktree add wt && test_commit apple && test_must_fail git fetch . HEAD:wt &&
Signature · 1 line
gitgitgadget
Re: [PATCH v2 1/2] worktree: add --reflink for copy-on-write worktree creation
In reply to Jason Newton via GitGitGadgetQuoted 24 lines · Jason Newton via GitGitGadget
Creating many worktrees from the same base -- for example to run a fleet of automated agents in parallel -- is expensive today: every "git worktree add" materializes the entire working tree by writing each tracked file out from the object store. The objects are shared via the common directory, but the working tree is not: N worktrees mean N full checkouts on disk and N times the file I/O.
Add a "--reflink" option that, on a copy-on-write filesystem, populates the new worktree by reflinking the current worktree's files and index instead. The subsequent "git reset --hard" then only rewrites the paths that actually differ between the current worktree and <commit-ish>; everything else (including untracked files such as build outputs) keeps sharing storage with the source until modified.
A new
reflink_file()helper incopy.cclones every regular file. There is no size cutoff.On Linux it uses the FICLONE ioctl. That is the primitive btrfs, XFS and bcachefs implement, and the one overlayfs forwards when the lower filesystem can clone. This path has been tested on btrfs.
On macOS it uses
clonefile(), the APFS primitive. That path is implemented and has not been run here.
Quoted 8 lines
On Windows it uses ReFS block cloning,
FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does. The destination is sized to the source, extents are cloned in chunks of at most 1GiB, and the tail is rounded up to a cluster (64K, then 4K) and cloned; Windows ignores the range past EOF. The tail is not byte-copied, and small files are not skipped. That path is implemented and has not been run on ReFS. A volume that rejects the control code, including NTFS, fails the probe.
That functionality is defective; see
https://github.com/git-lfs/git-lfs/issues/6312. We should not implement this on Windows because there's no reasonable way to avoid corruption. It's quite shocking that that bug exists, but it does. Unless we can guarantee a fixed version of Windows (which does not exist yet), we should skip reflinking there.
I'd also like to see us limit this to platforms where it _has_ been manually tested or is guaranteed to be automatically tested in CI. That means we should make sure this runs in CI on APFS for macOS (see below).
t/t2400-worktree-add.sh test_expect_success 'relative worktree sets extension config' ' · line 1356
Quoted 7 lines
t/t2400-worktree-add.sh +7
-0+# Reflinks are only available on copy-on-write filesystems (btrfs, XFS, +# bcachefs, APFS, ...). Where they are unavailable, --reflink transparently +# falls back to a regular checkout, which the test above already covers. +test_lazy_prereq REFLINK ' + echo probe >reflink-src && + cp --reflink=always reflink-src reflink-dst 2>/dev/null +'
Does this work properly on macOS? That is, does the macOS cp binary contain support for --reflink=always? If not, we won't be testing this case properly on macOS.
Signature · 2 lines
brian m. carlson (they/them) Toronto, Ontario, CA
Re: [PATCH v2 2/2] worktree: reap ephemeral branches created by "worktree add"
In reply to Jason Newton via GitGitGadgetQuoted 23 lines · Jason Newton via GitGitGadget
When "git worktree add <path>" is given no branch to check out, it auto-creates one named after the path. Such a branch exists only to back the new worktree, yet it outlives it: removing the worktree leaves the branch behind, so a fleet of throwaway worktrees fills refs/heads/ with dead branches that have to be cleaned up by hand.
Treat an auto-created branch as owned by the worktree. Record it in the worktree's administrative directory (worktrees/<id>/owned-branch) at creation time -- the one moment we can know for certain the branch is worktree-private, since we are the one that created it. "git worktree remove" then deletes the owned branch once the worktree is gone and it is no longer checked out.
Ownership is recorded only when "worktree add" itself creates the branch via DWIM auto-naming; a branch named explicitly with -b/-B, or one that already existed, is never owned and never reaped. Deletion is unconditional (even for unmerged commits) because an ephemeral branch is disposable by contract: to keep the work, pass --keep-branch, or anchor the commits with an ordinary branch beforehand -- a normal branch can point at the same commits, after which reaping the ephemeral ref loses nothing. "git branch -m" updates the marker so ownership follows a rename, and the branch stays ephemeral.
Quoted 5 lines
The marker is just another per-worktree administrative file, so older versions of Git simply ignore it (they keep the branch on removal, as today).
Signed-off-by Jason Newton <[email protected]>
Sorry, I may be missing something obvious, but this message never explains why we should add all this additional complexity for a use-case already directly supported by git worktree add -d <path>. Specifically, worktree's --detach option is exactly what you want when creating throwaway worktrees, even "fleets" of them (using terminology from this apparently fully or partially LLM-generated message). If your tooling or agent is not using -d/--detach for throwaway worktrees, then the problem is fixable by modifying the tooling or by teaching the agent about --detach, without bulking up the git-worktree implementation unnecessarily.
Moreover, a change like this which makes the new behavior unconditional is certainly going to break existing tooling and workflows, thus it will not go over well, even with the --keep-branch escape-hatch. At minimum, to be acceptable, assuming such new functionality is even desirable, to avoid breaking existing setups, it would have to be opt-in, not opt-out (via --keep-branch). But if it's going to be opt-in, then you may as well just use --detach to achieve the same effect.
Re: [PATCH v2 0/2] worktree: copy-on-write creation and ephemeral branches
In reply to Jason Newton via GitGitGadgetQuoted 15 lines · Jason Newton via GitGitGadget
When many worktrees share one repository -- for example a fleet of agents each needing an isolated checkout -- "git worktree add" is costly at scale. Objects are shared via the common directory, but the working tree is not:
each add rewrites every tracked file, so N worktrees cost N full checkouts of disk and I/O. A branch created only to back one of those worktrees is also left behind when the worktree is removed.Patch 1 adds "git worktree add --reflink". On a copy-on-write filesystem it populates the new worktree by reflinking the current worktree's files and index, then "git reset --hard" rewrites only the paths that differ from .
reflink_file()incopy.cuses FICLONE (Linux),clonefile()(macOS), and ReFS block cloning (FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does). Elsewhere it is probed up front and falls back to a normal checkout. worktree.reflink (true/false/auto) sets the default; --no-reflink overrides.
Quoted 5 lines
Changes since v1:
[...]
* Kept --reflink opt-in. A whole-tree reflink is not invisible: it carries untracked files, and cloning every file can be slower than checkout when the files are small (Junio; qub1n on the ReFS work).
I may be missing something, but it's still not clear from this explanation why --reflink must be opt-in rather than default behavior. The above only seems to be touching on Junio's superficial "cp treeA treeB" example[*], but doesn't answer the deeper question he asked about why this can't be the default behavior for individual tracked resources in the worktree (his "a_hash at a_path" example).
Re: [PATCH v2 1/2] worktree: add --reflink for copy-on-write worktree creation
In reply to brian m. carlsonBrian,
macOS cp has no --reflink=always. The clone option is -c, which is clonefile(2). t/t2400-worktree-add.sh already probes that way. The REFLINK prerequisite runs "cp -c" when the MACOS prerequisite is set, and "cp --reflink=always" otherwise. The test that depends on REFLINK is "--reflink carries untracked files and reconciles changes".
That is in 042485f0e9cd8cf1765b811ea7334247aec44971. The pull request CI for that commit is the APFS run:
https://github.com/git/git/pull/2317
https://github.com/git/git/actions/runs/39502233266
All four macos-14 jobs succeeded. The runners were macOS 14.8.9 (23J631), image macos-14-arm64 20260831.0302.1. "print test failures" was skipped on each job, which is the step that runs only when a test script fails.
osx-clang:
https://github.com/git/git/actions/runs/39502233266/job/118902393415t2400-worktree-add.sh ok, 55208 ms
osx-gcc:
https://github.com/git/git/actions/runs/39502233266/job/118902393588t2400-worktree-add.sh ok, 45173 ms
osx-meson:
https://github.com/git/git/actions/runs/39502233266/job/118902393644
osx-reftable:
https://github.com/git/git/actions/runs/39502233266/job/118902393664
The macOS evidence is those jobs.
Reflink is disabled on Windows in "worktree: disable reflink on Windows" (91806975a6909265b1325206ef2f33a169393920). The pull request head is 876fb541a8e1d43731bb72ddbbf1c49da8760775, which keeps that disable and leaves the filesystem probe compiled so the mingw build does not fail with -Werror=unused-function. The CI run linked above is the previous tip, 042485f. The warning names the build from the git-lfs report,
Windows 11 build 26200. A later note on that report reproduced the zero-fill on build 26200.9448 with git-lfs 3.6.1. No fixed build is identified, so every Windows version refuses the ioctl and falls back to a regular checkout. "git worktree add --reflink" and worktree.reflink=true warn with "Windows build 26200". worktree.reflink=auto stays silent. t2400 checks that warning under MINGW.
On the git-lfs report, the failure mode is real and documented here:
https://github.com/git-lfs/git-lfs/issues/6312
https://github.com/git-lfs/git-lfs/pull/6323
FSCTL_DUPLICATE_EXTENTS_TO_FILE can publish zeros when the source data is still cached. FlushFileBuffers on the read-only handle a checkout uses fails with ERROR_ACCESS_DENIED (win32 5). The change in 6323 reopens the source writable, flushes, and skips that flush when the volume does not advertise FILE_SUPPORTS_BLOCK_REFCOUNTING.
The numbers below are from the ioctl path before that disable, on this machine, Windows build 26300.9457. The read-only flush failed with win32 5, the same error the git-lfs report describes. The clones kept the original bytes.
NTFS rejects the ioctl: win32 1 on a USB NTFS partition, win32 50 on C:. The fallback there is a normal write. The USB disk would not format as ReFS. The ReFS volume was a 32 GiB Dev Drive, stored as a VHD on that disk, with block refcounting set. The clone path did not flush.
Content check, every clone, unbuffered SHA-256:
4000 files, 8,388,608 bytes each (33,554,432,000 bytes, 31.25 GiB of clone names). Created with FSCTL_DUPLICATE_EXTENTS_TO_FILE only.
Source before rewrite:106640827aa82610080b1e4c676e2028230b841fa4642faaa292046833950714 Pass 1: old=4000 new=0 other=0 errors=0
Source rewritten to a different pattern, still no flush:e58408ac406d407257cd3a2474b8bba7b615e76bd4ce03e8ec3df9b42a02bc89 Pass 2: old=4000 new=0 other=0 errors=0
FlushFileBuffers on the read-only handle: FAIL win32=5 Reopen writable and flush: OK
Source hash stayed on the rewritten pattern. Pass 3, after that flush: old=4000 new=0 other=0 errors=0
All 4000 clones still hashed to the original source. After they settled, free space was 30,462,386,176 bytes and the backing VHD was still 4,164,943,872 bytes. The volume stayed Healthy. Deleting the clones freed nothing further.
This is one Windows build, alongside the version described in git-lfs #6312.
Quoted 60 lines · brian m. carlson
Quoted 24 lines · Jason Newton via GitGitGadget
Creating many worktrees from the same base -- for example to run a fleet of automated agents in parallel -- is expensive today: every "git worktree add" materializes the entire working tree by writing each tracked file out from the object store. The objects are shared via the common directory, but the working tree is not: N worktrees mean N full checkouts on disk and N times the file I/O.
Add a "--reflink" option that, on a copy-on-write filesystem, populates the new worktree by reflinking the current worktree's files and index instead. The subsequent "git reset --hard" then only rewrites the paths that actually differ between the current worktree and <commit-ish>; everything else (including untracked files such as build outputs) keeps sharing storage with the source until modified.
A new
reflink_file()helper incopy.cclones every regular file. There is no size cutoff.On Linux it uses the FICLONE ioctl. That is the primitive btrfs, XFS and bcachefs implement, and the one overlayfs forwards when the lower filesystem can clone. This path has been tested on btrfs.
On macOS it uses
clonefile(), the APFS primitive. That path is implemented and has not been run here.Quoted 8 lines
On Windows it uses ReFS block cloning,
FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does. The destination is sized to the source, extents are cloned in chunks of at most 1GiB, and the tail is rounded up to a cluster (64K, then 4K) and cloned; Windows ignores the range past EOF. The tail is not byte-copied, and small files are not skipped. That path is implemented and has not been run on ReFS. A volume that rejects the control code, including NTFS, fails the probe.That functionality is defective; see
https://github.com/git-lfs/git-lfs/issues/6312. We should not implement this on Windows because there's no reasonable way to avoid corruption. It's quite shocking that that bug exists, but it does. Unless we can guarantee a fixed version of Windows (which does not exist yet), we should skip reflinking there.I'd also like to see us limit this to platforms where it _has_ been manually tested or is guaranteed to be automatically tested in CI. That means we should make sure this runs in CI on APFS for macOS (see below).
t/t2400-worktree-add.sh test_expect_success 'relative worktree sets extension config' ' · line 1356
Quoted 7 lines
t/t2400-worktree-add.sh +7
-0+# Reflinks are only available on copy-on-write filesystems (btrfs, XFS, +# bcachefs, APFS, ...). Where they are unavailable, --reflink transparently +# falls back to a regular checkout, which the test above already covers. +test_lazy_prereq REFLINK ' + echo probe >reflink-src && + cp --reflink=always reflink-src reflink-dst 2>/dev/null +'Does this work properly on macOS? That is, does the macOS
cpbinary contain support for--reflink=always? If not, we won't be testing this case properly on macOS.
--
brian m. carlson (they/them)
Toronto, Ontario, CA
v3 cover worktree: copy-on-write creation and ephemeral branches
In reply to Jason Newton via GitGitGadgetWhen many worktrees share one repository -- for example a fleet of agents each needing an isolated checkout -- "git worktree add" is costly at scale. Objects are shared via the common directory, but the working tree is not:
each add rewrites every tracked file, so N worktrees cost N full checkouts of disk and I/O. A branch created only to back one of those worktrees is also left behind when the worktree is removed.
Patch 1 adds "git worktree add --reflink". On a copy-on-write filesystem it populates the new worktree by reflinking the current worktree's files and index, then "git reset --hard" rewrites only the paths that differ from . reflink_file() in copy.c uses FICLONE (Linux), clonefile() (macOS), and ReFS block cloning (FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does). Elsewhere it is probed up front and falls back to a normal checkout. worktree.reflink (true/false/auto) sets the default; --no-reflink overrides.
Uncommitted edits to tracked files in the source do not leak into the new worktree. The copied index is refreshed before reset, and because every reflinked file is a new inode the refresh re-validates by content. Only untracked files are carried over. The option stays opt-in for that reason:
it copies the source's untracked and ignored files, which a plain checkout does not.
For many worktrees at one commit, the series does not share a branch. git-worktree(1) shows a single update-ref transaction that creates a ref per worktree (or a detached HEAD). A shared branch still lets a stale index in one worktree silently revert a commit made in another.
Patch 2 treats a branch that "git worktree add " created itself as owned by that worktree. "git worktree remove" deletes it. Branches named with -b/-B, or that already existed, are left alone. --keep-branch keeps an owned branch.
Changes since v1:
* Dropped the patch that allowed one branch to be checked out in several worktrees. Documented the per-ref / detached-HEAD pattern instead (brian m. carlson, Junio).
* Explained, in the commit message and the manual, why a dirty source worktree does not leak tracked content (brian m. carlson).
* Added the ReFS block-cloning arm (brian m. carlson).
* Noted overlayfs as a supported setup when the lower filesystem supports reflinks (brian m. carlson).
* Kept --reflink opt-in. A whole-tree reflink is not invisible: it carries untracked files, and cloning every file can be slower than checkout when the files are small (Junio; qub1n on the ReFS work).
* Rebased onto 47fdf2208c ("The 7th batch"). reflink_file() now takes a repository argument, matching copy_file().
* Added ephemeral-branch reaping for the auto-named branch that "worktree add " creates.
ReFS measurements (32 GiB Dev Drive, Windows build 26300). The USB disk itself would not format as ReFS, so the volume is a Dev Drive VHD on an NTFS partition of that disk. NTFS rejects the clone ioctl (win32 error 1 there, error 50 on C:) and falls back to a full write.
The clone shares the source clusters and does not flush. Four thousand clones of one 8 MiB file, 31.3 GiB of logical names, were read back unbuffered and SHA-256'd in full: 4000 of 4000 matched the source. The source was then overwritten with a different pattern. A read-only FlushFileBuffers failed with win32 error 5; a writable reopen succeeded. After that flush, 4000 of 4000 clones still hashed to the original contents and none hashed to the new source. While the clones were created the free-space counter fell to about 1 GiB, then returned to 28.38 GiB with the clones still present. The backing VHD did not grow. Deleting the clones freed nothing further.
Jason Newton (4):
worktree: add --reflink for copy-on-write worktree creation
worktree: reap ephemeral branches created by "worktree add"
worktree: disable reflink on Windows
worktree: keep the reflink probe referenced on Windows
Documentation/config/worktree.adoc | 12 ++ Documentation/git-worktree.adoc | 75 ++++++- builtin/branch.c | 28 +++ builtin/worktree.c | 315 ++++++++++++++++++++++++++++- copy.c | 83 ++++++++ copy.h | 14 ++ t/t2400-worktree-add.sh | 166 +++++++++++++++ t/t3200-branch.sh | 2 +- t/t3400-rebase.sh | 2 +- t/t5516-fetch-push.sh | 2 +- 10 files changed, 690 insertions(+), 9 deletions(-)
base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2317%2Fnevion%2Fworktree-reflink-cow-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2317/nevion/worktree-reflink-cow-v3
Pull-Request: https://github.com/git/git/pull/2317
Range-diff vs v2:
1: 83841020f4 ! 1: a163f737af worktree: add --reflink for copy-on-write worktree creation
@@ Commit message
and bcachefs implement, and the one overlayfs forwards when the lower filesystem can clone. This path has been tested on btrfs.
- On macOS it uses clonefile(), the APFS primitive. That path is
- implemented and has not been run here.
+ On macOS it uses clonefile(), the APFS primitive. The t2400
+ REFLINK prerequisite probes that with "cp -c", because BSD cp has no
+ --reflink=always, so the macOS CI jobs exercise this path on APFS.
On Windows it uses ReFS block cloning, FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does. The destination is
@@ t/t2400-worktree-add.sh: test_expect_success 'relative worktree sets extension c
+# Reflinks are only available on copy-on-write filesystems (btrfs, XFS, +# bcachefs, APFS, ...). Where they are unavailable, --reflink transparently +# falls back to a regular checkout, which the test above already covers.
++#
++# GNU cp probes with --reflink=always. macOS cp has no such option; -c ++# is clonefile(2), which is what the APFS CI jobs exercise.
+test_lazy_prereq REFLINK '
+ echo probe >reflink-src &&
-+ cp --reflink=always reflink-src reflink-dst 2>/dev/null
++ if test_have_prereq MACOS
++ then
++ cp -c reflink-src reflink-dst 2>/dev/null
++ else
++ cp --reflink=always reflink-src reflink-dst 2>/dev/null
++ fi
+'
+
+test_expect_success REFLINK '--reflink carries untracked files and reconciles changes' '
2: 6e2acdc5e2 = 2: 042485f0e9 worktree: reap ephemeral branches created by "worktree add" -: ---------- > 3: 91806975a6 worktree: disable reflink on Windows -: ---------- > 4: 876fb541a8 worktree: keep the reflink probe referenced on Windows
Signature · 1 line
gitgitgadget
PATCH v3 1/4 worktree: add --reflink for copy-on-write worktree creation
From: Jason Newton <[email protected]>
Creating many worktrees from the same base -- for example to run a fleet of automated agents in parallel -- is expensive today: every "git worktree add" materializes the entire working tree by writing each tracked file out from the object store. The objects are shared via the common directory, but the working tree is not: N worktrees mean N full checkouts on disk and N times the file I/O.
Add a "--reflink" option that, on a copy-on-write filesystem, populates the new worktree by reflinking the current worktree's files and index instead. The subsequent "git reset --hard" then only rewrites the paths that actually differ between the current worktree and <commit-ish>; everything else (including untracked files such as build outputs) keeps sharing storage with the source until modified.
A new reflink_file() helper in copy.c clones every regular file. There is no size cutoff.
On Linux it uses the FICLONE ioctl. That is the primitive btrfs, XFS and bcachefs implement, and the one overlayfs forwards when the lower filesystem can clone. This path has been tested on btrfs.
On macOS it uses clonefile(), the APFS primitive. The t2400 REFLINK prerequisite probes that with "cp -c", because BSD cp has no --reflink=always, so the macOS CI jobs exercise this path on APFS.
On Windows it uses ReFS block cloning, FSCTL_DUPLICATE_EXTENTS_TO_FILE, as Git LFS does. The destination is sized to the source, extents are cloned in chunks of at most 1GiB, and the tail is rounded up to a cluster (64K, then 4K) and cloned; Windows ignores the range past EOF. The tail is not byte-copied, and small files are not skipped. That path is implemented and has not been run on ReFS. A volume that rejects the control code, including NTFS, fails the probe.
Anywhere else there is no reflink primitive. Support is probed up front by cloning a one-byte file, and where that fails "--reflink" falls back to an ordinary checkout instead of byte-copying the source tree. The directory walk skips the new worktree itself when it lives inside the source one, and preserves symlinks and modes.
Uncommitted changes in the source do not leak into the new worktree. After reflinking the source's files we copy its index and run "update-index --refresh" before "reset --hard": because every reflinked file is a fresh inode, the cached stat never matches, so the refresh is forced to re-validate by content rather than by stat. A tracked path is therefore only left sharing extents when its content equals the indexed object; any path whose content differs from <commit-ish> -- which includes every uncommitted modification in the source -- is rewritten by the reset. The new worktree is thus always a faithful checkout of <commit-ish> for tracked paths, and only untracked files are carried over.
The behavior can be made the default with the worktree.reflink configuration ("true", "false" or "auto", the last suppressing the unsupported-filesystem warning), and turned off per-invocation with --no-reflink. A configured default degrades quietly in modes that cannot reflink (--orphan, --no-checkout) instead of erroring, so enabling it never breaks those commands. The checkout step continues to honor checkout.workers, so parallel checkout composes with --reflink for the paths that do need rewriting.
To check out many worktrees at the same commit safely, give each its own ref (or a detached HEAD) rather than sharing one branch; a git-worktree(1) example shows the one-shot "update-ref" transaction that pairs with --reflink.
--- Documentation/config/worktree.adoc | 10 ++ Documentation/git-worktree.adoc | 49 ++++++- builtin/worktree.c | 227 ++++++++++++++++++++++++++++- copy.c | 190 ++++++++++++++++++++++++ copy.h | 14 ++ t/t2400-worktree-add.sh | 96 ++++++++++++ 6 files changed, 584 insertions(+), 2 deletions(-)
Documentation/config/worktree.adoc +10 -0
@@ -18,3 +18,13 @@ Note that setting `worktree.useRelativePaths` to "`true`" implies enabling the `extensions.relativeWorktrees` config (see linkgit:git-config[1]), thus making it incompatible with older versions of Git. + +`worktree.reflink`:: + Controls whether `git worktree add` populates new worktrees with + copy-on-write (reflink) clones, as if `--reflink` had been given + (see linkgit:git-worktree[1]). May be set to "`true`", "`false`" + (the default), or "`auto`". With "`true`", a filesystem that does + not support reflinks produces a warning before falling back to an + ordinary checkout; with "`auto`", the fallback is silent. An + explicit `--reflink` or `--no-reflink` on the command line + overrides this setting.
Documentation/git-worktree.adoc +48 -1
67 lines not on this page · raw message
builtin/worktree.c +226 -1
287 lines not on this page · raw message
copy.c +190 -0
199 lines not on this page · raw message
copy.h +14 -0
@@ -11,4 +11,18 @@ int copy_file(struct repository *repo,int copy_file_with_time (struct repository *repo,const char *dst,const char *src,int mode); +/* +* Create "dst" as a copy-on-write (reflink) clone of the regular file +* "src", so that the two files share their data blocks until one of +* them is modified. "dst" must not already exist. +* +* This only succeeds on filesystems that support block cloning (e.g. +* btrfs, XFS or bcachefs on Linux, APFS on macOS). When the platform or +* filesystem does not support reflinks, -1 is returned with errno set to +* ENOSYS (or the underlying error). Callers are expected to fall back to +* copy_file() in that case. Returns 0 on success. +*/ +int reflink_file (struct repository *repo, +const char *dst,const char *src,int mode); +#endif /* COPY_H */
t/t2400-worktree-add.sh +96 -0
100 lines not on this page · raw message
Signature · 1 line
gitgitgadget
PATCH v3 2/4 worktree: reap ephemeral branches created by "worktree add"
In reply to Jason Newton via GitGitGadgetFrom: Jason Newton <[email protected]>
When "git worktree add <path>" is given no branch to check out, it auto-creates one named after the path. Such a branch exists only to back the new worktree, yet it outlives it: removing the worktree leaves the branch behind, so a fleet of throwaway worktrees fills refs/heads/ with dead branches that have to be cleaned up by hand.
Treat an auto-created branch as owned by the worktree. Record it in the worktree's administrative directory (worktrees/<id>/owned-branch) at creation time -- the one moment we can know for certain the branch is worktree-private, since we are the one that created it. "git worktree remove" then deletes the owned branch once the worktree is gone and it is no longer checked out.
Ownership is recorded only when "worktree add" itself creates the branch via DWIM auto-naming; a branch named explicitly with -b/-B, or one that already existed, is never owned and never reaped. Deletion is unconditional (even for unmerged commits) because an ephemeral branch is disposable by contract: to keep the work, pass --keep-branch, or anchor the commits with an ordinary branch beforehand -- a normal branch can point at the same commits, after which reaping the ephemeral ref loses nothing. "git branch -m" updates the marker so ownership follows a rename, and the branch stays ephemeral.
The marker is just another per-worktree administrative file, so older versions of Git simply ignore it (they keep the branch on removal, as today).
--- Documentation/git-worktree.adoc | 22 ++++++++-- builtin/branch.c | 28 +++++++++++++ builtin/worktree.c | 74 ++++++++++++++++++++++++++++++++- t/t2400-worktree-add.sh | 55 ++++++++++++++++++++++++ t/t3200-branch.sh | 2 +- t/t3400-rebase.sh | 2 +- t/t5516-fetch-push.sh | 2 +- 7 files changed, 178 insertions(+), 7 deletions(-)
Documentation/git-worktree.adoc +19 -3
@@ -15,7 +15,7 @@ git worktree list [-v | --porcelain [-z]] git worktree lock [--reason <string>] <worktree> git worktree move <worktree> <new-path> git worktree prune [-n] [-v] [--expire <expire>] -git worktree remove [-f] <worktree> +git worktree remove [-f] [--keep-branch] <worktree> git worktree repair [<path>...] git worktree unlock <worktree>
@@ -40,8 +40,11 @@ In its simplest form, `git worktree add <path>` automatically creates a new branch whose name is the final component of _<path>_, which is convenient if you plan to work on a new topic. For instance, `git worktree add ../hotfix` creates new branch `hotfix` and checks it out at -path `../hotfix`. To instead work on an existing branch in a new worktree, -use `git worktree add <path> <branch>`. On the other hand, if you just +path `../hotfix`. Such an automatically-created branch is _ephemeral_: it +is owned by the worktree and is deleted again when the worktree is removed +with `git worktree remove` (see that command and `--keep-branch`). To +instead work on an existing branch in a new worktree, use +`git worktree add <path> <branch>`. On the other hand, if you just plan to make some experimental changes or do testing without disturbing existing development, it is often convenient to create a 'throwaway' worktree not associated with any branch. For instance,
@@ -145,6 +148,14 @@ Remove a worktree. Only clean worktrees (no untracked files and no modification in tracked files) can be removed. Unclean worktrees or ones with submodules can be removed with `--force`. The main worktree cannot be removed. ++ +If the worktree owns an _ephemeral_ branch -- one that `git worktree add` +created itself because no branch was named (the auto-naming case above) -- +that branch is deleted along with the worktree, even if it has unmerged +commits, since it was created solely to back this worktree. To keep it, use +`--keep-branch`, or anchor the commits with an ordinary branch +(`git branch <name>`) beforehand. A branch you named explicitly (`-b`/`-B`) +or one that existed before the worktree is never deleted. `repair [<path>...]`::
@@ -192,6 +203,11 @@ allows the move to proceed; use `--force` twice if the destination is locked. `remove` refuses to remove an unclean worktree unless `--force` is used. To remove a locked worktree, specify `--force` twice. +`--keep-branch`:: + With `remove`, do not delete the worktree's ephemeral (owned) branch; + keep it as an ordinary branch. Has no effect on worktrees that do not + own a branch. + `-b <new-branch>`:: `-B <new-branch>`:: With `add`, create a new branch named _<new-branch>_ starting at
builtin/branch.c +28 -0
@@ -607,6 +607,32 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,#define IS_HEAD 1#define IS_ORPHAN 2 +/* +* When a branch that a worktree owns (its ephemeral branch, recorded in +* worktrees/<id>/owned-branch) is renamed, update the marker so ownership +* follows the new name -- it stays ephemeral. +*/ +static void rename_owned_branch_markers (struct worktree **worktrees, +const char *oldref,const char *newref) +{ +for (int i =0 ; worktrees[i]; i++) { +struct strbuf path =STRBUF_INIT , buf =STRBUF_INIT ; + +if (!worktrees[i]->id) +continue ;/* the main worktree keeps no marker */ +repo_common_path_append (the_repository, &path, +"worktrees/%s/owned-branch" , + worktrees[i]->id); +if (strbuf_read_file (&buf, path.buf,64 ) >0 ) { +strbuf_trim (&buf); +if (!strcmp (buf.buf, oldref)) +write_file (path.buf,"%s" , newref); + } +strbuf_release (&path); +strbuf_release (&buf); + } +} +static void copy_or_rename_branch (const char *oldname,const char *newname,int copy,int force) {struct strbuf oldref =STRBUF_INIT , newref =STRBUF_INIT , logmsg =STRBUF_INIT ;
@@ -675,6 +701,8 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, intif (!copy && !(oldref_usage &IS_ORPHAN ) &&refs_rename_ref (get_main_ref_store (the_repository), oldref.buf, newref.buf, logmsg.buf))die (_ ("branch rename failed" )); +if (!copy && !(oldref_usage &IS_ORPHAN )) +rename_owned_branch_markers (worktrees, oldref.buf, newref.buf);if (copy &&refs_copy_existing_ref (get_main_ref_store (the_repository), oldref.buf, newref.buf, logmsg.buf))die (_ ("branch copy failed" ));
builtin/worktree.c +73 -1
127 lines not on this page · raw message
t/t2400-worktree-add.sh +55 -0
59 lines not on this page · raw message
t/t3200-branch.sh +1 -1
@@ -190,7 +190,7 @@ test_expect_success 'git branch -M foo bar should fail when bar is checked out' test_expect_success 'git branch -M foo bar should fail when bar is checked out in worktree' ' git branch -f bar && - test_when_finished "git worktree remove wt && git branch -D wt" && + test_when_finished "git worktree remove --keep-branch wt && git branch -D wt" && git worktree add wt && test_must_fail git branch -M bar wt '
t/t3400-rebase.sh +1 -1
@@ -471,7 +471,7 @@ test_expect_success 'git rebase --update-ref with core.commentChar and branch on test_when_finished git branch -D base topic2 && test_when_finished git checkout main && test_when_finished git branch -D wt-topic && - test_when_finished git worktree remove wt-topic && + test_when_finished git worktree remove --keep-branch wt-topic && git checkout main && git checkout -b base && git checkout -b topic2 &&
t/t5516-fetch-push.sh +1 -1
@@ -1873,7 +1873,7 @@ test_expect_success 'updateInstead with bare repository worktree and unborn bare ' test_expect_success 'refuse fetch to current branch of worktree' ' - test_when_finished "git worktree remove --force wt && git branch -D wt" && + test_when_finished "git worktree remove --force --keep-branch wt && git branch -D wt" && git worktree add wt && test_commit apple && test_must_fail git fetch . HEAD:wt &&
Signature · 1 line
gitgitgadget
PATCH v3 3/4 worktree: disable reflink on Windows
In reply to Jason Newton via GitGitGadgetFrom: Jason Newton <[email protected]>
FSCTL_DUPLICATE_EXTENTS_TO_FILE can publish zeros when the source data is still cached. git-lfs issue 6312 reports that on Windows 11 build 26200, and a later reproduction hit build 26200.9448. No fixed Windows build is identified, so reflink_file() refuses on every Windows version.
git worktree add --reflink and worktree.reflink=true warn with that build number and fall back to a regular checkout. worktree.reflink=auto stays silent, as it does when the filesystem cannot reflink.
--- Documentation/config/worktree.adoc | 6 +- Documentation/git-worktree.adoc | 12 ++- builtin/worktree.c | 13 +++ copy.c | 133 +++-------------------------- t/t2400-worktree-add.sh | 15 ++++ 5 files changed, 53 insertions(+), 126 deletions(-)
Documentation/config/worktree.adoc +4 -2
@@ -25,6 +25,8 @@ thus making it incompatible with older versions of Git. (see linkgit:git-worktree[1]). May be set to "`true`", "`false`" (the default), or "`auto`". With "`true`", a filesystem that does not support reflinks produces a warning before falling back to an - ordinary checkout; with "`auto`", the fallback is silent. An - explicit `--reflink` or `--no-reflink` on the command line + ordinary checkout; with "`auto`", the fallback is silent. On Windows + the request always falls back. An explicit request warns that block + cloning can zero-fill files on Windows build 26200; "`auto`" stays + silent. An explicit `--reflink` or `--no-reflink` on the command line overrides this setting.
Documentation/git-worktree.adoc +8 -4
@@ -244,10 +244,14 @@ To remove a locked worktree, specify `--force` twice. + Reflinks require a copy-on-write filesystem: for example btrfs, XFS, bcachefs or ZFS on Linux (and overlayfs on top of one of these, as used by -containers), APFS on macOS, or ReFS on Windows. On a filesystem that does -not support reflinks, `--reflink` transparently falls back to an ordinary -checkout. Because the source worktree's untracked and ignored files are -cloned as well, only use `--reflink` when that is acceptable. +containers), or APFS on macOS. Reflink is disabled on Windows. ReFS block +cloning can zero-fill files on Windows build 26200 (git-lfs issue 6312), +and no fixed build is known. On Windows, `--reflink` falls back to an +ordinary checkout and, unless `worktree.reflink` is `auto`, warns with +that build number. On a filesystem that does not support reflinks, +`--reflink` transparently falls back to an ordinary checkout. Because the +source worktree's untracked and ignored files are cloned as well, only use +`--reflink` when that is acceptable. + This option cannot be combined with `--no-checkout` or `--orphan`. It can be enabled by default with the `worktree.reflink` configuration; see
builtin/worktree.c +13 -0
@@ -563,6 +563,18 @@ static int reflink_worktree(const char *path, const char *wt_git_dir,return error (_ ("--reflink needs a source working tree, but this " "repository does not have one" )); +#ifdef GIT_WINDOWS_NATIVE +/* +* reflink_file() refuses on Windows. Build 26200 is the version +* named in git-lfs issue 6312; no later build is known to be fixed. +* Auto mode stays quiet, matching the non-Windows fallback. +*/ +if (!opts->quiet && opts->reflink !=REFLINK_AUTO ) +warning (_ ("reflink is disabled on Windows; block cloning " +"can zero-fill files on Windows build 26200; " +"falling back to a regular checkout" )); +return 1 ; +#else if (!reflink_supported (path)) {/* In auto mode the fallback is expected, so stay quiet. */ if (!opts->quiet && opts->reflink !=REFLINK_AUTO )
@@ -570,6 +582,7 @@ static int reflink_worktree(const char *path, const char *wt_git_dir, "falling back to a regular checkout"), path); return 1; } +#endif if (stat (path, &dst_st))return error_errno (_ ("could not stat '%s'" ), path);
copy.c +13 -120
152 lines not on this page · raw message
t/t2400-worktree-add.sh +15 -0
@@ -1288,6 +1288,21 @@ test_expect_success 'relative worktree sets extension config' ' test_cmp_config -C repo true extensions.relativeworktrees ' +test_expect_success MINGW 'reflink is disabled on Windows build 26200' ' + test_when_finished "rm -rf winreflink" && + git init winreflink && + ( + cd winreflink && + test_commit base file.txt base && + git worktree add --reflink wt -b winbr base 2>err && + test_grep "build 26200" err && + echo base >expect && + test_cmp expect wt/file.txt && + git -C wt status --porcelain >status && + test_must_be_empty status + ) +' + test_expect_success '--reflink produces a correct checkout on any filesystem' ' test_when_finished "rm -rf reflinkrepo" && git init reflinkrepo &&
Signature · 1 line
gitgitgadget
PATCH v3 4/4 worktree: keep the reflink probe referenced on Windows
In reply to Jason Newton via GitGitGadgetFrom: Jason Newton <[email protected]>
The mingw build compiles with -Werror=unused-function. Putting the reflink_supported() call in the non-Windows branch left that function unused, so "win build" failed at builtin/worktree.c:445. Leave the call in the common path. The Windows return still happens first.
--- builtin/worktree.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-)
builtin/worktree.c +3 -2
@@ -568,13 +568,15 @@ static int reflink_worktree(const char *path, const char *wt_git_dir,* reflink_file() refuses on Windows. Build 26200 is the version * named in git-lfs issue 6312; no later build is known to be fixed. * Auto mode stays quiet, matching the non-Windows fallback. +* Return before the probe, but leave the call compiled: an #else +* around it makes reflink_supported() unused under -Werror. */ if (!opts->quiet && opts->reflink !=REFLINK_AUTO )warning (_ ("reflink is disabled on Windows; block cloning " "can zero-fill files on Windows build 26200; " "falling back to a regular checkout" ));return 1 ; -#else +#endif if (!reflink_supported (path)) {/* In auto mode the fallback is expected, so stay quiet. */ if (!opts->quiet && opts->reflink !=REFLINK_AUTO )
@@ -582,7 +584,6 @@ static int reflink_worktree(const char *path, const char *wt_git_dir, "falling back to a regular checkout"), path);return 1 ; } -#endif if (stat (path, &dst_st))return error_errno (_ ("could not stat '%s'" ), path);
Signature · 1 line
gitgitgadget
Re: [PATCH v2 2/2] worktree: reap ephemeral branches created by "worktree add"
In reply to Eric Sunshine--detach and an ephemeral branch differ while the worktree exists.
--detach leaves HEAD with no branch, so status, push, rename, upstream, and symbolic-ref have nothing to name. An ephemeral branch is a normal refs/heads/ ref for the life of the worktree. Commits go on it, and another ref can point at them before "worktree remove" deletes that branch.
Deleting the branch that "worktree add <path>" creates today would change the default, so this has to be opt-in. That opt-in is not --detach: the worktree stays on a branch until removal.
These worktrees might be minutes or they might go on for months; either way they should be easy to use and not require flags to assist
in the majority of common case behaviors, moreover detached feels
ike a degraded halfway state, frequently allowing a gotcha to strike or
a loss of a safety like a gc based reap.
"git worktree remove" does not delete whatever branch a worktree has checked out. It deletes a branch only when this "git worktree add" created it by DWIM (the final component of <path>) and recorded that in worktrees/<id>/owned-branch.
A branch named with -b/-B, a branch that already existed, and a detached HEAD are not owned and are not removed. A worktree created by an older Git has no marker, so removing it still leaves its branch in place.
"Unconditional" in the commit means that this one owned branch is deleted even if it has unmerged commits. --keep-branch turns that off. "git branch <name> <tip>" before removal keeps the commits on an ordinary branch; only the owned ref is deleted.
The call that changes is "git worktree add <path>" with no branch, followed by "git worktree remove". A caller who wants that branch to outlive the worktree already has a spelling this series does not
touch: "git worktree add -b <name> <path>", or "git worktree add <path> <existing>". --detach still creates no branch, so it is not the same state while the worktree exists.
Ownership is written only when the worktree is created, so I'll confirm an older worktree still has no marker after an upgrade. The branch stays. Upgrading does not mark a worktree you already have so no upgrade headaches are expected.
-Jason
Quoted 51 lines · Eric Sunshine
Quoted 23 lines · Jason Newton via GitGitGadget
When "git worktree add <path>" is given no branch to check out, it auto-creates one named after the path. Such a branch exists only to back the new worktree, yet it outlives it: removing the worktree leaves the branch behind, so a fleet of throwaway worktrees fills refs/heads/ with dead branches that have to be cleaned up by hand.
Treat an auto-created branch as owned by the worktree. Record it in the worktree's administrative directory (worktrees/<id>/owned-branch) at creation time -- the one moment we can know for certain the branch is worktree-private, since we are the one that created it. "git worktree remove" then deletes the owned branch once the worktree is gone and it is no longer checked out.
Ownership is recorded only when "worktree add" itself creates the branch via DWIM auto-naming; a branch named explicitly with -b/-B, or one that already existed, is never owned and never reaped. Deletion is unconditional (even for unmerged commits) because an ephemeral branch is disposable by contract: to keep the work, pass --keep-branch, or anchor the commits with an ordinary branch beforehand -- a normal branch can point at the same commits, after which reaping the ephemeral ref loses nothing. "git branch -m" updates the marker so ownership follows a rename, and the branch stays ephemeral.
Quoted 5 lines
The marker is just another per-worktree administrative file, so older versions of Git simply ignore it (they keep the branch on removal, as today).
Signed-off-by Jason Newton <[email protected]>Sorry, I may be missing something obvious, but this message never explains why we should add all this additional complexity for a use-case already directly supported by
git worktree add -d <path>. Specifically, worktree's --detach option is exactly what you want when creating throwaway worktrees, even "fleets" of them (using terminology from this apparently fully or partially LLM-generated message). If your tooling or agent is not using -d/--detach for throwaway worktrees, then the problem is fixable by modifying the tooling or by teaching the agent about --detach, without bulking up the git-worktree implementation unnecessarily.Moreover, a change like this which makes the new behavior unconditional is certainly going to break existing tooling and workflows, thus it will not go over well, even with the --keep-branch escape-hatch. At minimum, to be acceptable, assuming such new functionality is even desirable, to avoid breaking existing setups, it would have to be opt-in, not opt-out (via --keep-branch). But if it's going to be opt-in, then you may as well just use --detach to achieve the same effect.
Re: [PATCH v2 2/2] worktree: reap ephemeral branches created by "worktree add"
On Wed, Oct 21, 2026 at 3:40 AM Jason Newton <[email protected]> wrote:>
Quoted 40 lines
--detach and an ephemeral branch differ while the worktree exists.
--detach leaves HEAD with no branch, so status, push, rename, upstream, and symbolic-ref have nothing to name. An ephemeral branch is a normal refs/heads/ ref for the life of the worktree. Commits go on it, and another ref can point at them before "worktree remove" deletes that branch.
Deleting the branch that "worktree add <path>" creates today would change the default, so this has to be opt-in. That opt-in is not --detach: the worktree stays on a branch until removal.
These worktrees might be minutes or they might go on for months; either way they should be easy to use and not require flags to assist
in the majority of common case behaviors, moreover detached feels
ike a degraded halfway state, frequently allowing a gotcha to strike or
a loss of a safety like a gc based reap."git worktree remove" does not delete whatever branch a worktree has checked out. It deletes a branch only when this "git worktree add" created it by DWIM (the final component of <path>) and recorded that in worktrees/<id>/owned-branch.
A branch named with -b/-B, a branch that already existed, and a detached HEAD are not owned and are not removed. A worktree created by an older Git has no marker, so removing it still leaves its branch in place.
"Unconditional" in the commit means that this one owned branch is deleted even if it has unmerged commits. --keep-branch turns that off. "git branch <name> <tip>" before removal keeps the commits on an ordinary branch; only the owned ref is deleted.
The call that changes is "git worktree add <path>" with no branch, followed by "git worktree remove". A caller who wants that branch to outlive the worktree already has a spelling this series does not
touch: "git worktree add -b <name> <path>", or "git worktree add <path> <existing>". --detach still creates no branch, so it is not the same state while the worktree exists.
Quoted 4 lines
Ownership is written only when the worktree is created, so I'll confirm an older worktree still has no marker after an upgrade. The branch stays. Upgrading does not mark a worktree you already have so no upgrade headaches are expected.
Feeding a reviewer's comments into an agent and pasting its LLM-generated response into an email is not a good way to convince project stakeholders that the submission has value. If you can't explain its value in concrete terms in your own words, then you're going to have a hard time eliciting any responses or additional reviewer feedback.
Re: [PATCH v2 2/2] worktree: reap ephemeral branches created by "worktree add"
Please focus on reviewing, I use mixed but you shouldn't assume which parts are and are not llms.
What you don't realize by working in a silo for a long time is your communication skills to external parties gets reduced with jargon and nitpicking ; I have the llms both assume you're wrong and right while seeing the undesirable behaviors your stances have to the one that I know is correct. You seem overly sensitive to the usage so far. I have no tollerance left to give - so stay on point. You should consider several harnesses chose not to even go through PRs and work around the functionality like grok build did, patching it in externally which is kind of nuts - and replicated. I think it's something you should think about the nature of quite a bit.
As I mentioned, detached IOWs sucks and you shouldn't have bothered mentioning it because it fails hard, though it was noted and evaluated in earnest for a bad idea - want to go back to the long but more neutral form?
-Jason
Quoted 53 lines · Eric Sunshine
On Wed, Oct 21, 2026 at 3:40 AM Jason Newton <[email protected]> wrote:>
Quoted 40 lines
--detach and an ephemeral branch differ while the worktree exists.
--detach leaves HEAD with no branch, so status, push, rename, upstream, and symbolic-ref have nothing to name. An ephemeral branch is a normal refs/heads/ ref for the life of the worktree. Commits go on it, and another ref can point at them before "worktree remove" deletes that branch.
Deleting the branch that "worktree add <path>" creates today would change the default, so this has to be opt-in. That opt-in is not --detach: the worktree stays on a branch until removal.
These worktrees might be minutes or they might go on for months; either way they should be easy to use and not require flags to assist
in the majority of common case behaviors, moreover detached feels
ike a degraded halfway state, frequently allowing a gotcha to strike or
a loss of a safety like a gc based reap."git worktree remove" does not delete whatever branch a worktree has checked out. It deletes a branch only when this "git worktree add" created it by DWIM (the final component of <path>) and recorded that in worktrees/<id>/owned-branch.
A branch named with -b/-B, a branch that already existed, and a detached HEAD are not owned and are not removed. A worktree created by an older Git has no marker, so removing it still leaves its branch in place.
"Unconditional" in the commit means that this one owned branch is deleted even if it has unmerged commits. --keep-branch turns that off. "git branch <name> <tip>" before removal keeps the commits on an ordinary branch; only the owned ref is deleted.
The call that changes is "git worktree add <path>" with no branch, followed by "git worktree remove". A caller who wants that branch to outlive the worktree already has a spelling this series does not
touch: "git worktree add -b <name> <path>", or "git worktree add <path> <existing>". --detach still creates no branch, so it is not the same state while the worktree exists.Quoted 4 lines
Ownership is written only when the worktree is created, so I'll confirm an older worktree still has no marker after an upgrade. The branch stays. Upgrading does not mark a worktree you already have so no upgrade headaches are expected.
Feeding a reviewer's comments into an agent and pasting its LLM-generated response into an email is not a good way to convince project stakeholders that the submission has value. If you can't explain its value in concrete terms in your own words, then you're going to have a hard time eliciting any responses or additional reviewer feedback.
Re: [PATCH v2 2/2] worktree: reap ephemeral branches created by "worktree add"
Quoted 14 lines · Jason Newton
Please focus on reviewing, I use mixed but you shouldn't assume which parts are and are not llms.
What you don't realize by working in a silo for a long time is your communication skills to external parties gets reduced with jargon and nitpicking ; I have the llms both assume you're wrong and right while seeing the undesirable behaviors your stances have to the one that I know is correct. You seem overly sensitive to the usage so far. I have no tollerance left to give - so stay on point. You should consider several harnesses chose not to even go through PRs and work around the functionality like grok build did, patching it in externally which is kind of nuts - and replicated. I think it's something you should think about the nature of quite a bit.
Quoted 4 lines
As I mentioned, detached IOWs sucks and you shouldn't have bothered mentioning it because it fails hard, though it was noted and evaluated in earnest for a bad idea - want to go back to the long but more neutral form?
I'm afraid this messages falls far short of the standard of constructive discussion that is expected on this list. It is perfectly possible to disagree with someone without attacking them or telling them what they should or should not be doing.
It is clear that you do not like the current behavior of "git worktree add", but replying with a generic list of the differences between a branch checkout and a detached HEAD and then attacking the reviewer does not explain what problem you are trying to solve and is not going to convince anyone that this patch is a good idea.
Thanks
Phillip
Quoted 58 lines
-Jason
Quoted 53 lines · Eric Sunshine
On Wed, Oct 21, 2026 at 3:40 AM Jason Newton <[email protected]> wrote:>
Quoted 40 lines
--detach and an ephemeral branch differ while the worktree exists.
--detach leaves HEAD with no branch, so status, push, rename, upstream, and symbolic-ref have nothing to name. An ephemeral branch is a normal refs/heads/ ref for the life of the worktree. Commits go on it, and another ref can point at them before "worktree remove" deletes that branch.
Deleting the branch that "worktree add <path>" creates today would change the default, so this has to be opt-in. That opt-in is not --detach: the worktree stays on a branch until removal.
These worktrees might be minutes or they might go on for months; either way they should be easy to use and not require flags to assist
in the majority of common case behaviors, moreover detached feels
ike a degraded halfway state, frequently allowing a gotcha to strike or
a loss of a safety like a gc based reap."git worktree remove" does not delete whatever branch a worktree has checked out. It deletes a branch only when this "git worktree add" created it by DWIM (the final component of <path>) and recorded that in worktrees/<id>/owned-branch.
A branch named with -b/-B, a branch that already existed, and a detached HEAD are not owned and are not removed. A worktree created by an older Git has no marker, so removing it still leaves its branch in place.
"Unconditional" in the commit means that this one owned branch is deleted even if it has unmerged commits. --keep-branch turns that off. "git branch <name> <tip>" before removal keeps the commits on an ordinary branch; only the owned ref is deleted.
The call that changes is "git worktree add <path>" with no branch, followed by "git worktree remove". A caller who wants that branch to outlive the worktree already has a spelling this series does not
touch: "git worktree add -b <name> <path>", or "git worktree add <path> <existing>". --detach still creates no branch, so it is not the same state while the worktree exists.Quoted 4 lines
Ownership is written only when the worktree is created, so I'll confirm an older worktree still has no marker after an upgrade. The branch stays. Upgrading does not mark a worktree you already have so no upgrade headaches are expected.
Feeding a reviewer's comments into an agent and pasting its LLM-generated response into an email is not a good way to convince project stakeholders that the submission has value. If you can't explain its value in concrete terms in your own words, then you're going to have a hard time eliciting any responses or additional reviewer feedback.
Re: [PATCH v2 2/2] worktree: reap ephemeral branches created by "worktree add"
Just matching energy levels of Eric's messaging, I did not make the first move but I will defend my position using the same energy he brought to the table.
The detached-based workflow does not satisfy user expectations - my framing there is no user would like that that didn't explicitly want detached, which I'm not sure has a use case. At face value it does not seem a worth while suggestion, sometimes it's worth doing a cleaner thing even if you have 60% of a technical solution that can be leveraged to overlapping ends.
I do understand the usecase of short and long term worktrees which behave like branches though, and that's the common user expectation. The current behavior of can't checkout the same branch more than once is also not user expected behavior.
-Jason
Quoted 94 lines · Phillip Wood
Quoted 14 lines · Jason Newton
Please focus on reviewing, I use mixed but you shouldn't assume which parts are and are not llms.
What you don't realize by working in a silo for a long time is your communication skills to external parties gets reduced with jargon and nitpicking ; I have the llms both assume you're wrong and right while seeing the undesirable behaviors your stances have to the one that I know is correct. You seem overly sensitive to the usage so far. I have no tollerance left to give - so stay on point. You should consider several harnesses chose not to even go through PRs and work around the functionality like grok build did, patching it in externally which is kind of nuts - and replicated. I think it's something you should think about the nature of quite a bit.
Quoted 4 lines
As I mentioned, detached IOWs sucks and you shouldn't have bothered mentioning it because it fails hard, though it was noted and evaluated in earnest for a bad idea - want to go back to the long but more neutral form?
I'm afraid this messages falls far short of the standard of constructive discussion that is expected on this list. It is perfectly possible to disagree with someone without attacking them or telling them what they should or should not be doing.
It is clear that you do not like the current behavior of "git worktree add", but replying with a generic list of the differences between a branch checkout and a detached HEAD and then attacking the reviewer does not explain what problem you are trying to solve and is not going to convince anyone that this patch is a good idea.
Thanks
Phillip
Quoted 58 lines
-Jason
Quoted 53 lines · Eric Sunshine
On Wed, Oct 21, 2026 at 3:40 AM Jason Newton <[email protected]> wrote:>
Quoted 40 lines
--detach and an ephemeral branch differ while the worktree exists.
--detach leaves HEAD with no branch, so status, push, rename, upstream, and symbolic-ref have nothing to name. An ephemeral branch is a normal refs/heads/ ref for the life of the worktree. Commits go on it, and another ref can point at them before "worktree remove" deletes that branch.
Deleting the branch that "worktree add <path>" creates today would change the default, so this has to be opt-in. That opt-in is not --detach: the worktree stays on a branch until removal.
These worktrees might be minutes or they might go on for months; either way they should be easy to use and not require flags to assist
in the majority of common case behaviors, moreover detached feels
ike a degraded halfway state, frequently allowing a gotcha to strike or
a loss of a safety like a gc based reap."git worktree remove" does not delete whatever branch a worktree has checked out. It deletes a branch only when this "git worktree add" created it by DWIM (the final component of <path>) and recorded that in worktrees/<id>/owned-branch.
A branch named with -b/-B, a branch that already existed, and a detached HEAD are not owned and are not removed. A worktree created by an older Git has no marker, so removing it still leaves its branch in place.
"Unconditional" in the commit means that this one owned branch is deleted even if it has unmerged commits. --keep-branch turns that off. "git branch <name> <tip>" before removal keeps the commits on an ordinary branch; only the owned ref is deleted.
The call that changes is "git worktree add <path>" with no branch, followed by "git worktree remove". A caller who wants that branch to outlive the worktree already has a spelling this series does not
touch: "git worktree add -b <name> <path>", or "git worktree add <path> <existing>". --detach still creates no branch, so it is not the same state while the worktree exists.Quoted 4 lines
Ownership is written only when the worktree is created, so I'll confirm an older worktree still has no marker after an upgrade. The branch stays. Upgrading does not mark a worktree you already have so no upgrade headaches are expected.
Feeding a reviewer's comments into an agent and pasting its LLM-generated response into an email is not a good way to convince project stakeholders that the submission has value. If you can't explain its value in concrete terms in your own words, then you're going to have a hard time eliciting any responses or additional reviewer feedback.
Re: [PATCH v2 1/2] worktree: add --reflink for copy-on-write worktree creation
In reply to Jason NewtonQuoted 1 line · Jason Newton
Brian,
Hay,
Quoted 9 lines
macOS cp has no --reflink=always. The clone option is -c, which is clonefile(2).
t/t2400-worktree-add.shalready probes that way. The REFLINK prerequisite runs "cp -c" when the MACOS prerequisite is set, and "cp --reflink=always" otherwise. The test that depends on REFLINK is "--reflink carries untracked files and reconciles changes".That is in
042485f0e9cd8cf1765b811ea7334247aec44971. The pull request CI for that commit is the APFS run:
Quoted 7 lines
https://github.com/git/git/pull/2317
https://github.com/git/git/actions/runs/39502233266All four macos-14 jobs succeeded. The runners were macOS 14.8.9 (23J631), image macos-14-arm64 20260831.0302.1. "print test failures" was skipped on each job, which is the step that runs only when a test script fails.
You have a test_lazy_prereq for REFLINK in your test file and it uses cp --reflink=always. If that doesn't work on macOS, then the prerequisite fails, so the test with the REFLINK prerequisite is skipped. That means it doesn't fail, but it also doesn't run, so it isn't testing anything at all.
What I'm saying is that it should actually pass the REFLINK prerequisite and run the test on macOS. It looks like you did do that in v3, which is good.
It also looks like at the very least substantial portions of this email are the output of an LLM. You should read and understand our AI policy[0]. In any event, I'm not going to review or respond further to AI slop, especially since I work on Git entirely in my personal time and my time is currently quite limited.
We consider it important to explain our patches and our thought process well on this list and we have high standards for inclusion—as we should—since many developers around the world depend on us. You're welcome to participate, but you should respond to other humans with your own words and thoughts, since we're giving you the same time and courtesy. If I wanted to know what an LLM thinks about what I have to say, I know where to find one—I wouldn't have asked.[1]
[0] https://git-scm.com/docs/SubmittingPatches#ai
[1] For the avoidance of doubt, these em dashes are my own creation and come from my love of language and the fact that I have an English degree.
Signature · 2 lines
brian m. carlson (they/them) Toronto, Ontario, CA