From: "Jason Newton via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: "brian m. carlson" <sandals@crustytoothpaste.net>,
Jason Newton <nevion@gmail.com>
Subject: [PATCH v2 0/2] worktree: copy-on-write creation and ephemeral branches
Date: Tue, 20 Oct 2026 16:04:07 +0000 [thread overview]
Message-ID: <pull.2317.v2.git.git.1792512249.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2317.git.git.1780685368.gitgitgadget@gmail.com>
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() 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 <nevion@gmail.com>
@@ 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"
--
gitgitgadget
next prev parent reply other threads:[~2026-10-20 16:04 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-05 18:49 [PATCH 0/2] worktree: copy-on-write creation and shared-branch worktrees Jason Newton via GitGitGadget
2026-06-05 18:49 ` [PATCH 1/2] worktree: add --reflink for copy-on-write worktree creation Jason Newton via GitGitGadget
2026-06-05 18:49 ` [PATCH 2/2] worktree: allow sharing a checked-out branch across worktrees Jason Newton via GitGitGadget
2026-06-05 19:59 ` [PATCH 0/2] worktree: copy-on-write creation and shared-branch worktrees brian m. carlson
2026-06-08 14:36 ` Junio C Hamano
2026-10-20 16:04 ` Jason Newton via GitGitGadget [this message]
2026-10-20 16:04 ` [PATCH v2 1/2] worktree: add --reflink for copy-on-write worktree creation Jason Newton via GitGitGadget
2026-10-20 21:20 ` brian m. carlson
2026-10-20 16:04 ` [PATCH v2 2/2] worktree: reap ephemeral branches created by "worktree add" Jason Newton via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2317.v2.git.git.1792512249.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=git@vger.kernel.org \
--cc=nevion@gmail.com \
--cc=sandals@crustytoothpaste.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.