diff --git a/Documentation/config/worktree.adoc b/Documentation/config/worktree.adoc index a248076ea50bd5..a510c664a45028 100644 --- a/Documentation/config/worktree.adoc +++ b/Documentation/config/worktree.adoc @@ -8,6 +8,17 @@ for the new branch. If no such match can be found, it falls back to creating a new branch from the current `HEAD`. +`worktree.useReflink`:: + When populating a new worktree, try to create working tree + files as lightweight copy-on-write clones (reflinks) of the + corresponding files in the main worktree whenever they are + known to have identical content. This shares disk blocks + between worktrees and avoids reading file contents from the + object database. It requires support from the filesystem + (e.g. Btrfs or XFS on Linux); where unsupported, `git worktree + add` silently falls back to a regular checkout. This currently + only has an effect on Linux. Defaults to "`true`". + `worktree.useRelativePaths`:: Link worktrees using relative paths (when "`true`") or absolute paths (when "`false`"). This is particularly useful for setups diff --git a/Makefile b/Makefile index 1f3f099f5c5705..6c79dd2bc3c372 100644 --- a/Makefile +++ b/Makefile @@ -851,6 +851,7 @@ TEST_BUILTINS_OBJS += test-read-cache.o TEST_BUILTINS_OBJS += test-read-graph.o TEST_BUILTINS_OBJS += test-read-midx.o TEST_BUILTINS_OBJS += test-ref-store.o +TEST_BUILTINS_OBJS += test-reflink.o TEST_BUILTINS_OBJS += test-reftable.o TEST_BUILTINS_OBJS += test-regex.o TEST_BUILTINS_OBJS += test-rot13-filter.o diff --git a/builtin/worktree.c b/builtin/worktree.c index d21c43fde38b5e..ced74bb55260e3 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -458,6 +458,39 @@ static void setup_alternate_ref_dir(struct worktree *wt, const char *wt_git_path strbuf_release(&sb); } +static void push_reflink_source_env(struct strvec *child_env) +{ + struct worktree **worktrees; + int use_reflink = 1; + + repo_config_get_bool(the_repository, "worktree.usereflink", + &use_reflink); + if (!use_reflink) + return; + + worktrees = get_worktrees(); + if (worktrees[0] && !worktrees[0]->is_bare) { + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + cp.dir = worktrees[0]->path; + /* + * Clear any repo-local vars (e.g. GIT_DIR, GIT_WORK_TREE) + * inherited from the calling process so the child rediscovers + * the repository from cp.dir instead of reusing whichever + * worktree we happen to have been invoked from. + */ + strvec_pushv(&cp.env, (const char **)local_repo_env); + strvec_pushl(&cp.args, "update-index", "-q", "--refresh", NULL); + /* non-fatal: racy donor files just won't reflink */ + run_command(&cp); + strvec_pushf(child_env, "%s=%s", + GIT_WORKTREE_REFLINK_SOURCE_ENVIRONMENT, + worktrees[0]->path); + } + free_worktrees(worktrees); +} + static int add_worktree(const char *path, const char *refname, const struct add_opts *opts) { @@ -584,6 +617,8 @@ static int add_worktree(const char *path, const char *refname, strvec_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf); strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path); + if (opts->checkout && !opts->orphan) + push_reflink_source_env(&child_env); if (opts->orphan && (ret = make_worktree_orphan(refname, opts, &child_env))) diff --git a/entry.c b/entry.c index 6b79884e3246d6..45d9c66a1e606f 100644 --- a/entry.c +++ b/entry.c @@ -1,6 +1,8 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" +#include "abspath.h" +#include "convert.h" #include "odb.h" #include "odb/streaming.h" #include "dir.h" @@ -8,11 +10,16 @@ #include "gettext.h" #include "hex.h" #include "name-hash.h" +#include "path.h" +#include "read-cache-ll.h" +#include "repository.h" #include "sparse-index.h" #include "submodule.h" #include "symlinks.h" #include "progress.h" +#include "trace2.h" #include "fsmonitor.h" +#include "wrapper.h" #include "entry.h" #include "parallel-checkout.h" @@ -146,6 +153,145 @@ static int streaming_write_entry(const struct cache_entry *ce, char *path, return result; } +/* + * Copy-on-write population of a new worktree: when "git worktree add" + * points us at the primary worktree via GIT_WORKTREE_REFLINK_SOURCE, + * try to clone (reflink) files from it instead of writing blob + * contents from the object database. The donor's index tells us + * which of its files are known to be unmodified. + * + * Note that entries handled by parallel checkout (checkout.workers > + * 1) do not come through this path and are simply written normally. + */ +struct reflink_donor { + int state; /* -1 = not checked yet, 0 = disabled, 1 = active */ + const char *root; + struct index_state istate; +}; +static struct reflink_donor reflink_donor = { .state = -1 }; + +static int reflink_donor_active(void) +{ + if (reflink_donor.state < 0) { + const char *root; + char *index_path; + + reflink_donor.state = 0; + root = getenv(GIT_WORKTREE_REFLINK_SOURCE_ENVIRONMENT); + if (!root || !is_absolute_path(root)) + return 0; + + index_state_init(&reflink_donor.istate, the_repository); + index_path = repo_common_path(the_repository, "index"); + if (read_index_from(&reflink_donor.istate, index_path, + repo_get_common_dir(the_repository)) > 0) { + reflink_donor.root = xstrdup(root); + reflink_donor.state = 1; + } + free(index_path); + } + return reflink_donor.state; +} + +/* + * The checkout-direction (smudge) CRLF conversion is the identity for + * these actions: crlf_to_worktree() bails out immediately unless the + * action's output_eol() is EOL_CRLF, which is true only for the + * "*_CRLF" actions. For CRLF_BINARY and the input-style actions a + * blob therefore checks out byte-for-byte as its own content, so the + * worktree bytes of a fresh checkout equal the blob bytes. + */ +static int crlf_action_is_identity_smudge(enum convert_crlf_action action) +{ + switch (action) { + case CRLF_BINARY: + case CRLF_TEXT_INPUT: + case CRLF_AUTO_INPUT: + return 1; + default: + return 0; + } +} + +/* + * Try to create "path" as a reflink of the same file in the donor + * worktree. A zero return means the file is in place; any nonzero + * return means the caller must write it the normal way. + */ +static int try_reflink_entry(const struct cache_entry *ce, + const struct conv_attrs *ca, + const char *path) +{ + const struct cache_entry *dce; + struct stat st; + struct strbuf donor_path = STRBUF_INIT; + size_t blob_size; + int ret = -1; + + /* + * Only blobs whose worktree representation is identical to + * their content are eligible; anything that smudges is left + * to the regular code path. Input-style CRLF actions are + * admitted because their smudge is the identity (see + * crlf_action_is_identity_smudge()); the clean direction only + * ever removes CR bytes, so a donor whose on-disk size equals + * the blob size cannot have diverged from it -- the size gate + * below is the backstop that makes byte-divergent donors + * impossible. + */ + if (ca->drv || ca->ident || ca->working_tree_encoding || + !crlf_action_is_identity_smudge(ca->crlf_action)) + return -1; + + if (!reflink_donor_active()) + return -1; + + dce = index_file_exists(&reflink_donor.istate, ce->name, + strlen(ce->name), 0); + if (!dce || ce_skip_worktree(dce) || + dce->ce_mode != ce->ce_mode || !oideq(&dce->oid, &ce->oid)) + return -1; + + strbuf_addf(&donor_path, "%s/%s", reflink_donor.root, ce->name); + + /* + * The donor file must be stat-clean with respect to the + * donor's index; racily clean entries are conservatively + * treated as dirty. For an eligible (conversion-free) entry + * the file size must then also match the blob size, which + * guards against a donor checkout that smudged the file under + * attribute rules we no longer see. + */ + if (lstat(donor_path.buf, &st) || !S_ISREG(st.st_mode)) + goto out; + if (ie_match_stat(&reflink_donor.istate, dce, &st, + CE_MATCH_IGNORE_VALID | CE_MATCH_IGNORE_FSMONITOR | + CE_MATCH_RACY_IS_DIRTY)) + goto out; + if (odb_read_object_info(the_repository->objects, &ce->oid, + &blob_size) < 0 || + (uintmax_t)st.st_size != (uintmax_t)blob_size) + goto out; + + trace2_counter_add(TRACE2_COUNTER_ID_REFLINK_ATTEMPTS, 1); + if (reflink_file(donor_path.buf, path, + (ce->ce_mode & 0100) ? 0777 : 0666)) { + /* + * These errnos mean the filesystem cannot reflink at + * all, so do not keep trying for every file. + */ + if (errno == EOPNOTSUPP || errno == ENOTTY || + errno == EXDEV || errno == ENOSYS) + reflink_donor.state = 0; + goto out; + } + trace2_counter_add(TRACE2_COUNTER_ID_REFLINK_HITS, 1); + ret = 0; +out: + strbuf_release(&donor_path); + return ret; +} + void enable_delayed_checkout(struct checkout *state) { if (!state->delayed_checkout) { @@ -300,7 +446,12 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca clone_checkout_metadata(&meta, &state->meta, &ce->oid); if (ce_mode_s_ifmt == S_IFREG) { - struct stream_filter *filter = get_stream_filter_ca(ca, &ce->oid); + struct stream_filter *filter; + + if (!to_tempfile && !try_reflink_entry(ce, ca, path)) + goto finish; + + filter = get_stream_filter_ca(ca, &ce->oid); if (filter && !streaming_write_entry(ce, path, filter, state, to_tempfile, diff --git a/environment.c b/environment.c index 8f0c1c4f250193..e3ced3711bec0a 100644 --- a/environment.c +++ b/environment.c @@ -101,6 +101,8 @@ const char * const local_repo_env[] = { GIT_DIR_ENVIRONMENT, GIT_WORK_TREE_ENVIRONMENT, GIT_IMPLICIT_WORK_TREE_ENVIRONMENT, + /* internal; set by "git worktree add" for its checkout child */ + GIT_WORKTREE_REFLINK_SOURCE_ENVIRONMENT, GRAFT_ENVIRONMENT, INDEX_ENVIRONMENT, NO_REPLACE_OBJECTS_ENVIRONMENT, diff --git a/environment.h b/environment.h index 8aaedcfea36c47..68776715eabfc8 100644 --- a/environment.h +++ b/environment.h @@ -10,6 +10,12 @@ #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE" #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE" #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX" +/* + * Internal, set by "git worktree add" for the checkout it spawns: + * absolute path of the primary worktree, whose clean files may be + * reflinked instead of being written from the object database. + */ +#define GIT_WORKTREE_REFLINK_SOURCE_ENVIRONMENT "GIT_WORKTREE_REFLINK_SOURCE" #define DEFAULT_GIT_DIR_ENVIRONMENT ".git" #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" #define INDEX_ENVIRONMENT "GIT_INDEX_FILE" diff --git a/t/helper/meson.build b/t/helper/meson.build index 3235f10ab8aae1..af65605593f673 100644 --- a/t/helper/meson.build +++ b/t/helper/meson.build @@ -52,6 +52,7 @@ test_tool_sources = [ 'test-read-graph.c', 'test-read-midx.c', 'test-ref-store.c', + 'test-reflink.c', 'test-reftable.c', 'test-regex.c', 'test-repository.c', diff --git a/t/helper/test-reflink.c b/t/helper/test-reflink.c new file mode 100644 index 00000000000000..dc133e2f741edb --- /dev/null +++ b/t/helper/test-reflink.c @@ -0,0 +1,13 @@ +#include "test-tool.h" +#include "wrapper.h" + +int cmd__reflink(int argc, const char **argv) +{ + if (argc != 3) + usage("test-tool reflink "); + if (reflink_file(argv[1], argv[2], 0666)) { + fprintf(stderr, "reflink failed: %s\n", strerror(errno)); + return 1; + } + return 0; +} diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index b71a22b43bbc9e..8cd7004e9aec36 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -64,6 +64,7 @@ static struct test_cmd cmds[] = { { "read-graph", cmd__read_graph }, { "read-midx", cmd__read_midx }, { "ref-store", cmd__ref_store }, + { "reflink", cmd__reflink }, { "rot13-filter", cmd__rot13_filter }, { "regex", cmd__regex }, { "repository", cmd__repository }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index f2885b33d58aa8..01559591859e32 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -57,6 +57,7 @@ int cmd__read_cache(int argc, const char **argv); int cmd__read_graph(int argc, const char **argv); int cmd__read_midx(int argc, const char **argv); int cmd__ref_store(int argc, const char **argv); +int cmd__reflink(int argc, const char **argv); int cmd__rot13_filter(int argc, const char **argv); int cmd__regex(int argc, const char **argv); int cmd__repository(int argc, const char **argv); diff --git a/t/t2408-worktree-reflink.sh b/t/t2408-worktree-reflink.sh new file mode 100755 index 00000000000000..cc4f643554ff00 --- /dev/null +++ b/t/t2408-worktree-reflink.sh @@ -0,0 +1,313 @@ +#!/bin/sh + +test_description='copy-on-write (reflink) population of new worktrees' + +. ./test-lib.sh + +test_expect_success REFLINK 'test-tool reflink clones file contents' ' + echo content >src && + test-tool reflink src dst && + test_cmp src dst +' + +test_expect_success 'test-tool reflink fails on missing source' ' + test_must_fail test-tool reflink does-not-exist dst2 && + test_path_is_missing dst2 +' + +# Racily-clean donor entries (index written in the same second as the +# file's mtime) are conservatively skipped by the reflink logic, so a +# donor committed "just now" would never be cloned. Age the donor +# files into the past and refresh the index so that the recorded stat +# data is deterministically clean and non-racy. +test_expect_success REFLINK 'files are reflinked from donor during checkout' ' + test_commit file1 && + test-tool chmtime =-60 file1.t && + git update-index -q --refresh && + git worktree add --no-checkout wt1 && + GIT_TRACE2_EVENT="$PWD/trace1.json" \ + GIT_WORKTREE_REFLINK_SOURCE="$PWD" \ + git -C wt1 reset --hard && + test_cmp file1.t wt1/file1.t && + git -C wt1 status --porcelain >out && + test_must_be_empty out && + grep "\"category\":\"reflink\".*\"name\":\"hits\"" trace1.json +' + +test_expect_success REFLINK 'dirty donor file is not reflinked' ' + echo original >file2 && + git add file2 && + git commit -m file2 && + test-tool chmtime =-60 file2 && + git update-index -q --refresh && + echo dirtied >file2 && + git worktree add --no-checkout wt2 && + GIT_WORKTREE_REFLINK_SOURCE="$PWD" git -C wt2 reset --hard && + echo original >expect && + test_cmp expect wt2/file2 && + git checkout -- file2 +' + +test_expect_success REFLINK 'paths with conversion attributes are not reflinked' ' + git init attr-repo && + ( + cd attr-repo && + echo "file.txt text eol=crlf" >.gitattributes && + printf "one\r\ntwo\r\n" >file.txt && + git add . && + git commit -m one && + test-tool chmtime =-60 .gitattributes file.txt && + git update-index -q --refresh && + git worktree add --no-checkout wt && + GIT_TRACE2_EVENT="$PWD/trace.json" \ + GIT_WORKTREE_REFLINK_SOURCE="$PWD" \ + git -C wt reset --hard && + test_cmp file.txt wt/file.txt && + grep "\"category\":\"reflink\".*\"name\":\"hits\".*\"count\":1" trace.json + ) +' + +test_expect_success REFLINK,SYMLINKS 'symlinks are checked out correctly, not reflinked' ' + ln -s file1.t link1 && + git add link1 && + git commit -m link1 && + git worktree add --no-checkout wt3 && + GIT_WORKTREE_REFLINK_SOURCE="$PWD" git -C wt3 reset --hard && + test -h wt3/link1 +' + +test_expect_success REFLINK,POSIXPERM 'executable files keep their mode when reflinked' ' + test_write_lines "#!/bin/sh" "true" >exec.sh && + chmod +x exec.sh && + git add exec.sh && + git commit -m exec && + git worktree add --no-checkout wt4 && + GIT_WORKTREE_REFLINK_SOURCE="$PWD" git -C wt4 reset --hard && + test -x wt4/exec.sh +' + +test_expect_success 'bogus reflink source is harmless' ' + test_commit bogus-base && + git worktree add --no-checkout wt5 && + GIT_WORKTREE_REFLINK_SOURCE=/nonexistent git -C wt5 reset --hard && + test_cmp bogus-base.t wt5/bogus-base.t +' + +test_expect_success REFLINK 'git worktree add reflinks from the primary checkout' ' + test-tool chmtime =-60 file1.t && + GIT_TRACE2_EVENT="$PWD/trace-e2e.json" git worktree add wt-auto && + git -C wt-auto status --porcelain >out && + test_must_be_empty out && + test_cmp file1.t wt-auto/file1.t && + grep "\"category\":\"reflink\".*\"name\":\"hits\"" trace-e2e.json +' + +test_expect_success REFLINK 'worktree.useReflink=false disables reflinking' ' + GIT_TRACE2_EVENT="$PWD/trace-off.json" \ + git -c worktree.usereflink=false worktree add wt-off && + git -C wt-off status --porcelain >out && + test_must_be_empty out && + ! grep "\"category\":\"reflink\"" trace-off.json +' + +test_expect_success REFLINK 'worktree add from a linked worktree uses the primary donor' ' + test-tool chmtime =-60 file1.t && + GIT_TRACE2_EVENT="$PWD/trace-linked.json" \ + git -C wt-auto worktree add "$PWD/wt-from-linked" && + test_cmp file1.t wt-from-linked/file1.t && + grep "\"category\":\"reflink\".*\"name\":\"hits\"" trace-linked.json +' + +test_expect_success 'worktree add succeeds regardless of filesystem support' ' + git worktree add wt-plain && + git -C wt-plain status --porcelain >out && + test_must_be_empty out +' + +test_expect_success '--no-checkout skips donor refresh and reflink env' ' + GIT_TRACE2_EVENT="$PWD/trace-nc.json" \ + git worktree add --no-checkout wt-nc && + ! grep "update-index" trace-nc.json && + ! grep "\"category\":\"reflink\"" trace-nc.json +' + +# "eol=lf" resolves to an input-style CRLF action (CRLF_TEXT_INPUT) whose +# checkout-direction (smudge) conversion is the identity, so an LF blob +# checks out byte-for-byte as its own contents. Such files are eligible +# for reflink. Here both .gitattributes (no attribute matches its own +# name -> CRLF_BINARY) and file.txt (eol=lf -> CRLF_TEXT_INPUT) are +# reflinked, so the donor yields two hits. +test_expect_success REFLINK 'input-style CRLF (eol=lf) files are reflinked' ' + git init eol-lf-repo && + ( + cd eol-lf-repo && + echo "*.txt text eol=lf" >.gitattributes && + printf "one\ntwo\n" >file.txt && + git add . && + git commit -m one && + test-tool chmtime =-60 .gitattributes file.txt && + git update-index -q --refresh && + git worktree add --no-checkout wt && + GIT_TRACE2_EVENT="$PWD/trace.json" \ + GIT_WORKTREE_REFLINK_SOURCE="$PWD" \ + git -C wt reset --hard && + test_cmp file.txt wt/file.txt && + grep "\"category\":\"reflink\".*\"name\":\"hits\".*\"count\":2" trace.json + ) +' + +# Guard the belt-and-braces size gate: the working file holds CRLF bytes, +# but "eol=lf" cleans them to an LF blob, so git considers the file +# content-clean. The index records the on-disk size (10 bytes, CRLF) +# while the blob is 8 bytes (LF), so the donor entry is stat-clean yet its +# on-disk bytes diverge from the blob. The size==blob backstop must +# reject it, and the new worktree must receive the LF blob, never the +# donor's CRLF bytes. Only .gitattributes is reflinked -> exactly one hit. +test_expect_success REFLINK 'eol=lf donor with divergent worktree bytes is not reflinked' ' + git init eol-lf-crlf-repo && + ( + cd eol-lf-crlf-repo && + echo "*.txt text eol=lf" >.gitattributes && + printf "one\r\ntwo\r\n" >file.txt && + git add . && + git commit -m one && + test-tool chmtime =-60 .gitattributes file.txt && + git update-index -q --refresh && + git worktree add --no-checkout wt && + GIT_TRACE2_EVENT="$PWD/trace.json" \ + GIT_WORKTREE_REFLINK_SOURCE="$PWD" \ + git -C wt reset --hard && + printf "one\ntwo\n" >expect && + test_cmp expect wt/file.txt && + grep "\"category\":\"reflink\".*\"name\":\"hits\".*\"count\":1" trace.json + ) +' + +# On a filesystem where FICLONE is not supported at all (e.g. ext4), the +# first reflink attempt fails with EOPNOTSUPP/ENOTTY/ENOSYS/EXDEV and +# reflink_donor.state latches to "disabled" for the rest of the process, +# so no further attempts are made even though more eligible donor files +# remain. This only makes sense on a filesystem where FICLONE truly +# fails, hence the !REFLINK gate: the REFLINK prereq itself proves +# FICLONE works here, which would defeat the point of this test. +test_expect_success !REFLINK 'first FICLONE failure disables further attempts' ' + git init reflink-disable-repo && + ( + cd reflink-disable-repo && + test_commit fileA && + test_commit fileB && + test_commit fileC && + test-tool chmtime =-60 fileA.t fileB.t fileC.t && + git update-index -q --refresh && + git worktree add --no-checkout wt && + GIT_TRACE2_EVENT="$PWD/trace.json" \ + GIT_WORKTREE_REFLINK_SOURCE="$PWD" \ + git -C wt reset --hard && + test_cmp fileA.t wt/fileA.t && + test_cmp fileB.t wt/fileB.t && + test_cmp fileC.t wt/fileC.t && + grep "\"category\":\"reflink\".*\"name\":\"attempts\".*\"count\":1" trace.json && + ! grep "\"category\":\"reflink\".*\"name\":\"hits\"" trace.json + ) +' + +test_expect_success REFLINK 'skip-worktree donor entries are not reflinked' ' + git init skip-worktree-repo && + ( + cd skip-worktree-repo && + test_commit fileA && + test_commit fileB && + test-tool chmtime =-60 fileA.t fileB.t && + git update-index -q --refresh && + git update-index --skip-worktree fileB.t && + git worktree add --no-checkout wt && + GIT_TRACE2_EVENT="$PWD/trace.json" \ + GIT_WORKTREE_REFLINK_SOURCE="$PWD" \ + git -C wt reset --hard && + test_cmp fileA.t wt/fileA.t && + test_cmp fileB.t wt/fileB.t && + grep "\"category\":\"reflink\".*\"name\":\"hits\".*\"count\":1" trace.json + ) +' + +# The donor's entry carries CE_VALID (assume-unchanged), but its on-disk +# content was changed without telling the index. The replacement text is +# deliberately the same size as the original blob: that neutralizes the +# independent size==blob backstop (which would reject a differently-sized +# replacement on its own and mask whatever ie_match_stat() decided), so +# the only thing standing between this stale donor and a bad reflink is +# passing CE_MATCH_IGNORE_VALID to force a real stat compare instead of +# trusting the assume-unchanged bit. +test_expect_success REFLINK 'assume-unchanged donor with stale content is not reflinked' ' + git init assume-unchanged-repo && + ( + cd assume-unchanged-repo && + printf "hello world\n" >file.t && + git add file.t && + git commit -m file && + test-tool chmtime =-60 file.t && + git update-index -q --refresh && + git update-index --assume-unchanged file.t && + printf "HELLO WORLD\n" >file.t && + git worktree add --no-checkout wt && + GIT_TRACE2_EVENT="$PWD/trace.json" \ + GIT_WORKTREE_REFLINK_SOURCE="$PWD" \ + git -C wt reset --hard && + printf "hello world\n" >expect && + test_cmp expect wt/file.t && + ! grep "\"category\":\"reflink\"" trace.json + ) +' + +# "update-index --chmod=+x" only flips the mode bit recorded in the +# index; it does not touch the file on disk. That leaves the donor's +# index entry at 100755 while the committed tree (and the new worktree's +# own index, built fresh from that tree) still has 100644, so the +# ce_mode comparison in try_reflink_entry() must reject the donor before +# ever looking at its on-disk stat data. +test_expect_success REFLINK,POSIXPERM 'donor index mode differing from target tree is not reflinked' ' + git init mode-mismatch-repo && + ( + cd mode-mismatch-repo && + test_write_lines "#!/bin/sh" "true" >file.sh && + git add file.sh && + git commit -m file && + test-tool chmtime =-60 file.sh && + git update-index -q --refresh && + git update-index --chmod=+x file.sh && + git worktree add --no-checkout wt && + GIT_TRACE2_EVENT="$PWD/trace.json" \ + GIT_WORKTREE_REFLINK_SOURCE="$PWD" \ + git -C wt reset --hard && + test_cmp file.sh wt/file.sh && + ! test -x wt/file.sh && + ! grep "\"category\":\"reflink\"" trace.json + ) +' + +# Gitlink entries never reach try_reflink_entry() (it is only consulted +# for S_IFREG blobs), so they are unaffected by a reflink donor; this +# guards against a regression that made checkout mishandle gitlinks +# whenever a donor is active, by comparing against a control worktree +# with reflinking turned off via worktree.usereflink=false. +test_expect_success REFLINK 'gitlink entries are checked out correctly with reflink donor' ' + git init gitlink-repo && + ( + cd gitlink-repo && + git init inner && + test_commit -C inner inner-file && + inner_head=$(git -C inner rev-parse HEAD) && + git update-index --add --cacheinfo 160000,$inner_head,inner && + test_commit regular && + test-tool chmtime =-60 regular.t && + git update-index -q --refresh && + GIT_TRACE2_EVENT="$PWD/trace.json" git worktree add wt && + git -c worktree.usereflink=false worktree add wt-control && + test_cmp regular.t wt/regular.t && + test_path_is_dir wt/inner && + test_path_is_dir wt-control/inner && + grep "\"category\":\"reflink\".*\"name\":\"hits\".*\"count\":1" trace.json + ) +' + +test_done diff --git a/t/test-lib.sh b/t/test-lib.sh index d390c53ec1cfb9..87263a8215a07d 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1804,6 +1804,12 @@ test_lazy_prereq SYMLINKS ' ln -s x y && test -h y ' +test_lazy_prereq REFLINK ' + # test whether the filesystem supports reflinks (FICLONE) + echo reflink >reflink-src && + test-tool reflink reflink-src reflink-dst +' + test_lazy_prereq SYMLINKS_WINDOWS ' # test whether symbolic links are enabled on Windows test_have_prereq MINGW && diff --git a/trace2.h b/trace2.h index e4f23784e4620f..8480d0ea42d407 100644 --- a/trace2.h +++ b/trace2.h @@ -560,6 +560,10 @@ enum trace2_counter_id { TRACE2_COUNTER_ID_FSYNC_WRITEOUT_ONLY, TRACE2_COUNTER_ID_FSYNC_HARDWARE_FLUSH, + /* counts reflink attempts/successes when populating a worktree */ + TRACE2_COUNTER_ID_REFLINK_ATTEMPTS, + TRACE2_COUNTER_ID_REFLINK_HITS, + /* Add additional counter definitions before here. */ TRACE2_NUMBER_OF_COUNTERS }; diff --git a/trace2/tr2_ctr.c b/trace2/tr2_ctr.c index ee17bfa86b401b..cb2f22e4578fd9 100644 --- a/trace2/tr2_ctr.c +++ b/trace2/tr2_ctr.c @@ -47,6 +47,17 @@ static struct tr2_counter_metadata tr2_counter_metadata[TRACE2_NUMBER_OF_COUNTER .want_per_thread_events = 0, }, + [TRACE2_COUNTER_ID_REFLINK_ATTEMPTS] = { + .category = "reflink", + .name = "attempts", + .want_per_thread_events = 0, + }, + [TRACE2_COUNTER_ID_REFLINK_HITS] = { + .category = "reflink", + .name = "hits", + .want_per_thread_events = 0, + }, + /* Add additional metadata before here. */ }; diff --git a/wrapper.c b/wrapper.c index 16f5a63fbb614a..132bc3e3ada6d6 100644 --- a/wrapper.c +++ b/wrapper.c @@ -11,6 +11,11 @@ #include "strbuf.h" #include "trace2.h" +#ifdef __linux__ +#include +#include +#endif + #ifdef HAVE_RTLGENRANDOM /* This is required to get access to RtlGenRandom. */ #define SystemFunction036 NTAPI SystemFunction036 @@ -909,3 +914,41 @@ void *xmmap(void *start, size_t length, die_errno(_("mmap failed%s"), mmap_os_err()); return ret; } + +#if defined(__linux__) && defined(FICLONE) +int reflink_file(const char *src, const char *dst, int mode) +{ + int src_fd, dst_fd, ret, saved_errno; + + src_fd = open(src, O_RDONLY); + if (src_fd < 0) + return -1; + + dst_fd = open(dst, O_WRONLY | O_CREAT | O_EXCL, mode); + if (dst_fd < 0) { + saved_errno = errno; + close(src_fd); + errno = saved_errno; + return -1; + } + + ret = ioctl(dst_fd, FICLONE, src_fd); + saved_errno = errno; + close(src_fd); + if (close(dst_fd) < 0 && !ret) { + ret = -1; + saved_errno = errno; + } + if (ret) + unlink(dst); + errno = saved_errno; + return ret ? -1 : 0; +} +#else +int reflink_file(const char *src UNUSED, const char *dst UNUSED, + int mode UNUSED) +{ + errno = ENOSYS; + return -1; +} +#endif diff --git a/wrapper.h b/wrapper.h index 15ac3bab6e9748..dc0b336b3b2c34 100644 --- a/wrapper.h +++ b/wrapper.h @@ -170,4 +170,13 @@ static inline unsigned log2u(uintmax_t sz) return l - 1; } +/* + * Create "dst" as a lightweight copy-on-write clone (reflink) of + * "src", creating it with the given mode. Returns 0 on success and + * -1 with errno set on failure, in which case no "dst" is left + * behind. Only supported on Linux (FICLONE); elsewhere it always + * fails with ENOSYS. + */ +int reflink_file(const char *src, const char *dst, int mode); + #endif /* WRAPPER_H */