Skip to content

Commit 7dbd9ca

Browse files
committed
worktree: add --reflink for copy-on-write worktree creation
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. Signed-off-by: Jason Newton <nevion@gmail.com>
1 parent c69baaf commit 7dbd9ca

6 files changed

Lines changed: 431 additions & 2 deletions

File tree

Documentation/config/worktree.adoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,13 @@
1717
Note that setting `worktree.useRelativePaths` to "`true`" implies enabling the
1818
`extensions.relativeWorktrees` config (see linkgit:git-config[1]),
1919
thus making it incompatible with older versions of Git.
20+
21+
`worktree.reflink`::
22+
Controls whether `git worktree add` populates new worktrees with
23+
copy-on-write (reflink) clones, as if `--reflink` had been given
24+
(see linkgit:git-worktree[1]). May be set to "`true`", "`false`"
25+
(the default), or "`auto`". With "`true`", a filesystem that does
26+
not support reflinks produces a warning before falling back to an
27+
ordinary checkout; with "`auto`", the fallback is silent. An
28+
explicit `--reflink` or `--no-reflink` on the command line
29+
overrides this setting.

Documentation/git-worktree.adoc

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ SYNOPSIS
1010
--------
1111
[synopsis]
1212
git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]
13-
[--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]
13+
[--orphan] [--reflink] [(-b | -B) <new-branch>] <path> [<commit-ish>]
1414
git worktree list [-v | --porcelain [-z]]
1515
git worktree lock [--reason <string>] <worktree>
1616
git worktree move <worktree> <new-path>
@@ -213,6 +213,34 @@ To remove a locked worktree, specify `--force` twice.
213213
such as configuring sparse-checkout. See "Sparse checkout"
214214
in linkgit:git-read-tree[1].
215215

216+
`--reflink`::
217+
`--no-reflink`::
218+
Populate the new worktree by creating copy-on-write (reflink)
219+
clones of the current worktree's files and index instead of
220+
writing every file out from the object store. The checkout that
221+
follows then only has to rewrite the paths that actually differ
222+
between the current worktree and _<commit-ish>_; everything else
223+
(including untracked files such as build outputs) keeps sharing
224+
storage with the source worktree until modified. This makes
225+
`add` much faster and far cheaper on disk when creating many
226+
worktrees from the same base. `--no-reflink` forces an ordinary
227+
checkout, overriding the `worktree.reflink` configuration.
228+
+
229+
Reflinks require a copy-on-write filesystem (for example btrfs, XFS,
230+
bcachefs or ZFS on Linux, or APFS on macOS). On filesystems or platforms
231+
that do not support reflinks, `--reflink` transparently falls back to an
232+
ordinary checkout. Because the source worktree's untracked and ignored
233+
files are cloned as well, only use `--reflink` when that is acceptable.
234+
+
235+
This option cannot be combined with `--no-checkout` or `--orphan`. It can
236+
be enabled by default with the `worktree.reflink` configuration; see
237+
linkgit:git-config[1].
238+
+
239+
The checkout that populates a new worktree also honors the
240+
`checkout.workers` configuration (see linkgit:git-config[1]), so setting it
241+
parallelizes the file writes and can further speed up `add`, with or
242+
without `--reflink`.
243+
216244
`--guess-remote`::
217245
`--no-guess-remote`::
218246
With `worktree add <path>`, without _<commit-ish>_, instead

builtin/worktree.c

Lines changed: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
#define BUILTIN_WORKTREE_ADD_USAGE \
3232
N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n" \
33-
" [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]")
33+
" [--orphan] [--reflink] [(-b | -B) <new-branch>] <path> [<commit-ish>]")
3434

3535
#define BUILTIN_WORKTREE_LIST_USAGE \
3636
N_("git worktree list [-v | --porcelain [-z]]")
@@ -47,6 +47,11 @@
4747
#define BUILTIN_WORKTREE_UNLOCK_USAGE \
4848
N_("git worktree unlock <worktree>")
4949

50+
/* values for add_opts.reflink and the worktree.reflink config */
51+
#define REFLINK_OFF 0
52+
#define REFLINK_ON 1 /* warn and fall back if unsupported */
53+
#define REFLINK_AUTO 2 /* silently fall back if unsupported */
54+
5055
#define WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT \
5156
_("No possible source branch, inferring '--orphan'")
5257

@@ -123,13 +128,15 @@ struct add_opts {
123128
int checkout;
124129
int orphan;
125130
int relative_paths;
131+
int reflink;
126132
const char *keep_locked;
127133
};
128134

129135
static int show_only;
130136
static int verbose;
131137
static int guess_remote;
132138
static int use_relative_paths;
139+
static int reflink_config = REFLINK_OFF;
133140
static timestamp_t expire;
134141

135142
static int git_worktree_config(const char *var, const char *value,
@@ -141,6 +148,13 @@ static int git_worktree_config(const char *var, const char *value,
141148
} else if (!strcmp(var, "worktree.userelativepaths")) {
142149
use_relative_paths = git_config_bool(var, value);
143150
return 0;
151+
} else if (!strcmp(var, "worktree.reflink")) {
152+
if (value && !strcmp(value, "auto"))
153+
reflink_config = REFLINK_AUTO;
154+
else
155+
reflink_config = git_config_bool(var, value) ?
156+
REFLINK_ON : REFLINK_OFF;
157+
return 0;
144158
}
145159

146160
return git_default_config(var, value, ctx, cb);
@@ -397,6 +411,182 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir)
397411
free(to_file);
398412
}
399413

414+
/*
415+
* Probe whether the filesystem backing "dir" supports reflinks. We do this
416+
* once up front so that, on filesystems without copy-on-write support (or on
417+
* platforms such as Windows that lack a reflink primitive entirely), we can
418+
* fall back to a normal checkout instead of byte-copying the whole source
419+
* working tree -- which would include untracked files and be slower than the
420+
* checkout we are trying to avoid.
421+
*/
422+
static int reflink_supported(const char *dir)
423+
{
424+
struct strbuf src = STRBUF_INIT, dst = STRBUF_INIT;
425+
int fd, ok = 0;
426+
427+
strbuf_addf(&src, "%s/.git-reflink-probe-src", dir);
428+
strbuf_addf(&dst, "%s/.git-reflink-probe-dst", dir);
429+
430+
fd = open(src.buf, O_WRONLY | O_CREAT | O_TRUNC, 0600);
431+
if (fd >= 0) {
432+
write_in_full(fd, "x", 1);
433+
close(fd);
434+
if (!reflink_file(dst.buf, src.buf, 0600))
435+
ok = 1;
436+
unlink(dst.buf);
437+
}
438+
unlink(src.buf);
439+
440+
strbuf_release(&src);
441+
strbuf_release(&dst);
442+
return ok;
443+
}
444+
445+
/*
446+
* Reflink a single regular file, falling back to a regular copy when the
447+
* clone fails for this particular file (for example across mount points).
448+
*/
449+
static int reflink_or_copy(const char *dst, const char *src, int mode)
450+
{
451+
if (!reflink_file(dst, src, mode))
452+
return 0;
453+
return copy_file(dst, src, mode);
454+
}
455+
456+
/*
457+
* Recursively copy the working-tree directory "src" into "dst" using reflinks
458+
* for regular files. Directory entries that resolve to the destination
459+
* worktree itself (identified by skip_dev/skip_ino, which matters when the new
460+
* worktree lives inside the source one) and the top-level ".git" gitfile are
461+
* skipped.
462+
*/
463+
static int reflink_tree(const char *src, const char *dst,
464+
dev_t skip_dev, ino_t skip_ino, int top)
465+
{
466+
struct strbuf s = STRBUF_INIT, d = STRBUF_INIT;
467+
DIR *dir;
468+
struct dirent *de;
469+
int ret = 0;
470+
471+
dir = opendir(src);
472+
if (!dir)
473+
return error_errno(_("could not open directory '%s'"), src);
474+
475+
while (!ret && (de = readdir(dir))) {
476+
struct stat st;
477+
478+
if (is_dot_or_dotdot(de->d_name))
479+
continue;
480+
if (top && !strcmp(de->d_name, ".git"))
481+
continue;
482+
483+
strbuf_reset(&s);
484+
strbuf_addf(&s, "%s/%s", src, de->d_name);
485+
strbuf_reset(&d);
486+
strbuf_addf(&d, "%s/%s", dst, de->d_name);
487+
488+
if (lstat(s.buf, &st)) {
489+
ret = error_errno(_("could not stat '%s'"), s.buf);
490+
break;
491+
}
492+
493+
if (S_ISDIR(st.st_mode)) {
494+
/* never recurse into the new worktree itself */
495+
if (st.st_dev == skip_dev && st.st_ino == skip_ino)
496+
continue;
497+
if (mkdir(d.buf, st.st_mode & 07777) && errno != EEXIST) {
498+
ret = error_errno(_("could not create directory '%s'"), d.buf);
499+
break;
500+
}
501+
ret = reflink_tree(s.buf, d.buf, skip_dev, skip_ino, 0);
502+
} else if (S_ISLNK(st.st_mode)) {
503+
struct strbuf link = STRBUF_INIT;
504+
505+
if (strbuf_readlink(&link, s.buf, st.st_size))
506+
ret = error_errno(_("could not read symlink '%s'"), s.buf);
507+
else if (symlink(link.buf, d.buf))
508+
ret = error_errno(_("could not create symlink '%s'"), d.buf);
509+
strbuf_release(&link);
510+
} else if (S_ISREG(st.st_mode)) {
511+
if (reflink_or_copy(d.buf, s.buf, st.st_mode))
512+
ret = error_errno(_("could not copy '%s' to '%s'"),
513+
s.buf, d.buf);
514+
}
515+
/* silently skip fifos, sockets and device nodes */
516+
}
517+
518+
closedir(dir);
519+
strbuf_release(&s);
520+
strbuf_release(&d);
521+
return ret;
522+
}
523+
524+
/*
525+
* Populate the new worktree at "path" by reflinking the current worktree's
526+
* files and index. The subsequent "git reset --hard" then only has to rewrite
527+
* the paths that actually differ between the source and <commit-ish>, leaving
528+
* everything else sharing storage with the source. Returns 1 when reflinks are
529+
* unavailable so the caller can fall back to a plain checkout.
530+
*/
531+
static int reflink_worktree(const char *path, const char *wt_git_dir,
532+
const struct add_opts *opts, struct strvec *child_env)
533+
{
534+
const char *src_wt = the_repository->worktree;
535+
char *src_index = NULL, *dst_index = NULL;
536+
struct stat dst_st;
537+
int ret = 0;
538+
539+
if (!src_wt)
540+
return error(_("--reflink needs a source working tree, but this "
541+
"repository does not have one"));
542+
543+
if (!reflink_supported(path)) {
544+
/* In auto mode the fallback is expected, so stay quiet. */
545+
if (!opts->quiet && opts->reflink != REFLINK_AUTO)
546+
warning(_("the filesystem at '%s' does not support reflinks; "
547+
"falling back to a regular checkout"), path);
548+
return 1;
549+
}
550+
551+
if (stat(path, &dst_st))
552+
return error_errno(_("could not stat '%s'"), path);
553+
554+
if ((ret = reflink_tree(src_wt, path, dst_st.st_dev, dst_st.st_ino, 1)))
555+
return ret;
556+
557+
/*
558+
* Clone the source index so the following reset sees the source's
559+
* state and only materializes the differences to <commit-ish>.
560+
*/
561+
src_index = repo_git_path(the_repository, "index");
562+
dst_index = xstrfmt("%s/index", wt_git_dir);
563+
if (!access(src_index, F_OK) &&
564+
reflink_or_copy(dst_index, src_index, 0666)) {
565+
ret = error_errno(_("could not copy index to '%s'"), dst_index);
566+
goto out;
567+
}
568+
569+
/*
570+
* The cloned index still carries the source files' stat information.
571+
* Refresh it against the freshly reflinked files so that "git reset"
572+
* recognizes unchanged paths as up to date and leaves them sharing
573+
* storage instead of rewriting (and thus un-sharing) them.
574+
*/
575+
if (!access(dst_index, F_OK)) {
576+
struct child_process cp = CHILD_PROCESS_INIT;
577+
cp.git_cmd = 1;
578+
strvec_pushl(&cp.args, "update-index", "-q", "--refresh", NULL);
579+
strvec_pushv(&cp.env, child_env->v);
580+
/* a dirty working tree is not an error here */
581+
run_command(&cp);
582+
}
583+
584+
out:
585+
free(src_index);
586+
free(dst_index);
587+
return ret;
588+
}
589+
400590
static int checkout_worktree(const struct add_opts *opts,
401591
struct strvec *child_env)
402592
{
@@ -589,6 +779,20 @@ static int add_worktree(const char *path, const char *refname,
589779
(ret = make_worktree_orphan(refname, opts, &child_env)))
590780
goto done;
591781

782+
/*
783+
* When --reflink is requested and the filesystem supports it, copy the
784+
* current worktree (and its index) into the new one using copy-on-write
785+
* clones. checkout_worktree() then only rewrites the paths that differ
786+
* from <commit-ish>. reflink_worktree() returns 1 when reflinks are not
787+
* available, in which case we just do an ordinary checkout below.
788+
*/
789+
if (opts->checkout && opts->reflink) {
790+
ret = reflink_worktree(path, sb_repo.buf, opts, &child_env);
791+
if (ret < 0)
792+
goto done;
793+
ret = 0;
794+
}
795+
592796
if (opts->checkout &&
593797
(ret = checkout_worktree(opts, &child_env)))
594798
goto done;
@@ -801,6 +1005,7 @@ static int add(int ac, const char **av, const char *prefix,
8011005
const char *lock_reason = NULL;
8021006
int keep_locked = 0;
8031007
int used_new_branch_options;
1008+
int reflink_cli = -1;
8041009
struct option options[] = {
8051010
OPT__FORCE(&opts.force,
8061011
N_("checkout <branch> even if already checked out in other worktree"),
@@ -823,6 +1028,8 @@ static int add(int ac, const char **av, const char *prefix,
8231028
N_("try to match the new branch name with a remote-tracking branch")),
8241029
OPT_BOOL(0, "relative-paths", &opts.relative_paths,
8251030
N_("use relative paths for worktrees")),
1031+
OPT_BOOL(0, "reflink", &reflink_cli,
1032+
N_("populate the worktree using copy-on-write clones when supported")),
8261033
OPT_END()
8271034
};
8281035
int ret;
@@ -842,6 +1049,24 @@ static int add(int ac, const char **av, const char *prefix,
8421049
if (opts.orphan && !opts.checkout)
8431050
die(_("options '%s' and '%s' cannot be used together"),
8441051
"--orphan", "--no-checkout");
1052+
1053+
/*
1054+
* Resolve whether to reflink: an explicit --reflink/--no-reflink on
1055+
* the command line wins, otherwise fall back to the worktree.reflink
1056+
* configuration (which may select the "auto" mode).
1057+
*/
1058+
opts.reflink = (reflink_cli != -1) ? reflink_cli : reflink_config;
1059+
if (opts.reflink && (opts.orphan || !opts.checkout)) {
1060+
/*
1061+
* Reflinking is incompatible with these; only complain when it
1062+
* was explicitly requested, otherwise quietly do a plain
1063+
* checkout so a configured default does not break these modes.
1064+
*/
1065+
if (reflink_cli == REFLINK_ON)
1066+
die(_("options '%s' and '%s' cannot be used together"),
1067+
"--reflink", opts.orphan ? "--orphan" : "--no-checkout");
1068+
opts.reflink = REFLINK_OFF;
1069+
}
8451070
if (opts.orphan && ac == 2)
8461071
die(_("option '%s' and commit-ish cannot be used together"),
8471072
"--orphan");

0 commit comments

Comments
 (0)