From 8240e9a210c616a82864537691ebc245de87fd32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:55:35 +0000 Subject: [PATCH 01/28] Initial plan Signed-off-by: Tomas Kubes From fa12f591d46df9191d11cb31c4b0d9fc1e4a4976 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:02:49 +0000 Subject: [PATCH 02/28] compat: add a block-cloning primitive ReFS can share file extents through FSCTL_DUPLICATE_EXTENTS_TO_FILE, but callers should not need platform-specific logic. Provide a compatibility primitive that reports unsupported elsewhere, allowing callers to use block cloning as an opportunistic optimization with an ordinary write as fallback. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- compat/mingw.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ compat/mingw.h | 2 ++ git-compat-util.h | 11 +++++++++++ 3 files changed, 57 insertions(+) diff --git a/compat/mingw.c b/compat/mingw.c index fd014eef498e55..693098964a1afc 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -36,6 +36,50 @@ #define HCAST(type, handle) ((type)(intptr_t)handle) +int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) +{ + DUPLICATE_EXTENTS_DATA data = { + .FileHandle = HCAST(HANDLE, _get_osfhandle(src_fd)), + }; + HANDLE dst = HCAST(HANDLE, _get_osfhandle(dst_fd)); + const LONGLONG chunk_size = 1024 * 1024 * 1024; + const LONGLONG cluster_sizes[] = { 64 * 1024, 4 * 1024 }; + DWORD bytes_returned; + size_t i; + + if (ftruncate(dst_fd, size) < 0) + return -1; + if (!size) + return 0; + + while (data.SourceFileOffset.QuadPart + chunk_size < size) { + data.ByteCount.QuadPart = chunk_size; + if (!DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, + &data, sizeof(data), NULL, 0, + &bytes_returned, NULL)) { + errno = ENOSYS; + return -1; + } + data.SourceFileOffset.QuadPart += data.ByteCount.QuadPart; + data.TargetFileOffset = data.SourceFileOffset; + } + + for (i = 0; i < ARRAY_SIZE(cluster_sizes); i++) { + LONGLONG remaining = size - data.SourceFileOffset.QuadPart; + + data.ByteCount.QuadPart = + DIV_ROUND_UP(remaining, cluster_sizes[i]) * + cluster_sizes[i]; + if (DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, + &data, sizeof(data), NULL, 0, + &bytes_returned, NULL)) + return 0; + } + + errno = ENOSYS; + return -1; +} + void open_in_gdb(void) { static struct child_process cp = CHILD_PROCESS_INIT; diff --git a/compat/mingw.h b/compat/mingw.h index 379d7f5d079165..4b4cf030f78f35 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -7,6 +7,8 @@ struct config_context; int mingw_core_config(const char *var, const char *value, const struct config_context *ctx, void *cb); #define platform_core_config mingw_core_config +int mingw_block_clone_file(int dst_fd, int src_fd, off_t size); +#define block_clone_file mingw_block_clone_file #ifndef NO_OPENSSL #include diff --git a/git-compat-util.h b/git-compat-util.h index 10d3eec3c74ba5..8848d148f8dd56 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -223,6 +223,17 @@ static inline int noop_core_config(const char *var UNUSED, #define platform_core_config noop_core_config #endif +#ifndef block_clone_file +static inline int git_block_clone_file(int dst_fd UNUSED, + int src_fd UNUSED, + off_t size UNUSED) +{ + errno = ENOSYS; + return -1; +} +#define block_clone_file git_block_clone_file +#endif + #ifndef has_dos_drive_prefix static inline int git_has_dos_drive_prefix(const char *path UNUSED) { From 5161d810fab8cb2074ae348816014fca283a327a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:10:52 +0000 Subject: [PATCH 03/28] worktree: clone unchanged files on capable volumes Linked worktrees can contain many identical large files even though they already share the object database. On filesystems that advertise block refcounting, pass the current worktree to the checkout and clone only regular, unconverted files whose contents hash to the requested blob. Any unsupported operation or clone failure falls back silently to the normal checkout path. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- builtin/reset.c | 13 ++++++-- builtin/worktree.c | 10 ++++-- compat/mingw.c | 23 ++++++++++++++ compat/mingw.h | 2 ++ entry.c | 69 +++++++++++++++++++++++++++++++++++++++-- entry.h | 1 + environment.h | 2 ++ git-compat-util.h | 9 ++++++ t/t2400-worktree-add.sh | 26 ++++++++++++++++ unpack-trees.c | 1 + unpack-trees.h | 1 + 11 files changed, 150 insertions(+), 7 deletions(-) diff --git a/builtin/reset.c b/builtin/reset.c index 66c26eab62fc12..da1fb160459d69 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -62,7 +62,9 @@ static inline int is_merge(void) return !access(git_path_merge_head(the_repository), F_OK); } -static int reset_index(const char *ref, const struct object_id *oid, int reset_type, int quiet) +static int reset_index(const char *ref, const struct object_id *oid, + int reset_type, int quiet, + const char *block_clone_source) { int i, nr = 0; struct tree_desc desc[2]; @@ -76,6 +78,7 @@ static int reset_index(const char *ref, const struct object_id *oid, int reset_t opts.dst_index = the_repository->index; opts.fn = oneway_merge; opts.merge = 1; + opts.block_clone_source = block_clone_source; init_checkout_metadata(&opts.meta, ref, oid, NULL); if (!quiet) opts.verbose_update = 1; @@ -343,6 +346,8 @@ int cmd_reset(int argc, { int reset_type = NONE, update_ref_status = 0, quiet = 0; int no_refresh = 0; + const char *block_clone_source = + getenv(GIT_WORKTREE_BLOCK_CLONE_SOURCE); int patch_mode = 0, pathspec_file_nul = 0, unborn; const char *rev; char *pathspec_from_file = NULL; @@ -535,9 +540,11 @@ int cmd_reset(int argc, if (ref && !starts_with(ref, "refs/")) FREE_AND_NULL(ref); - err = reset_index(ref, &oid, reset_type, quiet); + err = reset_index(ref, &oid, reset_type, quiet, + block_clone_source); if (reset_type == KEEP && !err) - err = reset_index(ref, &oid, MIXED, quiet); + err = reset_index(ref, &oid, MIXED, quiet, + block_clone_source); if (err) die(_("Could not reset index file to revision '%s'."), rev); free(ref); diff --git a/builtin/worktree.c b/builtin/worktree.c index 77ecd0f71f4247..c740f22d42fbf9 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -395,14 +395,20 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir) } static int checkout_worktree(const struct add_opts *opts, - struct strvec *child_env) + struct strvec *child_env, const char *path) { struct child_process cp = CHILD_PROCESS_INIT; + const char *source = repo_get_work_tree(the_repository); + cp.git_cmd = 1; strvec_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); if (opts->quiet) strvec_push(&cp.args, "--quiet"); strvec_pushv(&cp.env, child_env->v); + if (source && + block_cloning_supported(source, absolute_path(path))) + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_BLOCK_CLONE_SOURCE, source); return run_command(&cp); } @@ -589,7 +595,7 @@ static int add_worktree(const char *path, const char *refname, goto done; if (opts->checkout && - (ret = checkout_worktree(opts, &child_env))) + (ret = checkout_worktree(opts, &child_env, path))) goto done; is_junk = 0; diff --git a/compat/mingw.c b/compat/mingw.c index 693098964a1afc..39c047a132b2ce 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -33,9 +33,32 @@ #include #define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056) +#ifndef FILE_SUPPORTS_BLOCK_REFCOUNTING +#define FILE_SUPPORTS_BLOCK_REFCOUNTING 0x08000000 +#endif #define HCAST(type, handle) ((type)(intptr_t)handle) +int mingw_block_cloning_supported(const char *source, const char *destination) +{ + wchar_t source_path[MAX_LONG_PATH], destination_path[MAX_LONG_PATH]; + wchar_t source_root[MAX_PATH], destination_root[MAX_PATH]; + DWORD flags; + + if (xutftowcs_long_path(source_path, source) < 0 || + xutftowcs_long_path(destination_path, destination) < 0 || + !GetVolumePathNameW(source_path, source_root, + ARRAY_SIZE(source_root)) || + !GetVolumePathNameW(destination_path, destination_root, + ARRAY_SIZE(destination_root)) || + _wcsicmp(source_root, destination_root) || + !GetVolumeInformationW(source_root, NULL, 0, NULL, NULL, &flags, + NULL, 0)) + return 0; + + return !!(flags & FILE_SUPPORTS_BLOCK_REFCOUNTING); +} + int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) { DUPLICATE_EXTENTS_DATA data = { diff --git a/compat/mingw.h b/compat/mingw.h index 4b4cf030f78f35..3190ec7e75433f 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -7,6 +7,8 @@ struct config_context; int mingw_core_config(const char *var, const char *value, const struct config_context *ctx, void *cb); #define platform_core_config mingw_core_config +int mingw_block_cloning_supported(const char *source, const char *destination); +#define block_cloning_supported mingw_block_cloning_supported int mingw_block_clone_file(int dst_fd, int src_fd, off_t size); #define block_clone_file mingw_block_clone_file diff --git a/entry.c b/entry.c index 5fd4b08991c6ed..e9a91619785c72 100644 --- a/entry.c +++ b/entry.c @@ -8,6 +8,7 @@ #include "gettext.h" #include "hex.h" #include "name-hash.h" +#include "object-file.h" #include "sparse-index.h" #include "submodule.h" #include "symlinks.h" @@ -15,6 +16,7 @@ #include "fsmonitor.h" #include "entry.h" #include "parallel-checkout.h" +#include "wrapper.h" static void create_directories(const char *path, int path_len, const struct checkout *state) @@ -128,6 +130,62 @@ static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempf } } +static int clone_entry(const struct cache_entry *ce, char *path, + const struct conv_attrs *ca, + const struct checkout *state, + int *fstat_done, struct stat *statbuf) +{ + struct strbuf source = STRBUF_INIT; + struct object_id oid; + struct stat st; + int src_fd = -1, dst_fd = -1, hash_fd; + int ret = 0; + + if (!state->block_clone_source || + ca->drv || ca->ident || ca->working_tree_encoding || + ca->crlf_action != CRLF_BINARY) + return 0; + + strbuf_addf(&source, "%s/%s", state->block_clone_source, ce->name); + src_fd = open_nofollow(source.buf, O_RDONLY); + if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode)) + goto done; + + hash_fd = dup(src_fd); + if (hash_fd < 0 || + index_fd(state->istate, &oid, hash_fd, &st, OBJ_BLOB, NULL, 0) || + !oideq(&oid, &ce->oid)) + goto done; + + dst_fd = open_output_fd(path, ce, 0); + if (dst_fd < 0) + goto done; + if (block_clone_file(dst_fd, src_fd, st.st_size)) { + close(dst_fd); + dst_fd = -1; + unlink(path); + goto done; + } + if (close(dst_fd)) { + dst_fd = -1; + unlink(path); + goto done; + } + dst_fd = -1; + + if (state->refresh_cache) + *fstat_done = !lstat(path, statbuf); + ret = 1; + +done: + if (dst_fd >= 0) + close(dst_fd); + if (src_fd >= 0) + close(src_fd); + strbuf_release(&source); + return ret; +} + int fstat_checkout_output(int fd, const struct checkout *state, struct stat *st) { /* use fstat() only when path == ce->name */ @@ -313,7 +371,13 @@ 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 && + clone_entry(ce, path, ca, state, &fstat_done, &st)) + goto finish; + + filter = get_stream_filter_ca(ca, &ce->oid); if (filter && !streaming_write_entry(ce, path, filter, state, to_tempfile, @@ -602,7 +666,8 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - if (!enqueue_checkout(ce, ca, nr_checkouts)) + if (!state->block_clone_source && + !enqueue_checkout(ce, ca, nr_checkouts)) return 0; return write_entry(ce, path.buf, ca, state, 0, nr_checkouts); diff --git a/entry.h b/entry.h index ca3ed35bc08654..07a7d39ef16dc2 100644 --- a/entry.h +++ b/entry.h @@ -13,6 +13,7 @@ struct checkout { const char *super_prefix; struct delayed_checkout *delayed_checkout; struct checkout_metadata meta; + const char *block_clone_source; unsigned force:1, quiet:1, not_new:1, diff --git a/environment.h b/environment.h index e7ec5b0437342d..8daab67068e5e8 100644 --- a/environment.h +++ b/environment.h @@ -9,6 +9,8 @@ #define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR" #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE" #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE" +#define GIT_WORKTREE_BLOCK_CLONE_SOURCE \ + "GIT_WORKTREE_BLOCK_CLONE_SOURCE" #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX" #define DEFAULT_GIT_DIR_ENVIRONMENT ".git" #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" diff --git a/git-compat-util.h b/git-compat-util.h index 8848d148f8dd56..c7e083d6d8032b 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -223,6 +223,15 @@ static inline int noop_core_config(const char *var UNUSED, #define platform_core_config noop_core_config #endif +#ifndef block_cloning_supported +static inline int git_block_cloning_supported(const char *source UNUSED, + const char *destination UNUSED) +{ + return 0; +} +#define block_cloning_supported git_block_cloning_supported +#endif + #ifndef block_clone_file static inline int git_block_clone_file(int dst_fd UNUSED, int src_fd UNUSED, diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index bdcca97633bc10..992b4f75c0a58d 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -71,6 +71,32 @@ test_expect_success '"add" worktree' ' ) ' +test_expect_success 'block-clone source does not change checkout contents' ' + test_create_repo clone-source && + test_when_finished \ + "git -C clone-source worktree remove --force ../clone-target" && + ( + cd clone-source && + printf "unchanged\n" >unchanged && + printf "committed\n" >dirty && + printf "converted\n" >converted && + echo "converted text eol=crlf" >.gitattributes && + git add . && + test_tick && + git commit -m files && + git worktree add --detach --no-checkout ../clone-target HEAD && + printf "modified\n" >dirty && + GIT_WORKTREE_BLOCK_CLONE_SOURCE="$PWD" \ + git -C ../clone-target reset --hard + ) && + printf "unchanged\n" >expect && + test_cmp expect clone-target/unchanged && + printf "committed\n" >expect && + test_cmp expect clone-target/dirty && + printf "converted\r\n" >expect && + test_cmp expect clone-target/converted +' + test_expect_success '"add" worktree with lock' ' git worktree add --detach --lock here-with-lock main && test_when_finished "git worktree unlock here-with-lock || :" && diff --git a/unpack-trees.c b/unpack-trees.c index 7958e1f25848cc..27fd72d9584610 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -437,6 +437,7 @@ static int check_updates(struct unpack_trees_options *o, state.quiet = 1; state.refresh_cache = 1; state.istate = index; + state.block_clone_source = o->block_clone_source; clone_checkout_metadata(&state.meta, &o->meta, NULL); if (!o->update || o->dry_run) { diff --git a/unpack-trees.h b/unpack-trees.h index 5867e26e177774..f9e194791d79d6 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -74,6 +74,7 @@ struct unpack_trees_options { enum unpack_trees_reset_type reset; const char *prefix; const char *super_prefix; + const char *block_clone_source; struct pathspec *pathspec; merge_fn_t fn; From f9c8abded6e172b73efbb94db5c77af8ee354aa7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:13:52 +0000 Subject: [PATCH 04/28] fixup! compat: add a block-cloning primitive Document the alignment and chunk-size requirements, and restore the exact destination length after cloning the rounded final range. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- compat/mingw.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/compat/mingw.c b/compat/mingw.c index 39c047a132b2ce..17ec32c4480869 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -65,6 +65,10 @@ int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) .FileHandle = HCAST(HANDLE, _get_osfhandle(src_fd)), }; HANDLE dst = HCAST(HANDLE, _get_osfhandle(dst_fd)); + /* + * Keep requests below ReFS's 4 GiB limit. ReFS volumes use either + * 64 KiB or 4 KiB clusters, so try both alignments for the tail. + */ const LONGLONG chunk_size = 1024 * 1024 * 1024; const LONGLONG cluster_sizes[] = { 64 * 1024, 4 * 1024 }; DWORD bytes_returned; @@ -96,7 +100,7 @@ int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) if (DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &data, sizeof(data), NULL, 0, &bytes_returned, NULL)) - return 0; + return ftruncate(dst_fd, size); } errno = ENOSYS; From 251825a21360de8f6251ed5d25f7cf009ff8c6e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:17:05 +0000 Subject: [PATCH 05/28] fixup! compat: add a block-cloning primitive Explain why the destination is sized both before and after cloning the aligned extent range. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- compat/mingw.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compat/mingw.c b/compat/mingw.c index 17ec32c4480869..370374c9f0de90 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -74,6 +74,7 @@ int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) DWORD bytes_returned; size_t i; + /* The destination range must exist before extents can be cloned. */ if (ftruncate(dst_fd, size) < 0) return -1; if (!size) @@ -99,8 +100,10 @@ int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) cluster_sizes[i]; if (DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &data, sizeof(data), NULL, 0, - &bytes_returned, NULL)) + &bytes_returned, NULL)) { + /* Discard any allocation-unit padding in the tail. */ return ftruncate(dst_fd, size); + } } errno = ENOSYS; From dfc3a3ede6f95075c6a0f72071ee268c342b5177 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:17:05 +0000 Subject: [PATCH 06/28] fixup! worktree: clone unchanged files on capable volumes Keep converted and non-regular entries eligible for parallel checkout because they cannot use the block-clone path. Document the helper fallback contract and source-path lifetime. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- entry.c | 20 ++++++++++++++++---- unpack-trees.h | 1 + 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/entry.c b/entry.c index e9a91619785c72..96984ee01db39c 100644 --- a/entry.c +++ b/entry.c @@ -130,6 +130,20 @@ static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempf } } +static int is_block_clone_candidate(const struct cache_entry *ce, + const struct conv_attrs *ca, + const struct checkout *state) +{ + return state->block_clone_source && S_ISREG(ce->ce_mode) && + ca && !ca->drv && !ca->ident && !ca->working_tree_encoding && + ca->crlf_action == CRLF_BINARY; +} + +/* + * Return 1 after cloning the verified source file. Return 0 without leaving + * the destination behind when cloning is inapplicable or fails, so the caller + * can use the normal checkout path. + */ static int clone_entry(const struct cache_entry *ce, char *path, const struct conv_attrs *ca, const struct checkout *state, @@ -141,9 +155,7 @@ static int clone_entry(const struct cache_entry *ce, char *path, int src_fd = -1, dst_fd = -1, hash_fd; int ret = 0; - if (!state->block_clone_source || - ca->drv || ca->ident || ca->working_tree_encoding || - ca->crlf_action != CRLF_BINARY) + if (!is_block_clone_candidate(ce, ca, state)) return 0; strbuf_addf(&source, "%s/%s", state->block_clone_source, ce->name); @@ -666,7 +678,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - if (!state->block_clone_source && + if (!is_block_clone_candidate(ce, ca, state) && !enqueue_checkout(ce, ca, nr_checkouts)) return 0; diff --git a/unpack-trees.h b/unpack-trees.h index f9e194791d79d6..94d9a39b23c87c 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -74,6 +74,7 @@ struct unpack_trees_options { enum unpack_trees_reset_type reset; const char *prefix; const char *super_prefix; + /* Borrowed source worktree path, valid for this unpack operation. */ const char *block_clone_source; struct pathspec *pathspec; merge_fn_t fn; From 55c7a595e5b50eb8a67175418753a6d6c7a9642c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 15:53:29 +0000 Subject: [PATCH 07/28] amend! compat: add a block-cloning primitive compat: add copy-on-write file support Linked worktrees can share the storage backing identical files on file systems that provide copy-on-write copies. Callers should be able to request that optimization without knowing the platform-specific mechanism or treating lack of support as an error. Provide file_copy_on_write() on Windows and a minimal ENOSYS fallback elsewhere. ReFS requires aligned ranges smaller than 4 GiB, so use 4 GiB minus 64 KiB for full ranges and issue one rounded request for the tail, trying the two valid ReFS cluster sizes. These are alternative tail alignments, not 64 KiB-sized chunks. Truncating afterward removes only the alignment padding. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- compat/mingw.c | 45 +++++++++++---------------------------------- compat/mingw.h | 7 +++---- git-compat-util.h | 20 ++++---------------- 3 files changed, 18 insertions(+), 54 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index 370374c9f0de90..e37f74e5d0f4df 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -33,44 +33,21 @@ #include #define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056) -#ifndef FILE_SUPPORTS_BLOCK_REFCOUNTING -#define FILE_SUPPORTS_BLOCK_REFCOUNTING 0x08000000 -#endif - #define HCAST(type, handle) ((type)(intptr_t)handle) -int mingw_block_cloning_supported(const char *source, const char *destination) -{ - wchar_t source_path[MAX_LONG_PATH], destination_path[MAX_LONG_PATH]; - wchar_t source_root[MAX_PATH], destination_root[MAX_PATH]; - DWORD flags; - - if (xutftowcs_long_path(source_path, source) < 0 || - xutftowcs_long_path(destination_path, destination) < 0 || - !GetVolumePathNameW(source_path, source_root, - ARRAY_SIZE(source_root)) || - !GetVolumePathNameW(destination_path, destination_root, - ARRAY_SIZE(destination_root)) || - _wcsicmp(source_root, destination_root) || - !GetVolumeInformationW(source_root, NULL, 0, NULL, NULL, &flags, - NULL, 0)) - return 0; - - return !!(flags & FILE_SUPPORTS_BLOCK_REFCOUNTING); -} - -int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) +int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size) { DUPLICATE_EXTENTS_DATA data = { .FileHandle = HCAST(HANDLE, _get_osfhandle(src_fd)), }; HANDLE dst = HCAST(HANDLE, _get_osfhandle(dst_fd)); /* - * Keep requests below ReFS's 4 GiB limit. ReFS volumes use either - * 64 KiB or 4 KiB clusters, so try both alignments for the tail. + * FSCTL_DUPLICATE_EXTENTS_TO_FILE accepts ranges smaller than 4 GiB, + * aligned to the volume's cluster size. ReFS uses 64 KiB or 4 KiB + * clusters, so this is the largest valid range for either. */ - const LONGLONG chunk_size = 1024 * 1024 * 1024; - const LONGLONG cluster_sizes[] = { 64 * 1024, 4 * 1024 }; + const LONGLONG max_range = (4LL * 1024 * 1024 * 1024) - (64 * 1024); + const LONGLONG tail_alignments[] = { 64 * 1024, 4 * 1024 }; DWORD bytes_returned; size_t i; @@ -80,8 +57,8 @@ int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) if (!size) return 0; - while (data.SourceFileOffset.QuadPart + chunk_size < size) { - data.ByteCount.QuadPart = chunk_size; + while (data.SourceFileOffset.QuadPart + max_range < size) { + data.ByteCount.QuadPart = max_range; if (!DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &data, sizeof(data), NULL, 0, &bytes_returned, NULL)) { @@ -92,12 +69,12 @@ int mingw_block_clone_file(int dst_fd, int src_fd, off_t size) data.TargetFileOffset = data.SourceFileOffset; } - for (i = 0; i < ARRAY_SIZE(cluster_sizes); i++) { + for (i = 0; i < ARRAY_SIZE(tail_alignments); i++) { LONGLONG remaining = size - data.SourceFileOffset.QuadPart; data.ByteCount.QuadPart = - DIV_ROUND_UP(remaining, cluster_sizes[i]) * - cluster_sizes[i]; + DIV_ROUND_UP(remaining, tail_alignments[i]) * + tail_alignments[i]; if (DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &data, sizeof(data), NULL, 0, &bytes_returned, NULL)) { diff --git a/compat/mingw.h b/compat/mingw.h index 3190ec7e75433f..69560b2e1d6e4a 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -7,10 +7,9 @@ struct config_context; int mingw_core_config(const char *var, const char *value, const struct config_context *ctx, void *cb); #define platform_core_config mingw_core_config -int mingw_block_cloning_supported(const char *source, const char *destination); -#define block_cloning_supported mingw_block_cloning_supported -int mingw_block_clone_file(int dst_fd, int src_fd, off_t size); -#define block_clone_file mingw_block_clone_file +#define copy_on_write_supported 1 +int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size); +#define file_copy_on_write mingw_file_copy_on_write #ifndef NO_OPENSSL #include diff --git a/git-compat-util.h b/git-compat-util.h index c7e083d6d8032b..58fc0bbdeb91b7 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -223,24 +223,12 @@ static inline int noop_core_config(const char *var UNUSED, #define platform_core_config noop_core_config #endif -#ifndef block_cloning_supported -static inline int git_block_cloning_supported(const char *source UNUSED, - const char *destination UNUSED) -{ - return 0; -} -#define block_cloning_supported git_block_cloning_supported +#ifndef copy_on_write_supported +#define copy_on_write_supported 0 #endif -#ifndef block_clone_file -static inline int git_block_clone_file(int dst_fd UNUSED, - int src_fd UNUSED, - off_t size UNUSED) -{ - errno = ENOSYS; - return -1; -} -#define block_clone_file git_block_clone_file +#ifndef file_copy_on_write +#define file_copy_on_write(dst_fd, src_fd, size) (errno = ENOSYS, -1) #endif #ifndef has_dos_drive_prefix From 8ddd5b9636b7179a2079cc03117c17411ff2a70b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 15:53:35 +0000 Subject: [PATCH 08/28] amend! compat: add a block-cloning primitive Co-authored-by: dscho <127790+dscho@users.noreply.github.com> Signed-off-by: Tomas Kubes --- Documentation/config/worktree.adoc | 5 ++ Makefile | 1 + builtin/reset.c | 53 +++++++++++++++++--- builtin/worktree.c | 30 ++++++++--- entry.c | 80 ++++++++++++++++++------------ entry.h | 8 ++- environment.h | 6 ++- t/helper/test-copy-on-write.c | 28 +++++++++++ t/helper/test-tool.c | 1 + t/helper/test-tool.h | 1 + t/t2400-worktree-add.sh | 60 +++++++++++++--------- unpack-trees.c | 2 +- unpack-trees.h | 4 +- 13 files changed, 204 insertions(+), 75 deletions(-) create mode 100644 t/helper/test-copy-on-write.c diff --git a/Documentation/config/worktree.adoc b/Documentation/config/worktree.adoc index 0930183b91cb8f..cd7cf186dea91c 100644 --- a/Documentation/config/worktree.adoc +++ b/Documentation/config/worktree.adoc @@ -9,6 +9,11 @@ fails. If no such match can be found, it falls back to creating a new branch from the current `HEAD`. +`worktree.copyOnWrite`:: + When creating a linked worktree, use copy-on-write file copies when + supported by the platform and file system. Setting this to `false` + disables the optimization. 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 c98be9155c54ae..31c529ffd970b0 100644 --- a/Makefile +++ b/Makefile @@ -819,6 +819,7 @@ TEST_BUILTINS_OBJS += test-bundle-uri.o TEST_BUILTINS_OBJS += test-cache-tree.o TEST_BUILTINS_OBJS += test-chmtime.o TEST_BUILTINS_OBJS += test-config.o +TEST_BUILTINS_OBJS += test-copy-on-write.o TEST_BUILTINS_OBJS += test-crontab.o TEST_BUILTINS_OBJS += test-csprng.o TEST_BUILTINS_OBJS += test-date.o diff --git a/builtin/reset.c b/builtin/reset.c index da1fb160459d69..25c1c4bf761fe5 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -23,6 +23,7 @@ #include "refs.h" #include "diff.h" #include "diffcore.h" +#include "entry.h" #include "tree.h" #include "branch.h" #include "object-name.h" @@ -62,9 +63,8 @@ static inline int is_merge(void) return !access(git_path_merge_head(the_repository), F_OK); } -static int reset_index(const char *ref, const struct object_id *oid, - int reset_type, int quiet, - const char *block_clone_source) +static int reset_index(const char *ref, const struct object_id *oid, int reset_type, int quiet, + struct checkout_copy_source *copy_source) { int i, nr = 0; struct tree_desc desc[2]; @@ -78,7 +78,7 @@ static int reset_index(const char *ref, const struct object_id *oid, opts.dst_index = the_repository->index; opts.fn = oneway_merge; opts.merge = 1; - opts.block_clone_source = block_clone_source; + opts.copy_source = copy_source; init_checkout_metadata(&opts.meta, ref, oid, NULL); if (!quiet) opts.verbose_update = 1; @@ -339,6 +339,37 @@ static int git_reset_config(const char *var, const char *value, return git_default_config(var, value, ctx, cb); } +static int init_copy_source(struct checkout_copy_source *source, + struct index_state *source_index) +{ + const char *worktree = getenv(GIT_WORKTREE_COPY_SOURCE); + const char *index_file = getenv(GIT_WORKTREE_COPY_SOURCE_INDEX); + const char *git_dir = getenv(GIT_WORKTREE_COPY_SOURCE_GIT_DIR); + const char *time_string = getenv(GIT_WORKTREE_COPY_SOURCE_TIME); + char *end; + int enabled = 1; + uintmax_t refreshed_at; + + if (!copy_on_write_supported || + (!repo_config_get_bool(the_repository, "worktree.copyonwrite", + &enabled) && !enabled) || + !worktree || !index_file || !git_dir || !time_string) + return 0; + + errno = 0; + refreshed_at = strtoumax(time_string, &end, 10); + if (errno || *end) + return 0; + if (read_index_from(source_index, index_file, git_dir) < 0) + return 0; + ensure_full_index(source_index); + + source->istate = source_index; + source->worktree = worktree; + source->refreshed_at = refreshed_at; + return 1; +} + int cmd_reset(int argc, const char **argv, const char *prefix, @@ -346,8 +377,10 @@ int cmd_reset(int argc, { int reset_type = NONE, update_ref_status = 0, quiet = 0; int no_refresh = 0; - const char *block_clone_source = - getenv(GIT_WORKTREE_BLOCK_CLONE_SOURCE); + struct index_state copy_source_index = + INDEX_STATE_INIT(the_repository); + struct checkout_copy_source copy_source = { 0 }; + struct checkout_copy_source *copy_source_ptr = NULL; int patch_mode = 0, pathspec_file_nul = 0, unborn; const char *rev; char *pathspec_from_file = NULL; @@ -500,6 +533,9 @@ int cmd_reset(int argc, if (repo_read_index(the_repository) < 0) die(_("index file corrupt")); + if (reset_type == HARD && + init_copy_source(©_source, ©_source_index)) + copy_source_ptr = ©_source; /* Soft reset does not touch the index file nor the working tree * at all, but requires them in a good order. Other resets reset @@ -541,10 +577,10 @@ int cmd_reset(int argc, FREE_AND_NULL(ref); err = reset_index(ref, &oid, reset_type, quiet, - block_clone_source); + copy_source_ptr); if (reset_type == KEEP && !err) err = reset_index(ref, &oid, MIXED, quiet, - block_clone_source); + copy_source_ptr); if (err) die(_("Could not reset index file to revision '%s'."), rev); free(ref); @@ -568,6 +604,7 @@ int cmd_reset(int argc, discard_index(the_repository->index); cleanup: + release_index(©_source_index); clear_pathspec(&pathspec); free(pathspec_from_file); return update_ref_status; diff --git a/builtin/worktree.c b/builtin/worktree.c index c740f22d42fbf9..b5b08f40648a72 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -395,20 +395,38 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir) } static int checkout_worktree(const struct add_opts *opts, - struct strvec *child_env, const char *path) + struct strvec *child_env) { struct child_process cp = CHILD_PROCESS_INIT; const char *source = repo_get_work_tree(the_repository); + struct timeval refreshed_at; cp.git_cmd = 1; strvec_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); if (opts->quiet) strvec_push(&cp.args, "--quiet"); strvec_pushv(&cp.env, child_env->v); - if (source && - block_cloning_supported(source, absolute_path(path))) - strvec_pushf(&cp.env, "%s=%s", - GIT_WORKTREE_BLOCK_CLONE_SOURCE, source); + if (source && repo_read_index(the_repository) >= 0) { + gettimeofday(&refreshed_at, NULL); + if (repo_refresh_and_write_index(the_repository, + REFRESH_QUIET | REFRESH_REALLY, + SKIP_IF_UNCHANGED, 1, + NULL, NULL, NULL) >= 0) { + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE, source); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE_INDEX, + repo_get_index_file(the_repository)); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE_GIT_DIR, + repo_get_git_dir(the_repository)); + strvec_pushf(&cp.env, "%s=%"PRIuMAX, + GIT_WORKTREE_COPY_SOURCE_TIME, + (uintmax_t)refreshed_at.tv_sec * + 1000000000 + + refreshed_at.tv_usec * 1000); + } + } return run_command(&cp); } @@ -595,7 +613,7 @@ static int add_worktree(const char *path, const char *refname, goto done; if (opts->checkout && - (ret = checkout_worktree(opts, &child_env, path))) + (ret = checkout_worktree(opts, &child_env))) goto done; is_junk = 0; diff --git a/entry.c b/entry.c index 96984ee01db39c..61b2250617e7c3 100644 --- a/entry.c +++ b/entry.c @@ -8,7 +8,6 @@ #include "gettext.h" #include "hex.h" #include "name-hash.h" -#include "object-file.h" #include "sparse-index.h" #include "submodule.h" #include "symlinks.h" @@ -16,6 +15,7 @@ #include "fsmonitor.h" #include "entry.h" #include "parallel-checkout.h" +#include "trace2.h" #include "wrapper.h" static void create_directories(const char *path, int path_len, @@ -130,49 +130,65 @@ static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempf } } -static int is_block_clone_candidate(const struct cache_entry *ce, - const struct conv_attrs *ca, - const struct checkout *state) +static struct cache_entry *copy_source_entry(const struct cache_entry *ce, + const struct checkout *state) { - return state->block_clone_source && S_ISREG(ce->ce_mode) && - ca && !ca->drv && !ca->ident && !ca->working_tree_encoding && - ca->crlf_action == CRLF_BINARY; + struct index_state *istate; + struct cache_entry *source; + int pos; + + if (!state->copy_source || !S_ISREG(ce->ce_mode)) + return NULL; + + istate = state->copy_source->istate; + pos = index_name_pos(istate, ce->name, ce_namelen(ce)); + if (pos < 0) + return NULL; + source = istate->cache[pos]; + if (!S_ISREG(source->ce_mode) || ce_skip_worktree(source) || + !oideq(&source->oid, &ce->oid)) + return NULL; + + return source; } -/* - * Return 1 after cloning the verified source file. Return 0 without leaving - * the destination behind when cloning is inapplicable or fails, so the caller - * can use the normal checkout path. - */ -static int clone_entry(const struct cache_entry *ce, char *path, - const struct conv_attrs *ca, - const struct checkout *state, - int *fstat_done, struct stat *statbuf) +static int source_is_uptodate(const struct checkout_copy_source *source, + const struct cache_entry *ce, struct stat *st) +{ + uint64_t mtime; + + if (match_stat_data(&ce->ce_stat_data, st)) + return 0; + + mtime = (uint64_t)st->st_mtime * 1000000000 + ST_MTIME_NSEC(*st); + return mtime < source->refreshed_at; +} + +static int try_copy_on_write(const struct cache_entry *ce, char *path, + const struct checkout *state, + int *fstat_done, struct stat *statbuf) { struct strbuf source = STRBUF_INIT; - struct object_id oid; - struct stat st; - int src_fd = -1, dst_fd = -1, hash_fd; + struct cache_entry *source_ce = copy_source_entry(ce, state); + struct stat st, st_after; + int src_fd = -1, dst_fd = -1; int ret = 0; - if (!is_block_clone_candidate(ce, ca, state)) + if (!source_ce) return 0; - strbuf_addf(&source, "%s/%s", state->block_clone_source, ce->name); + strbuf_addf(&source, "%s/%s", state->copy_source->worktree, ce->name); src_fd = open_nofollow(source.buf, O_RDONLY); - if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode)) - goto done; - - hash_fd = dup(src_fd); - if (hash_fd < 0 || - index_fd(state->istate, &oid, hash_fd, &st, OBJ_BLOB, NULL, 0) || - !oideq(&oid, &ce->oid)) + if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode) || + !source_is_uptodate(state->copy_source, source_ce, &st)) goto done; dst_fd = open_output_fd(path, ce, 0); if (dst_fd < 0) goto done; - if (block_clone_file(dst_fd, src_fd, st.st_size)) { + if (file_copy_on_write(dst_fd, src_fd, st.st_size) || + fstat(src_fd, &st_after) || + !source_is_uptodate(state->copy_source, source_ce, &st_after)) { close(dst_fd); dst_fd = -1; unlink(path); @@ -187,6 +203,8 @@ static int clone_entry(const struct cache_entry *ce, char *path, if (state->refresh_cache) *fstat_done = !lstat(path, statbuf); + trace2_data_string("checkout", the_repository, + "copy_on_write", ce->name); ret = 1; done: @@ -386,7 +404,7 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca struct stream_filter *filter; if (!to_tempfile && - clone_entry(ce, path, ca, state, &fstat_done, &st)) + try_copy_on_write(ce, path, state, &fstat_done, &st)) goto finish; filter = get_stream_filter_ca(ca, &ce->oid); @@ -678,7 +696,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - if (!is_block_clone_candidate(ce, ca, state) && + if (!copy_source_entry(ce, state) && !enqueue_checkout(ce, ca, nr_checkouts)) return 0; diff --git a/entry.h b/entry.h index 07a7d39ef16dc2..c24c353f5ba22c 100644 --- a/entry.h +++ b/entry.h @@ -6,6 +6,12 @@ struct cache_entry; struct index_state; +struct checkout_copy_source { + struct index_state *istate; + const char *worktree; + uint64_t refreshed_at; +}; + struct checkout { struct index_state *istate; const char *base_dir; @@ -13,7 +19,7 @@ struct checkout { const char *super_prefix; struct delayed_checkout *delayed_checkout; struct checkout_metadata meta; - const char *block_clone_source; + struct checkout_copy_source *copy_source; unsigned force:1, quiet:1, not_new:1, diff --git a/environment.h b/environment.h index 8daab67068e5e8..33d301903c8305 100644 --- a/environment.h +++ b/environment.h @@ -9,8 +9,10 @@ #define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR" #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE" #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE" -#define GIT_WORKTREE_BLOCK_CLONE_SOURCE \ - "GIT_WORKTREE_BLOCK_CLONE_SOURCE" +#define GIT_WORKTREE_COPY_SOURCE "GIT_WORKTREE_COPY_SOURCE" +#define GIT_WORKTREE_COPY_SOURCE_INDEX "GIT_WORKTREE_COPY_SOURCE_INDEX" +#define GIT_WORKTREE_COPY_SOURCE_GIT_DIR "GIT_WORKTREE_COPY_SOURCE_GIT_DIR" +#define GIT_WORKTREE_COPY_SOURCE_TIME "GIT_WORKTREE_COPY_SOURCE_TIME" #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX" #define DEFAULT_GIT_DIR_ENVIRONMENT ".git" #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" diff --git a/t/helper/test-copy-on-write.c b/t/helper/test-copy-on-write.c new file mode 100644 index 00000000000000..fe740cf9ebf90a --- /dev/null +++ b/t/helper/test-copy-on-write.c @@ -0,0 +1,28 @@ +#include "test-tool.h" + +int cmd__copy_on_write(int argc, const char **argv) +{ + struct stat st; + int src_fd, dst_fd; + int ret; + + if (argc != 3) + return 1; + + src_fd = open(argv[1], O_RDONLY); + if (src_fd < 0 || fstat(src_fd, &st)) + return 1; + dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_EXCL, 0666); + if (dst_fd < 0) { + close(src_fd); + return 1; + } + + ret = file_copy_on_write(dst_fd, src_fd, st.st_size); + if (close(dst_fd)) + ret = -1; + close(src_fd); + if (ret) + unlink(argv[2]); + return !!ret; +} diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index ee16b2cb23719e..81407f95dbbe77 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -17,6 +17,7 @@ static struct test_cmd cmds[] = { { "cache-tree", cmd__cache_tree }, { "chmtime", cmd__chmtime }, { "config", cmd__config }, + { "copy-on-write", cmd__copy_on_write }, { "crontab", cmd__crontab }, { "csprng", cmd__csprng }, { "date", cmd__date }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index 4cf9f935a4cdfa..4bebafc12c619b 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -10,6 +10,7 @@ int cmd__bundle_uri(int argc, const char **argv); int cmd__cache_tree(int argc, const char **argv); int cmd__chmtime(int argc, const char **argv); int cmd__config(int argc, const char **argv); +int cmd__copy_on_write(int argc, const char **argv); int cmd__crontab(int argc, const char **argv); int cmd__csprng(int argc, const char **argv); int cmd__date(int argc, const char **argv); diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 992b4f75c0a58d..6131724a50850d 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -10,6 +10,13 @@ TEST_CREATE_REPO_NO_TEMPLATE=1 . "$TEST_DIRECTORY"/lib-rebase.sh +test_lazy_prereq COPY_ON_WRITE ' + test-tool genzeros 65536 >copy-source && + test-tool copy-on-write copy-source copy-target && + test_cmp copy-source copy-target && + rm copy-source copy-target +' + test_expect_success 'setup' ' test_commit init ' @@ -71,30 +78,35 @@ test_expect_success '"add" worktree' ' ) ' -test_expect_success 'block-clone source does not change checkout contents' ' - test_create_repo clone-source && - test_when_finished \ - "git -C clone-source worktree remove --force ../clone-target" && - ( - cd clone-source && - printf "unchanged\n" >unchanged && - printf "committed\n" >dirty && - printf "converted\n" >converted && - echo "converted text eol=crlf" >.gitattributes && - git add . && - test_tick && - git commit -m files && - git worktree add --detach --no-checkout ../clone-target HEAD && - printf "modified\n" >dirty && - GIT_WORKTREE_BLOCK_CLONE_SOURCE="$PWD" \ - git -C ../clone-target reset --hard - ) && - printf "unchanged\n" >expect && - test_cmp expect clone-target/unchanged && - printf "committed\n" >expect && - test_cmp expect clone-target/dirty && - printf "converted\r\n" >expect && - test_cmp expect clone-target/converted +test_expect_success COPY_ON_WRITE 'worktree add uses copy-on-write' ' + old_head=$(git rev-parse HEAD) && + test_when_finished "git reset --hard $old_head" && + test_when_finished "git worktree remove --force copy-on-write" && + echo "*.crlf text eol=crlf" >.gitattributes && + echo clean >clean && + echo clean >clean.crlf && + echo clean >modified.crlf && + git add .gitattributes clean clean.crlf modified.crlf && + git commit -m "copy-on-write test files" && + rm clean.crlf modified.crlf && + git checkout -- clean.crlf modified.crlf && + echo modified >init.t && + echo modified >modified.crlf && + GIT_TRACE2_EVENT="$PWD/trace" \ + git worktree add --detach copy-on-write main && + test_grep "\"key\":\"copy_on_write\",\"value\":\"clean\"" trace && + test_grep "\"key\":\"copy_on_write\",\"value\":\"clean.crlf\"" trace && + test_must_fail test_grep "\"value\":\"init.t\"" trace && + test_must_fail test_grep "\"value\":\"modified.crlf\"" trace && + git -C copy-on-write diff --exit-code +' + +test_expect_success COPY_ON_WRITE 'worktree.copyOnWrite disables optimization' ' + test_when_finished "git worktree remove --force no-copy-on-write" && + test_config worktree.copyOnWrite false && + GIT_TRACE2_EVENT="$PWD/disabled-trace" \ + git worktree add --detach no-copy-on-write main && + test_must_fail test_grep "\"key\":\"copy_on_write\"" disabled-trace ' test_expect_success '"add" worktree with lock' ' diff --git a/unpack-trees.c b/unpack-trees.c index 27fd72d9584610..cbde1070fe1686 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -437,7 +437,7 @@ static int check_updates(struct unpack_trees_options *o, state.quiet = 1; state.refresh_cache = 1; state.istate = index; - state.block_clone_source = o->block_clone_source; + state.copy_source = o->copy_source; clone_checkout_metadata(&state.meta, &o->meta, NULL); if (!o->update || o->dry_run) { diff --git a/unpack-trees.h b/unpack-trees.h index 94d9a39b23c87c..c57346755ac182 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -10,6 +10,7 @@ #define MAX_UNPACK_TREES 8 struct cache_entry; +struct checkout_copy_source; struct unpack_trees_options; struct pattern_list; @@ -74,8 +75,7 @@ struct unpack_trees_options { enum unpack_trees_reset_type reset; const char *prefix; const char *super_prefix; - /* Borrowed source worktree path, valid for this unpack operation. */ - const char *block_clone_source; + struct checkout_copy_source *copy_source; struct pathspec *pathspec; merge_fn_t fn; From 80913d57feaae2621655e6fe8d2e5110585c3a2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:02:31 +0000 Subject: [PATCH 09/28] amend! compat: add a block-cloning primitive compat: add copy-on-write file support Linked worktrees can share the storage backing identical files on file systems that provide copy-on-write copies. Callers should be able to request that optimization without knowing the platform-specific mechanism or treating lack of support as an error. Provide file_copy_on_write() on Windows and a minimal ENOSYS fallback elsewhere. ReFS requires aligned ranges smaller than 4 GiB, so use 4 GiB minus 64 KiB for full ranges and issue one rounded request for the tail, trying the two valid ReFS cluster sizes. These are alternative tail alignments, not 64 KiB-sized chunks. Truncating afterward removes only the alignment padding. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> Signed-off-by: GitHub --- Documentation/config/worktree.adoc | 5 -- Makefile | 1 - builtin/reset.c | 53 +++----------------- builtin/worktree.c | 30 +++-------- entry.c | 80 ++++++++++++------------------ entry.h | 8 +-- environment.h | 6 +-- t/helper/test-copy-on-write.c | 28 ----------- t/helper/test-tool.c | 1 - t/helper/test-tool.h | 1 - t/t2400-worktree-add.sh | 60 +++++++++------------- unpack-trees.c | 2 +- unpack-trees.h | 4 +- 13 files changed, 75 insertions(+), 204 deletions(-) delete mode 100644 t/helper/test-copy-on-write.c diff --git a/Documentation/config/worktree.adoc b/Documentation/config/worktree.adoc index cd7cf186dea91c..0930183b91cb8f 100644 --- a/Documentation/config/worktree.adoc +++ b/Documentation/config/worktree.adoc @@ -9,11 +9,6 @@ fails. If no such match can be found, it falls back to creating a new branch from the current `HEAD`. -`worktree.copyOnWrite`:: - When creating a linked worktree, use copy-on-write file copies when - supported by the platform and file system. Setting this to `false` - disables the optimization. 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 31c529ffd970b0..c98be9155c54ae 100644 --- a/Makefile +++ b/Makefile @@ -819,7 +819,6 @@ TEST_BUILTINS_OBJS += test-bundle-uri.o TEST_BUILTINS_OBJS += test-cache-tree.o TEST_BUILTINS_OBJS += test-chmtime.o TEST_BUILTINS_OBJS += test-config.o -TEST_BUILTINS_OBJS += test-copy-on-write.o TEST_BUILTINS_OBJS += test-crontab.o TEST_BUILTINS_OBJS += test-csprng.o TEST_BUILTINS_OBJS += test-date.o diff --git a/builtin/reset.c b/builtin/reset.c index 25c1c4bf761fe5..da1fb160459d69 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -23,7 +23,6 @@ #include "refs.h" #include "diff.h" #include "diffcore.h" -#include "entry.h" #include "tree.h" #include "branch.h" #include "object-name.h" @@ -63,8 +62,9 @@ static inline int is_merge(void) return !access(git_path_merge_head(the_repository), F_OK); } -static int reset_index(const char *ref, const struct object_id *oid, int reset_type, int quiet, - struct checkout_copy_source *copy_source) +static int reset_index(const char *ref, const struct object_id *oid, + int reset_type, int quiet, + const char *block_clone_source) { int i, nr = 0; struct tree_desc desc[2]; @@ -78,7 +78,7 @@ static int reset_index(const char *ref, const struct object_id *oid, int reset_t opts.dst_index = the_repository->index; opts.fn = oneway_merge; opts.merge = 1; - opts.copy_source = copy_source; + opts.block_clone_source = block_clone_source; init_checkout_metadata(&opts.meta, ref, oid, NULL); if (!quiet) opts.verbose_update = 1; @@ -339,37 +339,6 @@ static int git_reset_config(const char *var, const char *value, return git_default_config(var, value, ctx, cb); } -static int init_copy_source(struct checkout_copy_source *source, - struct index_state *source_index) -{ - const char *worktree = getenv(GIT_WORKTREE_COPY_SOURCE); - const char *index_file = getenv(GIT_WORKTREE_COPY_SOURCE_INDEX); - const char *git_dir = getenv(GIT_WORKTREE_COPY_SOURCE_GIT_DIR); - const char *time_string = getenv(GIT_WORKTREE_COPY_SOURCE_TIME); - char *end; - int enabled = 1; - uintmax_t refreshed_at; - - if (!copy_on_write_supported || - (!repo_config_get_bool(the_repository, "worktree.copyonwrite", - &enabled) && !enabled) || - !worktree || !index_file || !git_dir || !time_string) - return 0; - - errno = 0; - refreshed_at = strtoumax(time_string, &end, 10); - if (errno || *end) - return 0; - if (read_index_from(source_index, index_file, git_dir) < 0) - return 0; - ensure_full_index(source_index); - - source->istate = source_index; - source->worktree = worktree; - source->refreshed_at = refreshed_at; - return 1; -} - int cmd_reset(int argc, const char **argv, const char *prefix, @@ -377,10 +346,8 @@ int cmd_reset(int argc, { int reset_type = NONE, update_ref_status = 0, quiet = 0; int no_refresh = 0; - struct index_state copy_source_index = - INDEX_STATE_INIT(the_repository); - struct checkout_copy_source copy_source = { 0 }; - struct checkout_copy_source *copy_source_ptr = NULL; + const char *block_clone_source = + getenv(GIT_WORKTREE_BLOCK_CLONE_SOURCE); int patch_mode = 0, pathspec_file_nul = 0, unborn; const char *rev; char *pathspec_from_file = NULL; @@ -533,9 +500,6 @@ int cmd_reset(int argc, if (repo_read_index(the_repository) < 0) die(_("index file corrupt")); - if (reset_type == HARD && - init_copy_source(©_source, ©_source_index)) - copy_source_ptr = ©_source; /* Soft reset does not touch the index file nor the working tree * at all, but requires them in a good order. Other resets reset @@ -577,10 +541,10 @@ int cmd_reset(int argc, FREE_AND_NULL(ref); err = reset_index(ref, &oid, reset_type, quiet, - copy_source_ptr); + block_clone_source); if (reset_type == KEEP && !err) err = reset_index(ref, &oid, MIXED, quiet, - copy_source_ptr); + block_clone_source); if (err) die(_("Could not reset index file to revision '%s'."), rev); free(ref); @@ -604,7 +568,6 @@ int cmd_reset(int argc, discard_index(the_repository->index); cleanup: - release_index(©_source_index); clear_pathspec(&pathspec); free(pathspec_from_file); return update_ref_status; diff --git a/builtin/worktree.c b/builtin/worktree.c index b5b08f40648a72..c740f22d42fbf9 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -395,38 +395,20 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir) } static int checkout_worktree(const struct add_opts *opts, - struct strvec *child_env) + struct strvec *child_env, const char *path) { struct child_process cp = CHILD_PROCESS_INIT; const char *source = repo_get_work_tree(the_repository); - struct timeval refreshed_at; cp.git_cmd = 1; strvec_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); if (opts->quiet) strvec_push(&cp.args, "--quiet"); strvec_pushv(&cp.env, child_env->v); - if (source && repo_read_index(the_repository) >= 0) { - gettimeofday(&refreshed_at, NULL); - if (repo_refresh_and_write_index(the_repository, - REFRESH_QUIET | REFRESH_REALLY, - SKIP_IF_UNCHANGED, 1, - NULL, NULL, NULL) >= 0) { - strvec_pushf(&cp.env, "%s=%s", - GIT_WORKTREE_COPY_SOURCE, source); - strvec_pushf(&cp.env, "%s=%s", - GIT_WORKTREE_COPY_SOURCE_INDEX, - repo_get_index_file(the_repository)); - strvec_pushf(&cp.env, "%s=%s", - GIT_WORKTREE_COPY_SOURCE_GIT_DIR, - repo_get_git_dir(the_repository)); - strvec_pushf(&cp.env, "%s=%"PRIuMAX, - GIT_WORKTREE_COPY_SOURCE_TIME, - (uintmax_t)refreshed_at.tv_sec * - 1000000000 + - refreshed_at.tv_usec * 1000); - } - } + if (source && + block_cloning_supported(source, absolute_path(path))) + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_BLOCK_CLONE_SOURCE, source); return run_command(&cp); } @@ -613,7 +595,7 @@ static int add_worktree(const char *path, const char *refname, goto done; if (opts->checkout && - (ret = checkout_worktree(opts, &child_env))) + (ret = checkout_worktree(opts, &child_env, path))) goto done; is_junk = 0; diff --git a/entry.c b/entry.c index 61b2250617e7c3..96984ee01db39c 100644 --- a/entry.c +++ b/entry.c @@ -8,6 +8,7 @@ #include "gettext.h" #include "hex.h" #include "name-hash.h" +#include "object-file.h" #include "sparse-index.h" #include "submodule.h" #include "symlinks.h" @@ -15,7 +16,6 @@ #include "fsmonitor.h" #include "entry.h" #include "parallel-checkout.h" -#include "trace2.h" #include "wrapper.h" static void create_directories(const char *path, int path_len, @@ -130,65 +130,49 @@ static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempf } } -static struct cache_entry *copy_source_entry(const struct cache_entry *ce, - const struct checkout *state) +static int is_block_clone_candidate(const struct cache_entry *ce, + const struct conv_attrs *ca, + const struct checkout *state) { - struct index_state *istate; - struct cache_entry *source; - int pos; - - if (!state->copy_source || !S_ISREG(ce->ce_mode)) - return NULL; - - istate = state->copy_source->istate; - pos = index_name_pos(istate, ce->name, ce_namelen(ce)); - if (pos < 0) - return NULL; - source = istate->cache[pos]; - if (!S_ISREG(source->ce_mode) || ce_skip_worktree(source) || - !oideq(&source->oid, &ce->oid)) - return NULL; - - return source; + return state->block_clone_source && S_ISREG(ce->ce_mode) && + ca && !ca->drv && !ca->ident && !ca->working_tree_encoding && + ca->crlf_action == CRLF_BINARY; } -static int source_is_uptodate(const struct checkout_copy_source *source, - const struct cache_entry *ce, struct stat *st) -{ - uint64_t mtime; - - if (match_stat_data(&ce->ce_stat_data, st)) - return 0; - - mtime = (uint64_t)st->st_mtime * 1000000000 + ST_MTIME_NSEC(*st); - return mtime < source->refreshed_at; -} - -static int try_copy_on_write(const struct cache_entry *ce, char *path, - const struct checkout *state, - int *fstat_done, struct stat *statbuf) +/* + * Return 1 after cloning the verified source file. Return 0 without leaving + * the destination behind when cloning is inapplicable or fails, so the caller + * can use the normal checkout path. + */ +static int clone_entry(const struct cache_entry *ce, char *path, + const struct conv_attrs *ca, + const struct checkout *state, + int *fstat_done, struct stat *statbuf) { struct strbuf source = STRBUF_INIT; - struct cache_entry *source_ce = copy_source_entry(ce, state); - struct stat st, st_after; - int src_fd = -1, dst_fd = -1; + struct object_id oid; + struct stat st; + int src_fd = -1, dst_fd = -1, hash_fd; int ret = 0; - if (!source_ce) + if (!is_block_clone_candidate(ce, ca, state)) return 0; - strbuf_addf(&source, "%s/%s", state->copy_source->worktree, ce->name); + strbuf_addf(&source, "%s/%s", state->block_clone_source, ce->name); src_fd = open_nofollow(source.buf, O_RDONLY); - if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode) || - !source_is_uptodate(state->copy_source, source_ce, &st)) + if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode)) + goto done; + + hash_fd = dup(src_fd); + if (hash_fd < 0 || + index_fd(state->istate, &oid, hash_fd, &st, OBJ_BLOB, NULL, 0) || + !oideq(&oid, &ce->oid)) goto done; dst_fd = open_output_fd(path, ce, 0); if (dst_fd < 0) goto done; - if (file_copy_on_write(dst_fd, src_fd, st.st_size) || - fstat(src_fd, &st_after) || - !source_is_uptodate(state->copy_source, source_ce, &st_after)) { + if (block_clone_file(dst_fd, src_fd, st.st_size)) { close(dst_fd); dst_fd = -1; unlink(path); @@ -203,8 +187,6 @@ static int try_copy_on_write(const struct cache_entry *ce, char *path, if (state->refresh_cache) *fstat_done = !lstat(path, statbuf); - trace2_data_string("checkout", the_repository, - "copy_on_write", ce->name); ret = 1; done: @@ -404,7 +386,7 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca struct stream_filter *filter; if (!to_tempfile && - try_copy_on_write(ce, path, state, &fstat_done, &st)) + clone_entry(ce, path, ca, state, &fstat_done, &st)) goto finish; filter = get_stream_filter_ca(ca, &ce->oid); @@ -696,7 +678,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - if (!copy_source_entry(ce, state) && + if (!is_block_clone_candidate(ce, ca, state) && !enqueue_checkout(ce, ca, nr_checkouts)) return 0; diff --git a/entry.h b/entry.h index c24c353f5ba22c..07a7d39ef16dc2 100644 --- a/entry.h +++ b/entry.h @@ -6,12 +6,6 @@ struct cache_entry; struct index_state; -struct checkout_copy_source { - struct index_state *istate; - const char *worktree; - uint64_t refreshed_at; -}; - struct checkout { struct index_state *istate; const char *base_dir; @@ -19,7 +13,7 @@ struct checkout { const char *super_prefix; struct delayed_checkout *delayed_checkout; struct checkout_metadata meta; - struct checkout_copy_source *copy_source; + const char *block_clone_source; unsigned force:1, quiet:1, not_new:1, diff --git a/environment.h b/environment.h index 33d301903c8305..8daab67068e5e8 100644 --- a/environment.h +++ b/environment.h @@ -9,10 +9,8 @@ #define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR" #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE" #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE" -#define GIT_WORKTREE_COPY_SOURCE "GIT_WORKTREE_COPY_SOURCE" -#define GIT_WORKTREE_COPY_SOURCE_INDEX "GIT_WORKTREE_COPY_SOURCE_INDEX" -#define GIT_WORKTREE_COPY_SOURCE_GIT_DIR "GIT_WORKTREE_COPY_SOURCE_GIT_DIR" -#define GIT_WORKTREE_COPY_SOURCE_TIME "GIT_WORKTREE_COPY_SOURCE_TIME" +#define GIT_WORKTREE_BLOCK_CLONE_SOURCE \ + "GIT_WORKTREE_BLOCK_CLONE_SOURCE" #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX" #define DEFAULT_GIT_DIR_ENVIRONMENT ".git" #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" diff --git a/t/helper/test-copy-on-write.c b/t/helper/test-copy-on-write.c deleted file mode 100644 index fe740cf9ebf90a..00000000000000 --- a/t/helper/test-copy-on-write.c +++ /dev/null @@ -1,28 +0,0 @@ -#include "test-tool.h" - -int cmd__copy_on_write(int argc, const char **argv) -{ - struct stat st; - int src_fd, dst_fd; - int ret; - - if (argc != 3) - return 1; - - src_fd = open(argv[1], O_RDONLY); - if (src_fd < 0 || fstat(src_fd, &st)) - return 1; - dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_EXCL, 0666); - if (dst_fd < 0) { - close(src_fd); - return 1; - } - - ret = file_copy_on_write(dst_fd, src_fd, st.st_size); - if (close(dst_fd)) - ret = -1; - close(src_fd); - if (ret) - unlink(argv[2]); - return !!ret; -} diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index 81407f95dbbe77..ee16b2cb23719e 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -17,7 +17,6 @@ static struct test_cmd cmds[] = { { "cache-tree", cmd__cache_tree }, { "chmtime", cmd__chmtime }, { "config", cmd__config }, - { "copy-on-write", cmd__copy_on_write }, { "crontab", cmd__crontab }, { "csprng", cmd__csprng }, { "date", cmd__date }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index 4bebafc12c619b..4cf9f935a4cdfa 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -10,7 +10,6 @@ int cmd__bundle_uri(int argc, const char **argv); int cmd__cache_tree(int argc, const char **argv); int cmd__chmtime(int argc, const char **argv); int cmd__config(int argc, const char **argv); -int cmd__copy_on_write(int argc, const char **argv); int cmd__crontab(int argc, const char **argv); int cmd__csprng(int argc, const char **argv); int cmd__date(int argc, const char **argv); diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 6131724a50850d..992b4f75c0a58d 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -10,13 +10,6 @@ TEST_CREATE_REPO_NO_TEMPLATE=1 . "$TEST_DIRECTORY"/lib-rebase.sh -test_lazy_prereq COPY_ON_WRITE ' - test-tool genzeros 65536 >copy-source && - test-tool copy-on-write copy-source copy-target && - test_cmp copy-source copy-target && - rm copy-source copy-target -' - test_expect_success 'setup' ' test_commit init ' @@ -78,35 +71,30 @@ test_expect_success '"add" worktree' ' ) ' -test_expect_success COPY_ON_WRITE 'worktree add uses copy-on-write' ' - old_head=$(git rev-parse HEAD) && - test_when_finished "git reset --hard $old_head" && - test_when_finished "git worktree remove --force copy-on-write" && - echo "*.crlf text eol=crlf" >.gitattributes && - echo clean >clean && - echo clean >clean.crlf && - echo clean >modified.crlf && - git add .gitattributes clean clean.crlf modified.crlf && - git commit -m "copy-on-write test files" && - rm clean.crlf modified.crlf && - git checkout -- clean.crlf modified.crlf && - echo modified >init.t && - echo modified >modified.crlf && - GIT_TRACE2_EVENT="$PWD/trace" \ - git worktree add --detach copy-on-write main && - test_grep "\"key\":\"copy_on_write\",\"value\":\"clean\"" trace && - test_grep "\"key\":\"copy_on_write\",\"value\":\"clean.crlf\"" trace && - test_must_fail test_grep "\"value\":\"init.t\"" trace && - test_must_fail test_grep "\"value\":\"modified.crlf\"" trace && - git -C copy-on-write diff --exit-code -' - -test_expect_success COPY_ON_WRITE 'worktree.copyOnWrite disables optimization' ' - test_when_finished "git worktree remove --force no-copy-on-write" && - test_config worktree.copyOnWrite false && - GIT_TRACE2_EVENT="$PWD/disabled-trace" \ - git worktree add --detach no-copy-on-write main && - test_must_fail test_grep "\"key\":\"copy_on_write\"" disabled-trace +test_expect_success 'block-clone source does not change checkout contents' ' + test_create_repo clone-source && + test_when_finished \ + "git -C clone-source worktree remove --force ../clone-target" && + ( + cd clone-source && + printf "unchanged\n" >unchanged && + printf "committed\n" >dirty && + printf "converted\n" >converted && + echo "converted text eol=crlf" >.gitattributes && + git add . && + test_tick && + git commit -m files && + git worktree add --detach --no-checkout ../clone-target HEAD && + printf "modified\n" >dirty && + GIT_WORKTREE_BLOCK_CLONE_SOURCE="$PWD" \ + git -C ../clone-target reset --hard + ) && + printf "unchanged\n" >expect && + test_cmp expect clone-target/unchanged && + printf "committed\n" >expect && + test_cmp expect clone-target/dirty && + printf "converted\r\n" >expect && + test_cmp expect clone-target/converted ' test_expect_success '"add" worktree with lock' ' diff --git a/unpack-trees.c b/unpack-trees.c index cbde1070fe1686..27fd72d9584610 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -437,7 +437,7 @@ static int check_updates(struct unpack_trees_options *o, state.quiet = 1; state.refresh_cache = 1; state.istate = index; - state.copy_source = o->copy_source; + state.block_clone_source = o->block_clone_source; clone_checkout_metadata(&state.meta, &o->meta, NULL); if (!o->update || o->dry_run) { diff --git a/unpack-trees.h b/unpack-trees.h index c57346755ac182..94d9a39b23c87c 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -10,7 +10,6 @@ #define MAX_UNPACK_TREES 8 struct cache_entry; -struct checkout_copy_source; struct unpack_trees_options; struct pattern_list; @@ -75,7 +74,8 @@ struct unpack_trees_options { enum unpack_trees_reset_type reset; const char *prefix; const char *super_prefix; - struct checkout_copy_source *copy_source; + /* Borrowed source worktree path, valid for this unpack operation. */ + const char *block_clone_source; struct pathspec *pathspec; merge_fn_t fn; From 7a91083f06784e2a1f92bc4237c6d6de4c0af7ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:02:45 +0000 Subject: [PATCH 10/28] amend! worktree: clone unchanged files on capable volumes worktree: copy unchanged files on write Creating a linked worktree normally materializes another physical copy of every checked-out file. File systems with copy-on-write support can avoid that cost, but only when an existing working-tree file is known to represent exactly the target index entry. For a non-bare source, refresh and write its index after recording the current wall-clock time. The reset child loads that index and considers non-sparse entries whose object IDs and conversion attributes match. It requires the opened file stat data to match the refreshed index and predate the recorded time, both before and after copying, which retains Git\047s racy-index protection without hashing converted or Git LFS content. Keep the decision in reset and honor worktree.copyOnWrite=false so users can disable the optimization if a platform implementation proves problematic. Unsupported operations and all validation failures retain the normal checkout path. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- builtin/reset.c | 53 +++++++++++++++++++++---- builtin/worktree.c | 30 +++++++++++--- entry.c | 97 +++++++++++++++++++++++++++++++--------------- entry.h | 8 +++- environment.h | 6 ++- unpack-trees.c | 2 +- unpack-trees.h | 4 +- 7 files changed, 149 insertions(+), 51 deletions(-) diff --git a/builtin/reset.c b/builtin/reset.c index da1fb160459d69..25c1c4bf761fe5 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -23,6 +23,7 @@ #include "refs.h" #include "diff.h" #include "diffcore.h" +#include "entry.h" #include "tree.h" #include "branch.h" #include "object-name.h" @@ -62,9 +63,8 @@ static inline int is_merge(void) return !access(git_path_merge_head(the_repository), F_OK); } -static int reset_index(const char *ref, const struct object_id *oid, - int reset_type, int quiet, - const char *block_clone_source) +static int reset_index(const char *ref, const struct object_id *oid, int reset_type, int quiet, + struct checkout_copy_source *copy_source) { int i, nr = 0; struct tree_desc desc[2]; @@ -78,7 +78,7 @@ static int reset_index(const char *ref, const struct object_id *oid, opts.dst_index = the_repository->index; opts.fn = oneway_merge; opts.merge = 1; - opts.block_clone_source = block_clone_source; + opts.copy_source = copy_source; init_checkout_metadata(&opts.meta, ref, oid, NULL); if (!quiet) opts.verbose_update = 1; @@ -339,6 +339,37 @@ static int git_reset_config(const char *var, const char *value, return git_default_config(var, value, ctx, cb); } +static int init_copy_source(struct checkout_copy_source *source, + struct index_state *source_index) +{ + const char *worktree = getenv(GIT_WORKTREE_COPY_SOURCE); + const char *index_file = getenv(GIT_WORKTREE_COPY_SOURCE_INDEX); + const char *git_dir = getenv(GIT_WORKTREE_COPY_SOURCE_GIT_DIR); + const char *time_string = getenv(GIT_WORKTREE_COPY_SOURCE_TIME); + char *end; + int enabled = 1; + uintmax_t refreshed_at; + + if (!copy_on_write_supported || + (!repo_config_get_bool(the_repository, "worktree.copyonwrite", + &enabled) && !enabled) || + !worktree || !index_file || !git_dir || !time_string) + return 0; + + errno = 0; + refreshed_at = strtoumax(time_string, &end, 10); + if (errno || *end) + return 0; + if (read_index_from(source_index, index_file, git_dir) < 0) + return 0; + ensure_full_index(source_index); + + source->istate = source_index; + source->worktree = worktree; + source->refreshed_at = refreshed_at; + return 1; +} + int cmd_reset(int argc, const char **argv, const char *prefix, @@ -346,8 +377,10 @@ int cmd_reset(int argc, { int reset_type = NONE, update_ref_status = 0, quiet = 0; int no_refresh = 0; - const char *block_clone_source = - getenv(GIT_WORKTREE_BLOCK_CLONE_SOURCE); + struct index_state copy_source_index = + INDEX_STATE_INIT(the_repository); + struct checkout_copy_source copy_source = { 0 }; + struct checkout_copy_source *copy_source_ptr = NULL; int patch_mode = 0, pathspec_file_nul = 0, unborn; const char *rev; char *pathspec_from_file = NULL; @@ -500,6 +533,9 @@ int cmd_reset(int argc, if (repo_read_index(the_repository) < 0) die(_("index file corrupt")); + if (reset_type == HARD && + init_copy_source(©_source, ©_source_index)) + copy_source_ptr = ©_source; /* Soft reset does not touch the index file nor the working tree * at all, but requires them in a good order. Other resets reset @@ -541,10 +577,10 @@ int cmd_reset(int argc, FREE_AND_NULL(ref); err = reset_index(ref, &oid, reset_type, quiet, - block_clone_source); + copy_source_ptr); if (reset_type == KEEP && !err) err = reset_index(ref, &oid, MIXED, quiet, - block_clone_source); + copy_source_ptr); if (err) die(_("Could not reset index file to revision '%s'."), rev); free(ref); @@ -568,6 +604,7 @@ int cmd_reset(int argc, discard_index(the_repository->index); cleanup: + release_index(©_source_index); clear_pathspec(&pathspec); free(pathspec_from_file); return update_ref_status; diff --git a/builtin/worktree.c b/builtin/worktree.c index c740f22d42fbf9..b5b08f40648a72 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -395,20 +395,38 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir) } static int checkout_worktree(const struct add_opts *opts, - struct strvec *child_env, const char *path) + struct strvec *child_env) { struct child_process cp = CHILD_PROCESS_INIT; const char *source = repo_get_work_tree(the_repository); + struct timeval refreshed_at; cp.git_cmd = 1; strvec_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); if (opts->quiet) strvec_push(&cp.args, "--quiet"); strvec_pushv(&cp.env, child_env->v); - if (source && - block_cloning_supported(source, absolute_path(path))) - strvec_pushf(&cp.env, "%s=%s", - GIT_WORKTREE_BLOCK_CLONE_SOURCE, source); + if (source && repo_read_index(the_repository) >= 0) { + gettimeofday(&refreshed_at, NULL); + if (repo_refresh_and_write_index(the_repository, + REFRESH_QUIET | REFRESH_REALLY, + SKIP_IF_UNCHANGED, 1, + NULL, NULL, NULL) >= 0) { + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE, source); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE_INDEX, + repo_get_index_file(the_repository)); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE_GIT_DIR, + repo_get_git_dir(the_repository)); + strvec_pushf(&cp.env, "%s=%"PRIuMAX, + GIT_WORKTREE_COPY_SOURCE_TIME, + (uintmax_t)refreshed_at.tv_sec * + 1000000000 + + refreshed_at.tv_usec * 1000); + } + } return run_command(&cp); } @@ -595,7 +613,7 @@ static int add_worktree(const char *path, const char *refname, goto done; if (opts->checkout && - (ret = checkout_worktree(opts, &child_env, path))) + (ret = checkout_worktree(opts, &child_env))) goto done; is_junk = 0; diff --git a/entry.c b/entry.c index 96984ee01db39c..e0e6261ebe5077 100644 --- a/entry.c +++ b/entry.c @@ -8,7 +8,6 @@ #include "gettext.h" #include "hex.h" #include "name-hash.h" -#include "object-file.h" #include "sparse-index.h" #include "submodule.h" #include "symlinks.h" @@ -16,6 +15,7 @@ #include "fsmonitor.h" #include "entry.h" #include "parallel-checkout.h" +#include "trace2.h" #include "wrapper.h" static void create_directories(const char *path, int path_len, @@ -130,49 +130,82 @@ static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempf } } -static int is_block_clone_candidate(const struct cache_entry *ce, - const struct conv_attrs *ca, - const struct checkout *state) +static int conversion_attrs_equal(const struct conv_attrs *a, + const struct conv_attrs *b) { - return state->block_clone_source && S_ISREG(ce->ce_mode) && - ca && !ca->drv && !ca->ident && !ca->working_tree_encoding && - ca->crlf_action == CRLF_BINARY; + return a->drv == b->drv && + a->crlf_action == b->crlf_action && + a->ident == b->ident && + (!a->working_tree_encoding == !b->working_tree_encoding) && + (!a->working_tree_encoding || + !strcmp(a->working_tree_encoding, b->working_tree_encoding)); } -/* - * Return 1 after cloning the verified source file. Return 0 without leaving - * the destination behind when cloning is inapplicable or fails, so the caller - * can use the normal checkout path. - */ -static int clone_entry(const struct cache_entry *ce, char *path, - const struct conv_attrs *ca, - const struct checkout *state, - int *fstat_done, struct stat *statbuf) +static struct cache_entry *copy_source_entry(const struct cache_entry *ce, + const struct conv_attrs *ca, + const struct checkout *state) +{ + struct index_state *istate; + struct cache_entry *source; + struct conv_attrs source_ca; + int pos; + + if (!state->copy_source || !ca || !S_ISREG(ce->ce_mode)) + return NULL; + + istate = state->copy_source->istate; + pos = index_name_pos(istate, ce->name, ce_namelen(ce)); + if (pos < 0) + return NULL; + source = istate->cache[pos]; + if (!S_ISREG(source->ce_mode) || ce_skip_worktree(source) || + !oideq(&source->oid, &ce->oid)) + return NULL; + convert_attrs(istate, &source_ca, ce->name); + if (!conversion_attrs_equal(ca, &source_ca)) + return NULL; + + return source; +} + +static int source_is_uptodate(const struct checkout_copy_source *source, + const struct cache_entry *ce, struct stat *st) +{ + uint64_t mtime; + + if (match_stat_data(&ce->ce_stat_data, st)) + return 0; + + mtime = (uint64_t)st->st_mtime * 1000000000 + ST_MTIME_NSEC(*st); + return mtime < source->refreshed_at; +} + +static int try_copy_on_write(const struct cache_entry *ce, char *path, + const struct conv_attrs *ca, + const struct checkout *state, + int *fstat_done, struct stat *statbuf) { struct strbuf source = STRBUF_INIT; - struct object_id oid; - struct stat st; - int src_fd = -1, dst_fd = -1, hash_fd; + struct cache_entry *source_ce = copy_source_entry(ce, ca, state); + struct stat st, st_after; + int src_fd = -1, dst_fd = -1; int ret = 0; - if (!is_block_clone_candidate(ce, ca, state)) + if (!source_ce) return 0; - strbuf_addf(&source, "%s/%s", state->block_clone_source, ce->name); + strbuf_addf(&source, "%s/%s", state->copy_source->worktree, ce->name); src_fd = open_nofollow(source.buf, O_RDONLY); - if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode)) - goto done; - - hash_fd = dup(src_fd); - if (hash_fd < 0 || - index_fd(state->istate, &oid, hash_fd, &st, OBJ_BLOB, NULL, 0) || - !oideq(&oid, &ce->oid)) + if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode) || + !source_is_uptodate(state->copy_source, source_ce, &st)) goto done; dst_fd = open_output_fd(path, ce, 0); if (dst_fd < 0) goto done; - if (block_clone_file(dst_fd, src_fd, st.st_size)) { + if (file_copy_on_write(dst_fd, src_fd, st.st_size) || + fstat(src_fd, &st_after) || + !source_is_uptodate(state->copy_source, source_ce, &st_after)) { close(dst_fd); dst_fd = -1; unlink(path); @@ -187,6 +220,8 @@ static int clone_entry(const struct cache_entry *ce, char *path, if (state->refresh_cache) *fstat_done = !lstat(path, statbuf); + trace2_data_string("checkout", the_repository, + "copy_on_write", ce->name); ret = 1; done: @@ -386,7 +421,7 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca struct stream_filter *filter; if (!to_tempfile && - clone_entry(ce, path, ca, state, &fstat_done, &st)) + try_copy_on_write(ce, path, ca, state, &fstat_done, &st)) goto finish; filter = get_stream_filter_ca(ca, &ce->oid); @@ -678,7 +713,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - if (!is_block_clone_candidate(ce, ca, state) && + if (!copy_source_entry(ce, ca, state) && !enqueue_checkout(ce, ca, nr_checkouts)) return 0; diff --git a/entry.h b/entry.h index 07a7d39ef16dc2..c24c353f5ba22c 100644 --- a/entry.h +++ b/entry.h @@ -6,6 +6,12 @@ struct cache_entry; struct index_state; +struct checkout_copy_source { + struct index_state *istate; + const char *worktree; + uint64_t refreshed_at; +}; + struct checkout { struct index_state *istate; const char *base_dir; @@ -13,7 +19,7 @@ struct checkout { const char *super_prefix; struct delayed_checkout *delayed_checkout; struct checkout_metadata meta; - const char *block_clone_source; + struct checkout_copy_source *copy_source; unsigned force:1, quiet:1, not_new:1, diff --git a/environment.h b/environment.h index 8daab67068e5e8..33d301903c8305 100644 --- a/environment.h +++ b/environment.h @@ -9,8 +9,10 @@ #define GIT_COMMON_DIR_ENVIRONMENT "GIT_COMMON_DIR" #define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE" #define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE" -#define GIT_WORKTREE_BLOCK_CLONE_SOURCE \ - "GIT_WORKTREE_BLOCK_CLONE_SOURCE" +#define GIT_WORKTREE_COPY_SOURCE "GIT_WORKTREE_COPY_SOURCE" +#define GIT_WORKTREE_COPY_SOURCE_INDEX "GIT_WORKTREE_COPY_SOURCE_INDEX" +#define GIT_WORKTREE_COPY_SOURCE_GIT_DIR "GIT_WORKTREE_COPY_SOURCE_GIT_DIR" +#define GIT_WORKTREE_COPY_SOURCE_TIME "GIT_WORKTREE_COPY_SOURCE_TIME" #define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX" #define DEFAULT_GIT_DIR_ENVIRONMENT ".git" #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" diff --git a/unpack-trees.c b/unpack-trees.c index 27fd72d9584610..cbde1070fe1686 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -437,7 +437,7 @@ static int check_updates(struct unpack_trees_options *o, state.quiet = 1; state.refresh_cache = 1; state.istate = index; - state.block_clone_source = o->block_clone_source; + state.copy_source = o->copy_source; clone_checkout_metadata(&state.meta, &o->meta, NULL); if (!o->update || o->dry_run) { diff --git a/unpack-trees.h b/unpack-trees.h index 94d9a39b23c87c..c57346755ac182 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -10,6 +10,7 @@ #define MAX_UNPACK_TREES 8 struct cache_entry; +struct checkout_copy_source; struct unpack_trees_options; struct pattern_list; @@ -74,8 +75,7 @@ struct unpack_trees_options { enum unpack_trees_reset_type reset; const char *prefix; const char *super_prefix; - /* Borrowed source worktree path, valid for this unpack operation. */ - const char *block_clone_source; + struct checkout_copy_source *copy_source; struct pathspec *pathspec; merge_fn_t fn; From 0deaaee6762ade6aafc1cc54c0d306c852c250e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:02:45 +0000 Subject: [PATCH 11/28] fixup! compat: add a block-cloning primitive Expose the primitive through test-tool so tests can establish a runtime prerequisite by performing a real copy-on-write operation. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- Makefile | 1 + t/helper/test-copy-on-write.c | 28 ++++++++++++++++++++++++++++ t/helper/test-tool.c | 1 + t/helper/test-tool.h | 1 + 4 files changed, 31 insertions(+) create mode 100644 t/helper/test-copy-on-write.c diff --git a/Makefile b/Makefile index c98be9155c54ae..31c529ffd970b0 100644 --- a/Makefile +++ b/Makefile @@ -819,6 +819,7 @@ TEST_BUILTINS_OBJS += test-bundle-uri.o TEST_BUILTINS_OBJS += test-cache-tree.o TEST_BUILTINS_OBJS += test-chmtime.o TEST_BUILTINS_OBJS += test-config.o +TEST_BUILTINS_OBJS += test-copy-on-write.o TEST_BUILTINS_OBJS += test-crontab.o TEST_BUILTINS_OBJS += test-csprng.o TEST_BUILTINS_OBJS += test-date.o diff --git a/t/helper/test-copy-on-write.c b/t/helper/test-copy-on-write.c new file mode 100644 index 00000000000000..fe740cf9ebf90a --- /dev/null +++ b/t/helper/test-copy-on-write.c @@ -0,0 +1,28 @@ +#include "test-tool.h" + +int cmd__copy_on_write(int argc, const char **argv) +{ + struct stat st; + int src_fd, dst_fd; + int ret; + + if (argc != 3) + return 1; + + src_fd = open(argv[1], O_RDONLY); + if (src_fd < 0 || fstat(src_fd, &st)) + return 1; + dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_EXCL, 0666); + if (dst_fd < 0) { + close(src_fd); + return 1; + } + + ret = file_copy_on_write(dst_fd, src_fd, st.st_size); + if (close(dst_fd)) + ret = -1; + close(src_fd); + if (ret) + unlink(argv[2]); + return !!ret; +} diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index ee16b2cb23719e..81407f95dbbe77 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -17,6 +17,7 @@ static struct test_cmd cmds[] = { { "cache-tree", cmd__cache_tree }, { "chmtime", cmd__chmtime }, { "config", cmd__config }, + { "copy-on-write", cmd__copy_on_write }, { "crontab", cmd__crontab }, { "csprng", cmd__csprng }, { "date", cmd__date }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index 4cf9f935a4cdfa..4bebafc12c619b 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -10,6 +10,7 @@ int cmd__bundle_uri(int argc, const char **argv); int cmd__cache_tree(int argc, const char **argv); int cmd__chmtime(int argc, const char **argv); int cmd__config(int argc, const char **argv); +int cmd__copy_on_write(int argc, const char **argv); int cmd__crontab(int argc, const char **argv); int cmd__csprng(int argc, const char **argv); int cmd__date(int argc, const char **argv); From e34ff82561fd961651557b3f15098cc49e034e19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:02:45 +0000 Subject: [PATCH 12/28] fixup! worktree: clone unchanged files on capable volumes Exercise the optimization only after a real copy-on-write probe succeeds. Verify Trace2 records successful copies for clean raw and CRLF-converted files, excludes dirty raw and converted files, and honors the configuration opt-out. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- t/t2400-worktree-add.sh | 60 ++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 992b4f75c0a58d..6131724a50850d 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -10,6 +10,13 @@ TEST_CREATE_REPO_NO_TEMPLATE=1 . "$TEST_DIRECTORY"/lib-rebase.sh +test_lazy_prereq COPY_ON_WRITE ' + test-tool genzeros 65536 >copy-source && + test-tool copy-on-write copy-source copy-target && + test_cmp copy-source copy-target && + rm copy-source copy-target +' + test_expect_success 'setup' ' test_commit init ' @@ -71,30 +78,35 @@ test_expect_success '"add" worktree' ' ) ' -test_expect_success 'block-clone source does not change checkout contents' ' - test_create_repo clone-source && - test_when_finished \ - "git -C clone-source worktree remove --force ../clone-target" && - ( - cd clone-source && - printf "unchanged\n" >unchanged && - printf "committed\n" >dirty && - printf "converted\n" >converted && - echo "converted text eol=crlf" >.gitattributes && - git add . && - test_tick && - git commit -m files && - git worktree add --detach --no-checkout ../clone-target HEAD && - printf "modified\n" >dirty && - GIT_WORKTREE_BLOCK_CLONE_SOURCE="$PWD" \ - git -C ../clone-target reset --hard - ) && - printf "unchanged\n" >expect && - test_cmp expect clone-target/unchanged && - printf "committed\n" >expect && - test_cmp expect clone-target/dirty && - printf "converted\r\n" >expect && - test_cmp expect clone-target/converted +test_expect_success COPY_ON_WRITE 'worktree add uses copy-on-write' ' + old_head=$(git rev-parse HEAD) && + test_when_finished "git reset --hard $old_head" && + test_when_finished "git worktree remove --force copy-on-write" && + echo "*.crlf text eol=crlf" >.gitattributes && + echo clean >clean && + echo clean >clean.crlf && + echo clean >modified.crlf && + git add .gitattributes clean clean.crlf modified.crlf && + git commit -m "copy-on-write test files" && + rm clean.crlf modified.crlf && + git checkout -- clean.crlf modified.crlf && + echo modified >init.t && + echo modified >modified.crlf && + GIT_TRACE2_EVENT="$PWD/trace" \ + git worktree add --detach copy-on-write main && + test_grep "\"key\":\"copy_on_write\",\"value\":\"clean\"" trace && + test_grep "\"key\":\"copy_on_write\",\"value\":\"clean.crlf\"" trace && + test_must_fail test_grep "\"value\":\"init.t\"" trace && + test_must_fail test_grep "\"value\":\"modified.crlf\"" trace && + git -C copy-on-write diff --exit-code +' + +test_expect_success COPY_ON_WRITE 'worktree.copyOnWrite disables optimization' ' + test_when_finished "git worktree remove --force no-copy-on-write" && + test_config worktree.copyOnWrite false && + GIT_TRACE2_EVENT="$PWD/disabled-trace" \ + git worktree add --detach no-copy-on-write main && + test_must_fail test_grep "\"key\":\"copy_on_write\"" disabled-trace ' test_expect_success '"add" worktree with lock' ' From 6de9e4e25a21f26516726c46883a8da109e56c45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:02:45 +0000 Subject: [PATCH 13/28] fixup! worktree: clone unchanged files on capable volumes Document the copy-on-write optimization and its opt-out. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- Documentation/config/worktree.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/config/worktree.adoc b/Documentation/config/worktree.adoc index 0930183b91cb8f..cd7cf186dea91c 100644 --- a/Documentation/config/worktree.adoc +++ b/Documentation/config/worktree.adoc @@ -9,6 +9,11 @@ fails. If no such match can be found, it falls back to creating a new branch from the current `HEAD`. +`worktree.copyOnWrite`:: + When creating a linked worktree, use copy-on-write file copies when + supported by the platform and file system. Setting this to `false` + disables the optimization. Defaults to `true`. + `worktree.useRelativePaths`:: Link worktrees using relative paths (when "`true`") or absolute paths (when "`false`"). This is particularly useful for setups From 8c436fa893c157c991b2e9ad5dfc013b02297b23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:07:08 +0000 Subject: [PATCH 14/28] fixup! worktree: clone unchanged files on capable volumes Avoid refreshing and writing the source index on platforms where the copy-on-write primitive is unavailable. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- builtin/worktree.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/builtin/worktree.c b/builtin/worktree.c index b5b08f40648a72..df53869499b904 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -406,7 +406,8 @@ static int checkout_worktree(const struct add_opts *opts, if (opts->quiet) strvec_push(&cp.args, "--quiet"); strvec_pushv(&cp.env, child_env->v); - if (source && repo_read_index(the_repository) >= 0) { + if (copy_on_write_supported && source && + repo_read_index(the_repository) >= 0) { gettimeofday(&refreshed_at, NULL); if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET | REFRESH_REALLY, From 700730d57d5badac9e539ea4a42d50b8f31b39f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:07:08 +0000 Subject: [PATCH 15/28] fixup! worktree: clone unchanged files on capable volumes Document the lifetime of the caller-owned copy source passed through unpack_trees_options. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- unpack-trees.h | 1 + 1 file changed, 1 insertion(+) diff --git a/unpack-trees.h b/unpack-trees.h index c57346755ac182..f9db06138902c3 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -75,6 +75,7 @@ struct unpack_trees_options { enum unpack_trees_reset_type reset; const char *prefix; const char *super_prefix; + /* Borrowed for the duration of this unpack operation. */ struct checkout_copy_source *copy_source; struct pathspec *pathspec; merge_fn_t fn; From 78b53c4f66d70388fb6f4e1d7d2e53482b186eab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:09:09 +0000 Subject: [PATCH 16/28] fixup! worktree: clone unchanged files on capable volumes Keep the configuration opt-out separate from capability and metadata validation so the successful lookup semantics are explicit. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- builtin/reset.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/builtin/reset.c b/builtin/reset.c index 25c1c4bf761fe5..eafffc8a908f77 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -351,10 +351,11 @@ static int init_copy_source(struct checkout_copy_source *source, uintmax_t refreshed_at; if (!copy_on_write_supported || - (!repo_config_get_bool(the_repository, "worktree.copyonwrite", - &enabled) && !enabled) || !worktree || !index_file || !git_dir || !time_string) return 0; + if (!repo_config_get_bool(the_repository, "worktree.copyonwrite", + &enabled) && !enabled) + return 0; errno = 0; refreshed_at = strtoumax(time_string, &end, 10); From 9d6b7dd75a7e1487f227f0b4b367ea10c02bd7f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:09:09 +0000 Subject: [PATCH 17/28] fixup! worktree: clone unchanged files on capable volumes State explicitly that both the copy source and its index remain caller-owned for the duration of unpacking. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- unpack-trees.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unpack-trees.h b/unpack-trees.h index f9db06138902c3..1faaa648abbb0d 100644 --- a/unpack-trees.h +++ b/unpack-trees.h @@ -75,7 +75,7 @@ struct unpack_trees_options { enum unpack_trees_reset_type reset; const char *prefix; const char *super_prefix; - /* Borrowed for the duration of this unpack operation. */ + /* Caller-owned source and index, valid for this unpack operation. */ struct checkout_copy_source *copy_source; struct pathspec *pathspec; merge_fn_t fn; From e1d8f6a0a881f32a6c44f4319f4a7a0f3313f6c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:11:34 +0000 Subject: [PATCH 18/28] fixup! worktree: clone unchanged files on capable volumes Resolve the source index entry and its conversion attributes once before deciding whether checkout can run in parallel, then carry that entry into the synchronous copy-on-write path. Signed-off-by: GitHub Co-authored-by: GPT-5.4 Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- entry.c | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/entry.c b/entry.c index e0e6261ebe5077..2c3233d0f58c0c 100644 --- a/entry.c +++ b/entry.c @@ -180,20 +180,16 @@ static int source_is_uptodate(const struct checkout_copy_source *source, return mtime < source->refreshed_at; } -static int try_copy_on_write(const struct cache_entry *ce, char *path, - const struct conv_attrs *ca, +static int try_copy_on_write(const struct cache_entry *ce, + const struct cache_entry *source_ce, char *path, const struct checkout *state, int *fstat_done, struct stat *statbuf) { struct strbuf source = STRBUF_INIT; - struct cache_entry *source_ce = copy_source_entry(ce, ca, state); struct stat st, st_after; int src_fd = -1, dst_fd = -1; int ret = 0; - if (!source_ce) - return 0; - strbuf_addf(&source, "%s/%s", state->copy_source->worktree, ce->name); src_fd = open_nofollow(source.buf, O_RDONLY); if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode) || @@ -399,6 +395,7 @@ void update_ce_after_write(const struct checkout *state, struct cache_entry *ce, /* Note: ca is used (and required) iff the entry refers to a regular file. */ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca, + const struct cache_entry *source_ce, const struct checkout *state, int to_tempfile, int *nr_checkouts) { @@ -420,8 +417,9 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca if (ce_mode_s_ifmt == S_IFREG) { struct stream_filter *filter; - if (!to_tempfile && - try_copy_on_write(ce, path, ca, state, &fstat_done, &st)) + if (source_ce && + try_copy_on_write(ce, source_ce, path, state, + &fstat_done, &st)) goto finish; filter = get_stream_filter_ca(ca, &ce->oid); @@ -613,6 +611,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, static struct strbuf path = STRBUF_INIT; struct stat st; struct conv_attrs ca_buf; + struct cache_entry *source_ce = NULL; if (ce->ce_flags & CE_WT_REMOVE) { if (topath) @@ -630,7 +629,8 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, convert_attrs(state->istate, &ca_buf, ce->name); ca = &ca_buf; } - return write_entry(ce, topath, ca, state, 1, nr_checkouts); + return write_entry(ce, topath, ca, NULL, state, 1, + nr_checkouts); } strbuf_reset(&path); @@ -713,11 +713,12 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - if (!copy_source_entry(ce, ca, state) && - !enqueue_checkout(ce, ca, nr_checkouts)) + source_ce = copy_source_entry(ce, ca, state); + if (!source_ce && !enqueue_checkout(ce, ca, nr_checkouts)) return 0; - return write_entry(ce, path.buf, ca, state, 0, nr_checkouts); + return write_entry(ce, path.buf, ca, source_ce, state, 0, + nr_checkouts); } void unlink_entry(const struct cache_entry *ce, const char *super_prefix) From d4127a98ee6d1acf4d9107d49374988723c83800 Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Sun, 4 Oct 2026 00:41:53 +0200 Subject: [PATCH 19/28] files which needs conversion are skipped from cloning Signed-off-by: Tomas Kubes --- entry.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/entry.c b/entry.c index 2c3233d0f58c0c..7a1e0d19366342 100644 --- a/entry.c +++ b/entry.c @@ -150,7 +150,9 @@ static struct cache_entry *copy_source_entry(const struct cache_entry *ce, struct conv_attrs source_ca; int pos; - if (!state->copy_source || !ca || !S_ISREG(ce->ce_mode)) + if (!state->copy_source || !ca || !S_ISREG(ce->ce_mode) || + ca->drv || ca->ident || ca->working_tree_encoding || + ca->crlf_action != CRLF_BINARY) return NULL; istate = state->copy_source->istate; From 5f75884a6627c2bea754e4e84d7c58f45038ad6e Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Sun, 4 Oct 2026 00:44:36 +0200 Subject: [PATCH 20/28] path of index needs be absolute to point old index so it can resolve source of cloning properly Signed-off-by: Tomas Kubes --- builtin/worktree.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/worktree.c b/builtin/worktree.c index df53869499b904..0dafbabbe9617a 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -417,10 +417,10 @@ static int checkout_worktree(const struct add_opts *opts, GIT_WORKTREE_COPY_SOURCE, source); strvec_pushf(&cp.env, "%s=%s", GIT_WORKTREE_COPY_SOURCE_INDEX, - repo_get_index_file(the_repository)); + absolute_path(repo_get_index_file(the_repository))); strvec_pushf(&cp.env, "%s=%s", GIT_WORKTREE_COPY_SOURCE_GIT_DIR, - repo_get_git_dir(the_repository)); + absolute_path(repo_get_git_dir(the_repository))); strvec_pushf(&cp.env, "%s=%"PRIuMAX, GIT_WORKTREE_COPY_SOURCE_TIME, (uintmax_t)refreshed_at.tv_sec * From 422ce68c7d5028db6146bb68b5c70e2b330f30d7 Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Sun, 4 Oct 2026 00:47:30 +0200 Subject: [PATCH 21/28] Test writes cloned blocks to output for later evaluation, CRLF are not expected to be cloned even it those files are clean, that is more conservative approach. Signed-off-by: Tomas Kubes --- t/t2400-worktree-add.sh | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 6131724a50850d..72a71ff3bbf5a9 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -5,6 +5,9 @@ test_description='test git worktree add' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +GIT_TRACE2_EVENT_NESTING=10 +export GIT_TRACE2_EVENT_NESTING + TEST_CREATE_REPO_NO_TEMPLATE=1 . ./test-lib.sh @@ -86,7 +89,8 @@ test_expect_success COPY_ON_WRITE 'worktree add uses copy-on-write' ' echo clean >clean && echo clean >clean.crlf && echo clean >modified.crlf && - git add .gitattributes clean clean.crlf modified.crlf && + echo clean >unconverted.crlf && + git add .gitattributes clean clean.crlf modified.crlf unconverted.crlf && git commit -m "copy-on-write test files" && rm clean.crlf modified.crlf && git checkout -- clean.crlf modified.crlf && @@ -95,10 +99,14 @@ test_expect_success COPY_ON_WRITE 'worktree add uses copy-on-write' ' GIT_TRACE2_EVENT="$PWD/trace" \ git worktree add --detach copy-on-write main && test_grep "\"key\":\"copy_on_write\",\"value\":\"clean\"" trace && - test_grep "\"key\":\"copy_on_write\",\"value\":\"clean.crlf\"" trace && - test_must_fail test_grep "\"value\":\"init.t\"" trace && - test_must_fail test_grep "\"value\":\"modified.crlf\"" trace && - git -C copy-on-write diff --exit-code + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"clean.crlf\"" trace && + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"unconverted.crlf\"" trace && + test_grep ! "\"value\":\"init.t\"" trace && + test_grep ! "\"value\":\"modified.crlf\"" trace && + git -C copy-on-write diff --exit-code && + printf "clean\r\n" >expect-crlf && + test_cmp expect-crlf copy-on-write/clean.crlf && + test_cmp expect-crlf copy-on-write/unconverted.crlf ' test_expect_success COPY_ON_WRITE 'worktree.copyOnWrite disables optimization' ' @@ -106,7 +114,7 @@ test_expect_success COPY_ON_WRITE 'worktree.copyOnWrite disables optimization' ' test_config worktree.copyOnWrite false && GIT_TRACE2_EVENT="$PWD/disabled-trace" \ git worktree add --detach no-copy-on-write main && - test_must_fail test_grep "\"key\":\"copy_on_write\"" disabled-trace + test_grep ! "\"key\":\"copy_on_write\"" disabled-trace ' test_expect_success '"add" worktree with lock' ' From bcd0e1f3c12e91b6daddacc8079b042b066c423f Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Sun, 4 Oct 2026 09:58:24 +0200 Subject: [PATCH 22/28] significant performance improvement - cloned files are not prefilled with zeros Signed-off-by: Tomas Kubes --- compat/mingw.c | 22 +++++++++++++++++++--- t/t2400-worktree-add.sh | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index e37f74e5d0f4df..787921357b2619 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -35,6 +35,19 @@ #define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056) #define HCAST(type, handle) ((type)(intptr_t)handle) +static int set_copy_on_write_file_size(HANDLE handle, off_t size) +{ + LARGE_INTEGER end; + + end.QuadPart = size; + if (!SetFilePointerEx(handle, end, NULL, FILE_BEGIN) || + !SetEndOfFile(handle)) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + return 0; +} + int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size) { DUPLICATE_EXTENTS_DATA data = { @@ -51,8 +64,11 @@ int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size) DWORD bytes_returned; size_t i; - /* The destination range must exist before extents can be cloned. */ - if (ftruncate(dst_fd, size) < 0) + /* + * Prepare the destination without ftruncate(), which zero-fills + * the extension through _chsize_s() before we replace its extents. + */ + if (set_copy_on_write_file_size(dst, size) < 0) return -1; if (!size) return 0; @@ -79,7 +95,7 @@ int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size) &data, sizeof(data), NULL, 0, &bytes_returned, NULL)) { /* Discard any allocation-unit padding in the tail. */ - return ftruncate(dst_fd, size); + return set_copy_on_write_file_size(dst, size); } } diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 72a71ff3bbf5a9..0d0043158ceebe 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -20,6 +20,20 @@ test_lazy_prereq COPY_ON_WRITE ' rm copy-source copy-target ' +test_expect_success COPY_ON_WRITE 'copy-on-write preserves sizes and source' ' + test_when_finished "rm -f size-source size-target size-expect" && + for size in 0 1 4095 4096 4097 65535 65536 65537 1048577 + do + test-tool genrandom clone-size $size >size-source && + cp size-source size-expect && + test-tool copy-on-write size-source size-target && + test_cmp size-source size-target && + echo changed >>size-target && + test_cmp size-expect size-source && + rm size-target || return 1 + done +' + test_expect_success 'setup' ' test_commit init ' From a93d90ed68a3af9b789592080dba540b8641aabb Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Sun, 4 Oct 2026 16:31:16 +0200 Subject: [PATCH 23/28] ReFS alignment is cached for given drive, it also solves problem if failed clone attempt on NTFS Signed-off-by: Tomas Kubes --- builtin/worktree.c | 6 +- compat/mingw.c | 151 +++++++++++++++++++++++++++------- compat/mingw.h | 5 +- entry.c | 5 +- git-compat-util.h | 6 +- t/helper/test-copy-on-write.c | 43 ++++++++-- t/t2400-worktree-add.sh | 33 ++++++++ 7 files changed, 207 insertions(+), 42 deletions(-) diff --git a/builtin/worktree.c b/builtin/worktree.c index 0dafbabbe9617a..d795cb8909c89e 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -395,7 +395,7 @@ static void copy_filtered_worktree_config(const char *worktree_git_dir) } static int checkout_worktree(const struct add_opts *opts, - struct strvec *child_env) + struct strvec *child_env, const char *path) { struct child_process cp = CHILD_PROCESS_INIT; const char *source = repo_get_work_tree(the_repository); @@ -406,7 +406,7 @@ static int checkout_worktree(const struct add_opts *opts, if (opts->quiet) strvec_push(&cp.args, "--quiet"); strvec_pushv(&cp.env, child_env->v); - if (copy_on_write_supported && source && + if (file_copy_on_write_supported(path) && source && repo_read_index(the_repository) >= 0) { gettimeofday(&refreshed_at, NULL); if (repo_refresh_and_write_index(the_repository, @@ -614,7 +614,7 @@ static int add_worktree(const char *path, const char *refname, goto done; if (opts->checkout && - (ret = checkout_worktree(opts, &child_env))) + (ret = checkout_worktree(opts, &child_env, path))) goto done; is_junk = 0; diff --git a/compat/mingw.c b/compat/mingw.c index 787921357b2619..be72a62e75be27 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -48,28 +48,117 @@ static int set_copy_on_write_file_size(HANDLE handle, off_t size) return 0; } -int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size) +/* Zero means unprobed; -1 disables cloning for this process. */ +static int copy_on_write_alignments[26]; +/* Re-resolve relative paths after mingw_chdir(). */ +static int copy_on_write_current_drive = -2; + +static int copy_on_write_drive(const char *path) +{ + int drive; + + if (is_dir_sep(path[0]) && is_dir_sep(path[1]) && + (path[2] == '?' || path[2] == '.') && is_dir_sep(path[3])) + path += 4; + if (has_dos_drive_prefix(path)) { + drive = tolower((unsigned char)path[0]) - 'a'; + return drive >= 0 && drive < 26 ? drive : -1; + } + /* UNC paths have no drive entry in this conservative cache. */ + if (is_dir_sep(path[0]) && is_dir_sep(path[1])) + return -1; + return copy_on_write_current_drive; +} + +static int resolve_copy_on_write_current_drive(void) +{ + WCHAR cwd[MAX_LONG_PATH]; + const WCHAR *current = cwd; + DWORD length; + int drive; + + length = GetCurrentDirectoryW(ARRAY_SIZE(cwd), cwd); + if (!length || length >= ARRAY_SIZE(cwd)) + return copy_on_write_current_drive = -1; + if (length > 5 && cwd[0] == L'\\' && cwd[1] == L'\\' && + (cwd[2] == L'?' || cwd[2] == L'.') && cwd[3] == L'\\') + current += 4; + drive = current[1] == L':' ? towlower(current[0]) - L'a' : -1; + copy_on_write_current_drive = drive >= 0 && drive < 26 ? drive : -1; + return copy_on_write_current_drive; +} + +static int resolve_copy_on_write_drive(const char *path) +{ + int drive = copy_on_write_drive(path); + + return drive == -2 ? resolve_copy_on_write_current_drive() : drive; +} + +static int copy_on_write_alignment(int drive) +{ + return drive < 0 ? -1 : copy_on_write_alignments[drive]; +} + +static int probe_copy_on_write_alignment(int drive) +{ + WCHAR root[] = L"A:\\", filesystem[16]; + DWORD sectors, bytes, free_clusters, clusters; + + root[0] += drive; + if (!GetVolumeInformationW(root, NULL, 0, NULL, NULL, NULL, + filesystem, ARRAY_SIZE(filesystem)) || + wcscmp(filesystem, L"ReFS") || + !GetDiskFreeSpaceW(root, §ors, &bytes, &free_clusters, + &clusters) || + !sectors || !bytes || sectors > 65536 / bytes) + return -1; + /* Only the cluster size is used; free-space results are ignored. */ + return sectors * bytes == 4096 || sectors * bytes == 65536 ? + sectors * bytes : -1; +} + +static int prepare_copy_on_write_alignment(int drive) +{ + int alignment = copy_on_write_alignment(drive); + + if (alignment) + return alignment; + alignment = probe_copy_on_write_alignment(drive); + copy_on_write_alignments[drive] = alignment; + trace2_data_intmax("checkout", NULL, "copy_on_write_alignment", + alignment); + return alignment; +} + +int mingw_copy_on_write_supported(const char *path) +{ + int drive = resolve_copy_on_write_drive(path); + + return prepare_copy_on_write_alignment(drive) > 0; +} + +int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, + const char *path) { DUPLICATE_EXTENTS_DATA data = { .FileHandle = HCAST(HANDLE, _get_osfhandle(src_fd)), }; HANDLE dst = HCAST(HANDLE, _get_osfhandle(dst_fd)); - /* - * FSCTL_DUPLICATE_EXTENTS_TO_FILE accepts ranges smaller than 4 GiB, - * aligned to the volume's cluster size. ReFS uses 64 KiB or 4 KiB - * clusters, so this is the largest valid range for either. - */ - const LONGLONG max_range = (4LL * 1024 * 1024 * 1024) - (64 * 1024); - const LONGLONG tail_alignments[] = { 64 * 1024, 4 * 1024 }; + int drive = resolve_copy_on_write_drive(path); + int alignment = prepare_copy_on_write_alignment(drive); + LONGLONG max_range; DWORD bytes_returned; - size_t i; + int saved_errno; - /* - * Prepare the destination without ftruncate(), which zero-fills - * the extension through _chsize_s() before we replace its extents. - */ - if (set_copy_on_write_file_size(dst, size) < 0) + if (alignment < 0) { + errno = ENOSYS; return -1; + } + /* Ranges must be cluster-aligned and smaller than 4 GiB. */ + max_range = (4LL * 1024 * 1024 * 1024) - alignment; + if (set_copy_on_write_file_size(dst, size) < 0) + goto failed; if (!size) return 0; @@ -79,27 +168,32 @@ int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size) &data, sizeof(data), NULL, 0, &bytes_returned, NULL)) { errno = ENOSYS; - return -1; + goto failed; } data.SourceFileOffset.QuadPart += data.ByteCount.QuadPart; data.TargetFileOffset = data.SourceFileOffset; } - for (i = 0; i < ARRAY_SIZE(tail_alignments); i++) { - LONGLONG remaining = size - data.SourceFileOffset.QuadPart; - - data.ByteCount.QuadPart = - DIV_ROUND_UP(remaining, tail_alignments[i]) * - tail_alignments[i]; - if (DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, - &data, sizeof(data), NULL, 0, - &bytes_returned, NULL)) { - /* Discard any allocation-unit padding in the tail. */ - return set_copy_on_write_file_size(dst, size); - } + data.ByteCount.QuadPart = + DIV_ROUND_UP(size - data.SourceFileOffset.QuadPart, alignment) * + alignment; + if (!DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, + &data, sizeof(data), NULL, 0, + &bytes_returned, NULL)) { + errno = ENOSYS; + goto failed; } + /* Discard allocation-unit padding without zero-filling. */ + if (set_copy_on_write_file_size(dst, size) < 0) + goto failed; + return 0; - errno = ENOSYS; +failed: + /* A file-specific failure also disables later volume attempts. */ + saved_errno = errno; + copy_on_write_alignments[drive] = -1; + trace2_data_intmax("checkout", NULL, "copy_on_write_disabled", drive); + errno = saved_errno; return -1; } @@ -1273,6 +1367,7 @@ int mingw_chdir(const char *dirname) } result = _wchdir(normalize_ntpath(wdirname)); + copy_on_write_current_drive = -2; current_directory_len = GetCurrentDirectoryW(0, NULL); return result; } diff --git a/compat/mingw.h b/compat/mingw.h index 69560b2e1d6e4a..0d2b1dc6b910bd 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -8,7 +8,10 @@ int mingw_core_config(const char *var, const char *value, const struct config_context *ctx, void *cb); #define platform_core_config mingw_core_config #define copy_on_write_supported 1 -int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size); +int mingw_copy_on_write_supported(const char *path); +#define file_copy_on_write_supported mingw_copy_on_write_supported +int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, + const char *path); #define file_copy_on_write mingw_file_copy_on_write #ifndef NO_OPENSSL diff --git a/entry.c b/entry.c index 7a1e0d19366342..a2543f74c629d4 100644 --- a/entry.c +++ b/entry.c @@ -192,6 +192,9 @@ static int try_copy_on_write(const struct cache_entry *ce, int src_fd = -1, dst_fd = -1; int ret = 0; + if (!file_copy_on_write_supported(path)) + return 0; + strbuf_addf(&source, "%s/%s", state->copy_source->worktree, ce->name); src_fd = open_nofollow(source.buf, O_RDONLY); if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode) || @@ -201,7 +204,7 @@ static int try_copy_on_write(const struct cache_entry *ce, dst_fd = open_output_fd(path, ce, 0); if (dst_fd < 0) goto done; - if (file_copy_on_write(dst_fd, src_fd, st.st_size) || + if (file_copy_on_write(dst_fd, src_fd, st.st_size, path) || fstat(src_fd, &st_after) || !source_is_uptodate(state->copy_source, source_ce, &st_after)) { close(dst_fd); diff --git a/git-compat-util.h b/git-compat-util.h index 58fc0bbdeb91b7..7b0a15fffdcd9c 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -227,8 +227,12 @@ static inline int noop_core_config(const char *var UNUSED, #define copy_on_write_supported 0 #endif +#ifndef file_copy_on_write_supported +#define file_copy_on_write_supported(path) copy_on_write_supported +#endif + #ifndef file_copy_on_write -#define file_copy_on_write(dst_fd, src_fd, size) (errno = ENOSYS, -1) +#define file_copy_on_write(dst_fd, src_fd, size, path) (errno = ENOSYS, -1) #endif #ifndef has_dos_drive_prefix diff --git a/t/helper/test-copy-on-write.c b/t/helper/test-copy-on-write.c index fe740cf9ebf90a..0cb63d70192914 100644 --- a/t/helper/test-copy-on-write.c +++ b/t/helper/test-copy-on-write.c @@ -1,28 +1,55 @@ #include "test-tool.h" -int cmd__copy_on_write(int argc, const char **argv) +static int copy_one(const char *source, const char *target, int fail_cache) { struct stat st; int src_fd, dst_fd; int ret; - if (argc != 3) - return 1; - - src_fd = open(argv[1], O_RDONLY); + src_fd = open(source, O_RDONLY); if (src_fd < 0 || fstat(src_fd, &st)) return 1; - dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_EXCL, 0666); + if (fail_cache) { + /* A read-only destination forces a real resize failure. */ + ret = file_copy_on_write(src_fd, src_fd, st.st_size, source); + close(src_fd); + return !ret || file_copy_on_write_supported(target); + } + dst_fd = open(target, O_WRONLY | O_CREAT | O_EXCL, 0666); if (dst_fd < 0) { close(src_fd); return 1; } - ret = file_copy_on_write(dst_fd, src_fd, st.st_size); + ret = file_copy_on_write(dst_fd, src_fd, st.st_size, target); if (close(dst_fd)) ret = -1; close(src_fd); if (ret) - unlink(argv[2]); + unlink(target); return !!ret; } + +int cmd__copy_on_write(int argc, const char **argv) +{ + int i; + + if (argc >= 4 && !(argc % 2) && !strcmp(argv[1], "--probe-cwd")) { + for (i = 2; i < argc; i += 2) { + if (chdir(argv[i]) || + !!file_copy_on_write_supported("relative-file") != + !!atoi(argv[i + 1])) + return 1; + } + return 0; + } + if (argc == 4 && !strcmp(argv[1], "--fail-cache")) + return copy_one(argv[2], argv[3], 1); + if (argc >= 4 && !(argc % 2) && !strcmp(argv[1], "--batch")) { + for (i = 2; i < argc; i += 2) + if (copy_one(argv[i], argv[i + 1], 0)) + return 1; + return 0; + } + return argc == 3 ? copy_one(argv[1], argv[2], 0) : 1; +} diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 0d0043158ceebe..219df51f3923c7 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -34,6 +34,39 @@ test_expect_success COPY_ON_WRITE 'copy-on-write preserves sizes and source' ' done ' +test_expect_success MINGW,COPY_ON_WRITE 'clone alignment is cached' ' + test_when_finished "rm -f cache-source cache-one cache-two" && + test-tool genrandom clone-cache 102400 >cache-source && + GIT_TRACE2_EVENT="$PWD/alignment-trace" \ + test-tool copy-on-write --batch cache-source cache-one \ + cache-source cache-two && + test_cmp cache-source cache-one && + test_cmp cache-source cache-two && + grep "\"key\":\"copy_on_write_alignment\"" alignment-trace \ + >alignment-events && + test_line_count = 1 alignment-events && + test_grep ! "\"key\":\"copy_on_write_disabled\"" alignment-trace +' + +test_expect_success MINGW,COPY_ON_WRITE 'clone failure disables drive cache' ' + test_when_finished "rm -f cache-source cache-target cache-expect" && + test-tool genrandom clone-failure 65536 >cache-source && + cp cache-source cache-expect && + GIT_TRACE2_EVENT="$PWD/failure-trace" \ + test-tool copy-on-write --fail-cache cache-source \ + cache-target && + test_path_is_missing cache-target && + test_cmp cache-expect cache-source && + test_grep "\"key\":\"copy_on_write_disabled\"" failure-trace +' + +test_expect_success MINGW,COPY_ON_WRITE 'drive cache follows chdir' ' + test_when_finished "rm -rf cache-dir-one cache-dir-two" && + mkdir cache-dir-one cache-dir-two && + test-tool copy-on-write --probe-cwd "$PWD/cache-dir-one" 1 \ + "$PWD/cache-dir-two" 1 +' + test_expect_success 'setup' ' test_commit init ' From 8d84c23da790be6733d2ca5126d32ed8bd3bed2b Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Sun, 4 Oct 2026 19:49:36 +0200 Subject: [PATCH 24/28] Removal of useless allocation padding which hurt performance. It is not needed because proper file size was set on file creation. Signed-off-by: Tomas Kubes --- compat/mingw.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index be72a62e75be27..7f159511ccda34 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -183,9 +183,6 @@ int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, errno = ENOSYS; goto failed; } - /* Discard allocation-unit padding without zero-filling. */ - if (set_copy_on_write_file_size(dst, size) < 0) - goto failed; return 0; failed: From 0b65b8cafdd7b1c9fba52d8ef9be17abda191c1d Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Sun, 4 Oct 2026 20:32:25 +0200 Subject: [PATCH 25/28] optimized cache lookup function without tolower() ans simplified most often use case Signed-off-by: Tomas Kubes --- compat/mingw.c | 36 ++++++++++++++++++++++------------- t/helper/test-copy-on-write.c | 7 +++++++ t/t2400-worktree-add.sh | 12 ++++++++++++ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index 7f159511ccda34..a09adee69dc846 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -49,7 +49,7 @@ static int set_copy_on_write_file_size(HANDLE handle, off_t size) } /* Zero means unprobed; -1 disables cloning for this process. */ -static int copy_on_write_alignments[26]; +static int copy_on_write_alignments[256]; /* Re-resolve relative paths after mingw_chdir(). */ static int copy_on_write_current_drive = -2; @@ -95,9 +95,10 @@ static int resolve_copy_on_write_drive(const char *path) return drive == -2 ? resolve_copy_on_write_current_drive() : drive; } -static int copy_on_write_alignment(int drive) +static void set_copy_on_write_alignment(int drive, int alignment) { - return drive < 0 ? -1 : copy_on_write_alignments[drive]; + copy_on_write_alignments['A' + drive] = alignment; + copy_on_write_alignments['a' + drive] = alignment; } static int probe_copy_on_write_alignment(int drive) @@ -118,14 +119,22 @@ static int probe_copy_on_write_alignment(int drive) sectors * bytes : -1; } -static int prepare_copy_on_write_alignment(int drive) +static int prepare_copy_on_write_alignment(const char *path) { - int alignment = copy_on_write_alignment(drive); + int drive, alignment; + /* A cached drive-letter path needs no parsing or case conversion. */ + if (path[0] && path[1] == ':' && + (alignment = copy_on_write_alignments[(unsigned char)path[0]])) + return alignment; + drive = resolve_copy_on_write_drive(path); + if (drive < 0) + return -1; + alignment = copy_on_write_alignments['A' + drive]; if (alignment) return alignment; alignment = probe_copy_on_write_alignment(drive); - copy_on_write_alignments[drive] = alignment; + set_copy_on_write_alignment(drive, alignment); trace2_data_intmax("checkout", NULL, "copy_on_write_alignment", alignment); return alignment; @@ -133,9 +142,7 @@ static int prepare_copy_on_write_alignment(int drive) int mingw_copy_on_write_supported(const char *path) { - int drive = resolve_copy_on_write_drive(path); - - return prepare_copy_on_write_alignment(drive) > 0; + return prepare_copy_on_write_alignment(path) > 0; } int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, @@ -145,8 +152,7 @@ int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, .FileHandle = HCAST(HANDLE, _get_osfhandle(src_fd)), }; HANDLE dst = HCAST(HANDLE, _get_osfhandle(dst_fd)); - int drive = resolve_copy_on_write_drive(path); - int alignment = prepare_copy_on_write_alignment(drive); + int drive, alignment = prepare_copy_on_write_alignment(path); LONGLONG max_range; DWORD bytes_returned; int saved_errno; @@ -188,8 +194,12 @@ int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, failed: /* A file-specific failure also disables later volume attempts. */ saved_errno = errno; - copy_on_write_alignments[drive] = -1; - trace2_data_intmax("checkout", NULL, "copy_on_write_disabled", drive); + drive = resolve_copy_on_write_drive(path); + if (drive >= 0) { + set_copy_on_write_alignment(drive, -1); + trace2_data_intmax("checkout", NULL, + "copy_on_write_disabled", drive); + } errno = saved_errno; return -1; } diff --git a/t/helper/test-copy-on-write.c b/t/helper/test-copy-on-write.c index 0cb63d70192914..836c3173b8d04b 100644 --- a/t/helper/test-copy-on-write.c +++ b/t/helper/test-copy-on-write.c @@ -34,6 +34,13 @@ int cmd__copy_on_write(int argc, const char **argv) { int i; + if (argc >= 4 && !(argc % 2) && !strcmp(argv[1], "--probe-path")) { + for (i = 2; i < argc; i += 2) + if (!!file_copy_on_write_supported(argv[i]) != + !!atoi(argv[i + 1])) + return 1; + return 0; + } if (argc >= 4 && !(argc % 2) && !strcmp(argv[1], "--probe-cwd")) { for (i = 2; i < argc; i += 2) { if (chdir(argv[i]) || diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 219df51f3923c7..ed60638c82387a 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -48,6 +48,18 @@ test_expect_success MINGW,COPY_ON_WRITE 'clone alignment is cached' ' test_grep ! "\"key\":\"copy_on_write_disabled\"" alignment-trace ' +test_expect_success MINGW,COPY_ON_WRITE 'drive-letter cache accepts both cases' ' + drive=$(cygpath -m "$PWD" | cut -c1) && + upper=$(printf %s "$drive" | tr "[:lower:]" "[:upper:]") && + lower=$(printf %s "$drive" | tr "[:upper:]" "[:lower:]") && + GIT_TRACE2_EVENT="$PWD/case-trace" \ + test-tool copy-on-write --probe-path \ + "$upper:/clone-probe" 1 "$lower:/clone-probe" 1 && + grep "\"key\":\"copy_on_write_alignment\"" case-trace \ + >case-events && + test_line_count = 1 case-events +' + test_expect_success MINGW,COPY_ON_WRITE 'clone failure disables drive cache' ' test_when_finished "rm -f cache-source cache-target cache-expect" && test-tool genrandom clone-failure 65536 >cache-source && From 69712da91c6af098f69428197064f763a1d6a08d Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Mon, 5 Oct 2026 00:58:01 +0200 Subject: [PATCH 26/28] performance optimization, minimizing system calls Signed-off-by: Tomas Kubes --- compat/mingw.c | 81 +++++++++++++++++++++++++++-------- compat/mingw.h | 1 + entry.c | 4 +- t/helper/test-copy-on-write.c | 10 +++++ t/t2400-worktree-add.sh | 8 ++++ wrapper.c | 18 ++++++++ wrapper.h | 1 + 7 files changed, 103 insertions(+), 20 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index a09adee69dc846..73501ae94f588f 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -37,11 +37,11 @@ static int set_copy_on_write_file_size(HANDLE handle, off_t size) { - LARGE_INTEGER end; + FILE_END_OF_FILE_INFO end; - end.QuadPart = size; - if (!SetFilePointerEx(handle, end, NULL, FILE_BEGIN) || - !SetEndOfFile(handle)) { + end.EndOfFile.QuadPart = size; + if (!SetFileInformationByHandle(handle, FileEndOfFileInfo, + &end, sizeof(end))) { errno = err_win_to_posix(GetLastError()); return -1; } @@ -1586,31 +1586,76 @@ int mingw_lstat(const char *file_name, struct stat *buf) int (*lstat)(const char *file_name, struct stat *buf) = mingw_lstat; -static int get_file_info_by_handle(HANDLE hnd, struct stat *buf) +static void fill_stat_from_file_info(HANDLE hnd, + const BY_HANDLE_FILE_INFORMATION *fdata, + struct stat *buf) { - BY_HANDLE_FILE_INFORMATION fdata; - - if (!GetFileInformationByHandle(hnd, &fdata)) { - errno = err_win_to_posix(GetLastError()); - return -1; - } - buf->st_ino = 0; buf->st_gid = 0; buf->st_uid = 0; buf->st_nlink = 1; - buf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes, 0, NULL); - buf->st_size = fdata.nFileSizeLow | - (((off_t)fdata.nFileSizeHigh)<<32); + buf->st_mode = file_attr_to_st_mode(fdata->dwFileAttributes, 0, NULL); + buf->st_size = fdata->nFileSizeLow | + (((off_t)fdata->nFileSizeHigh)<<32); buf->st_dev = buf->st_rdev = 0; /* not used by Git */ - filetime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim)); - filetime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim)); - filetime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim)); + filetime_to_timespec(&(fdata->ftLastAccessTime), &(buf->st_atim)); + filetime_to_timespec(&(fdata->ftLastWriteTime), &(buf->st_mtim)); + filetime_to_timespec(&(fdata->ftCreationTime), &(buf->st_ctim)); if (are_wsl_compatible_mode_bits_enabled()) get_wsl_mode_bits_by_handle(hnd, &buf->st_mode); +} + +static int get_file_info_by_handle(HANDLE hnd, struct stat *buf) +{ + BY_HANDLE_FILE_INFORMATION fdata; + + if (!GetFileInformationByHandle(hnd, &fdata)) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + fill_stat_from_file_info(hnd, &fdata, buf); return 0; } +int mingw_open_nofollow_stat(const char *path, struct stat *st) +{ + wchar_t wpath[MAX_LONG_PATH]; + BY_HANDLE_FILE_INFORMATION info; + HANDLE handle; + int fd; + + if (!is_valid_win32_path(path, 0)) { + errno = ENOENT; + return -1; + } + if (xutftowcs_long_path(wpath, path) < 0) + return -1; + /* Inspect the opened object so replacement of the path cannot fool us. */ + handle = CreateFileW(wpath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, + NULL); + if (handle == INVALID_HANDLE_VALUE) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + if (!GetFileInformationByHandle(handle, &info)) { + errno = err_win_to_posix(GetLastError()); + goto failed; + } + if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { + errno = ELOOP; + goto failed; + } + fill_stat_from_file_info(handle, &info, st); + fd = _open_osfhandle((intptr_t)handle, O_RDONLY | O_BINARY); + if (fd >= 0) + return fd; +failed: + CloseHandle(handle); + return -1; +} + int mingw_stat(const char *file_name, struct stat *buf) { wchar_t wfile_name[MAX_LONG_PATH]; diff --git a/compat/mingw.h b/compat/mingw.h index 0d2b1dc6b910bd..718207dc832aef 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -13,6 +13,7 @@ int mingw_copy_on_write_supported(const char *path); int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, const char *path); #define file_copy_on_write mingw_file_copy_on_write +int mingw_open_nofollow_stat(const char *path, struct stat *st); #ifndef NO_OPENSSL #include diff --git a/entry.c b/entry.c index a2543f74c629d4..4a38217feff463 100644 --- a/entry.c +++ b/entry.c @@ -196,8 +196,8 @@ static int try_copy_on_write(const struct cache_entry *ce, return 0; strbuf_addf(&source, "%s/%s", state->copy_source->worktree, ce->name); - src_fd = open_nofollow(source.buf, O_RDONLY); - if (src_fd < 0 || fstat(src_fd, &st) || !S_ISREG(st.st_mode) || + src_fd = open_nofollow_stat(source.buf, &st); + if (src_fd < 0 || !S_ISREG(st.st_mode) || !source_is_uptodate(state->copy_source, source_ce, &st)) goto done; diff --git a/t/helper/test-copy-on-write.c b/t/helper/test-copy-on-write.c index 836c3173b8d04b..30a84828aae3e0 100644 --- a/t/helper/test-copy-on-write.c +++ b/t/helper/test-copy-on-write.c @@ -33,6 +33,16 @@ static int copy_one(const char *source, const char *target, int fail_cache) int cmd__copy_on_write(int argc, const char **argv) { int i; + struct stat st; + int fd; + + if (argc == 3 && !strcmp(argv[1], "--open-nofollow-stat")) { + fd = open_nofollow_stat(argv[2], &st); + if (fd < 0) + return 1; + close(fd); + return !S_ISREG(st.st_mode); + } if (argc >= 4 && !(argc % 2) && !strcmp(argv[1], "--probe-path")) { for (i = 2; i < argc; i += 2) diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index ed60638c82387a..71a41a038ff947 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -34,6 +34,14 @@ test_expect_success COPY_ON_WRITE 'copy-on-write preserves sizes and source' ' done ' +test_expect_success MINGW,SYMLINKS,COPY_ON_WRITE 'source open rejects symlinks' ' + test_when_finished "rm -f clone-regular clone-link" && + echo content >clone-regular && + ln -s clone-regular clone-link && + test-tool copy-on-write --open-nofollow-stat clone-regular && + test_must_fail test-tool copy-on-write --open-nofollow-stat clone-link +' + test_expect_success MINGW,COPY_ON_WRITE 'clone alignment is cached' ' test_when_finished "rm -f cache-source cache-one cache-two" && test-tool genrandom clone-cache 102400 >cache-source && diff --git a/wrapper.c b/wrapper.c index 561f9ee9c99fc1..89f71ef80e8be8 100644 --- a/wrapper.c +++ b/wrapper.c @@ -860,6 +860,24 @@ int open_nofollow(const char *path, int flags) #endif } +int open_nofollow_stat(const char *path, struct stat *st) +{ +#ifdef GIT_WINDOWS_NATIVE + return mingw_open_nofollow_stat(path, st); +#else + int fd = open_nofollow(path, O_RDONLY); + + if (fd >= 0 && fstat(fd, st)) { + int saved_errno = errno; + + close(fd); + errno = saved_errno; + return -1; + } + return fd; +#endif +} + int csprng_bytes(void *buf, size_t len, MAYBE_UNUSED unsigned flags) { #if defined(HAVE_ARC4RANDOM) || defined(HAVE_ARC4RANDOM_LIBBSD) diff --git a/wrapper.h b/wrapper.h index a6287d7f4d11be..39dfae16740dcc 100644 --- a/wrapper.h +++ b/wrapper.h @@ -138,6 +138,7 @@ int warn_on_fopen_errors(const char *path); * simultaneously create paths. */ int open_nofollow(const char *path, int flags); +int open_nofollow_stat(const char *path, struct stat *st); void sleep_millisec(int millisec); From 9aac5964cab974badd8782d359d775e31a8142bb Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Mon, 5 Oct 2026 07:07:04 +0200 Subject: [PATCH 27/28] performance optimization - minimizing system calls, some checks are skipped if worktree is created in empty folder Signed-off-by: Tomas Kubes --- builtin/reset.c | 15 +++++++ compat/mingw.c | 36 +++++++++++++++++ compat/mingw.h | 1 + entry.c | 73 +++++++++++++++++++++++++++-------- entry.h | 4 +- t/helper/test-copy-on-write.c | 9 +++++ t/t2400-worktree-add.sh | 13 +++++++ unpack-trees.c | 1 + 8 files changed, 135 insertions(+), 17 deletions(-) diff --git a/builtin/reset.c b/builtin/reset.c index eafffc8a908f77..96ae27286f78f5 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -368,6 +368,21 @@ static int init_copy_source(struct checkout_copy_source *source, source->istate = source_index; source->worktree = worktree; source->refreshed_at = refreshed_at; + /* The linked worktree is created before reset, so it contains .git. */ + { + DIR *dir = opendir(repo_get_work_tree(the_repository)); + struct dirent *de; + + if (dir) { + source->fresh_worktree = 1; + while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) + if (strcmp(de->d_name, ".git")) { + source->fresh_worktree = 0; + break; + } + closedir(dir); + } + } return 1; } diff --git a/compat/mingw.c b/compat/mingw.c index 73501ae94f588f..e9f1d7ab5b2f0e 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -1656,6 +1656,42 @@ int mingw_open_nofollow_stat(const char *path, struct stat *st) return -1; } +int mingw_create_file_nofollow(const char *path, unsigned int mode) +{ + wchar_t wpath[MAX_LONG_PATH]; + HANDLE handle; + int fd; + + if (!is_valid_win32_path(path, 0)) { + errno = EINVAL; + return -1; + } + if (xutftowcs_long_path(wpath, path) < 0) + return -1; + /* CREATE_NEW must reject even a dangling reparse point. */ + handle = CreateFileW(wpath, GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, CREATE_NEW, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, + NULL); + if (handle == INVALID_HANDLE_VALUE) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + fd = _open_osfhandle((intptr_t)handle, O_WRONLY | O_BINARY); + if (fd < 0) { + CloseHandle(handle); + return -1; + } + if (are_wsl_compatible_mode_bits_enabled()) { + _mode_t wsl_mode = S_IFREG | (mode & 0777); + set_wsl_mode_bits_by_handle((HANDLE)_get_osfhandle(fd), wsl_mode); + } + if (needs_hiding(path) && set_hidden_flag(wpath, 1)) + warning("could not mark '%s' as hidden.", path); + return fd; +} + int mingw_stat(const char *file_name, struct stat *buf) { wchar_t wfile_name[MAX_LONG_PATH]; diff --git a/compat/mingw.h b/compat/mingw.h index 718207dc832aef..3a64cb92ed508a 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -14,6 +14,7 @@ int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, const char *path); #define file_copy_on_write mingw_file_copy_on_write int mingw_open_nofollow_stat(const char *path, struct stat *st); +int mingw_create_file_nofollow(const char *path, unsigned int mode); #ifndef NO_OPENSSL #include diff --git a/entry.c b/entry.c index 4a38217feff463..0cd062b44ee8b7 100644 --- a/entry.c +++ b/entry.c @@ -98,9 +98,13 @@ static void remove_subtree(struct strbuf *path) die_errno("cannot rmdir '%s'", path->buf); } -static int create_file(const char *path, unsigned int mode) +static int create_file(const char *path, unsigned int mode, int fresh_worktree) { mode = (mode & 0100) ? 0777 : 0666; +#ifdef GIT_WINDOWS_NATIVE + if (fresh_worktree) + return mingw_create_file_nofollow(path, mode); +#endif return open(path, O_WRONLY | O_CREAT | O_EXCL, mode); } @@ -118,7 +122,8 @@ void *read_blob_entry(const struct cache_entry *ce, size_t *size) return NULL; } -static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempfile) +static int open_output_fd(char *path, const struct cache_entry *ce, + const struct checkout *state, int to_tempfile) { int symlink = (ce->ce_mode & S_IFMT) != S_IFREG; if (to_tempfile) { @@ -126,7 +131,8 @@ static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempf symlink ? ".merge_link_XXXXXX" : ".merge_file_XXXXXX"); return mkstemp(path); } else { - return create_file(path, !symlink ? ce->ce_mode : 0666); + return create_file(path, !symlink ? ce->ce_mode : 0666, + state->fresh_worktree && !symlink); } } @@ -201,9 +207,14 @@ static int try_copy_on_write(const struct cache_entry *ce, !source_is_uptodate(state->copy_source, source_ce, &st)) goto done; - dst_fd = open_output_fd(path, ce, 0); - if (dst_fd < 0) + dst_fd = open_output_fd(path, ce, state, 0); + if (dst_fd < 0) { + if (state->fresh_worktree) { + ret = -1; + goto done; + } goto done; + } if (file_copy_on_write(dst_fd, src_fd, st.st_size, path) || fstat(src_fd, &st_after) || !source_is_uptodate(state->copy_source, source_ce, &st_after)) { @@ -252,7 +263,7 @@ static int streaming_write_entry(const struct cache_entry *ce, char *path, int result = 0; int fd; - fd = open_output_fd(path, ce, to_tempfile); + fd = open_output_fd(path, ce, state, to_tempfile); if (fd < 0) return -1; @@ -422,10 +433,14 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca if (ce_mode_s_ifmt == S_IFREG) { struct stream_filter *filter; - if (source_ce && - try_copy_on_write(ce, source_ce, path, state, - &fstat_done, &st)) - goto finish; + if (source_ce) { + int cloned = try_copy_on_write(ce, source_ce, path, state, + &fstat_done, &st); + if (cloned < 0) + return -2; /* retry after checking a collision */ + if (cloned) + goto finish; + } filter = get_stream_filter_ca(ca, &ce->oid); if (filter && @@ -505,9 +520,11 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca */ write_file_entry: - fd = open_output_fd(path, ce, to_tempfile); + fd = open_output_fd(path, ce, state, to_tempfile); if (fd < 0) { free(new_blob); + if (state->fresh_worktree) + return -2; return error_errno("unable to create file %s", path); } @@ -617,6 +634,9 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, struct stat st; struct conv_attrs ca_buf; struct cache_entry *source_ce = NULL; + struct checkout slow_state; + const struct checkout *write_state = state; + int fast_path = 0, ret; if (ce->ce_flags & CE_WT_REMOVE) { if (topath) @@ -642,7 +662,18 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, strbuf_add(&path, state->base_dir, state->base_dir_len); strbuf_add(&path, ce->name, ce_namelen(ce)); - if (!check_path(path.buf, path.len, &st, state->base_dir_len)) { + if (state->fresh_worktree && S_ISREG(ce->ce_mode)) { + if (!ca) { + convert_attrs(state->istate, &ca_buf, ce->name); + ca = &ca_buf; + } + source_ce = copy_source_entry(ce, ca, state); + fast_path = !!source_ce; + } + +slow_path: + if (!fast_path && + !check_path(path.buf, path.len, &st, state->base_dir_len)) { const struct submodule *sub; unsigned changed = ie_match_stat(state->istate, ce, &st, CE_MATCH_IGNORE_VALID | CE_MATCH_IGNORE_SKIP_WORKTREE); @@ -708,7 +739,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, remove_subtree(&path); } else if (unlink(path.buf)) return error_errno("unable to unlink old '%s'", path.buf); - } else if (state->not_new) + } else if (!fast_path && state->not_new) return 0; create_directories(path.buf, path.len, state); @@ -718,12 +749,22 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - source_ce = copy_source_entry(ce, ca, state); + if (!source_ce) + source_ce = copy_source_entry(ce, ca, state); if (!source_ce && !enqueue_checkout(ce, ca, nr_checkouts)) return 0; - return write_entry(ce, path.buf, ca, source_ce, state, 0, - nr_checkouts); + ret = write_entry(ce, path.buf, ca, source_ce, write_state, 0, + nr_checkouts); + if (ret == -2 && fast_path) { + /* Resolve any failed create with the original path checks. */ + fast_path = 0; + slow_state = *state; + slow_state.fresh_worktree = 0; + write_state = &slow_state; + goto slow_path; + } + return ret; } void unlink_entry(const struct cache_entry *ce, const char *super_prefix) diff --git a/entry.h b/entry.h index c24c353f5ba22c..48585ef4393499 100644 --- a/entry.h +++ b/entry.h @@ -10,6 +10,7 @@ struct checkout_copy_source { struct index_state *istate; const char *worktree; uint64_t refreshed_at; + int fresh_worktree; }; struct checkout { @@ -24,7 +25,8 @@ struct checkout { quiet:1, not_new:1, clone:1, - refresh_cache:1; + refresh_cache:1, + fresh_worktree:1; }; #define CHECKOUT_INIT { .base_dir = "" } diff --git a/t/helper/test-copy-on-write.c b/t/helper/test-copy-on-write.c index 30a84828aae3e0..ae6fc164e0b924 100644 --- a/t/helper/test-copy-on-write.c +++ b/t/helper/test-copy-on-write.c @@ -36,6 +36,15 @@ int cmd__copy_on_write(int argc, const char **argv) struct stat st; int fd; +#ifdef GIT_WINDOWS_NATIVE + if (argc == 3 && !strcmp(argv[1], "--create-nofollow")) { + fd = mingw_create_file_nofollow(argv[2], 0666); + if (fd < 0) + return 1; + return !!close(fd); + } +#endif + if (argc == 3 && !strcmp(argv[1], "--open-nofollow-stat")) { fd = open_nofollow_stat(argv[2], &st); if (fd < 0) diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 71a41a038ff947..325031ee549961 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -42,6 +42,19 @@ test_expect_success MINGW,SYMLINKS,COPY_ON_WRITE 'source open rejects symlinks' test_must_fail test-tool copy-on-write --open-nofollow-stat clone-link ' +test_expect_success MINGW 'exclusive nofollow creation rejects existing files' ' + test_when_finished "rm -f exclusive-target" && + test-tool copy-on-write --create-nofollow exclusive-target && + test_must_fail test-tool copy-on-write --create-nofollow exclusive-target +' + +test_expect_success MINGW,SYMLINKS 'exclusive creation rejects dangling symlinks' ' + test_when_finished "rm -f exclusive-link exclusive-missing" && + ln -s exclusive-missing exclusive-link && + test_must_fail test-tool copy-on-write --create-nofollow exclusive-link && + test_path_is_missing exclusive-missing +' + test_expect_success MINGW,COPY_ON_WRITE 'clone alignment is cached' ' test_when_finished "rm -f cache-source cache-one cache-two" && test-tool genrandom clone-cache 102400 >cache-source && diff --git a/unpack-trees.c b/unpack-trees.c index cbde1070fe1686..c86b8038c3a42b 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -438,6 +438,7 @@ static int check_updates(struct unpack_trees_options *o, state.refresh_cache = 1; state.istate = index; state.copy_source = o->copy_source; + state.fresh_worktree = o->copy_source && o->copy_source->fresh_worktree; clone_checkout_metadata(&state.meta, &o->meta, NULL); if (!o->update || o->dry_run) { From 93e35689d9830083c731109b332be93637c3e6b6 Mon Sep 17 00:00:00 2001 From: Tomas Kubes Date: Mon, 5 Oct 2026 18:29:25 +0200 Subject: [PATCH 28/28] cloning is done only for files over 160KB by default, last unaligned segment is not cloned because cloning not aligned segment is slow, it is rather copied from source. Fixed and optimized check for source-clone for git repositories with autocrlf=false where cloning was not executed at all (facebook/react, microsoft/vscode, kubernetes/kubernetes). Signed-off-by: Tomas Kubes --- builtin/worktree.c | 32 ++++---- entry.c | 91 +++++++++++++++++++++-- t/t2400-worktree-add.sh | 160 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 257 insertions(+), 26 deletions(-) diff --git a/builtin/worktree.c b/builtin/worktree.c index d795cb8909c89e..d59916107a7685 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -409,24 +409,20 @@ static int checkout_worktree(const struct add_opts *opts, if (file_copy_on_write_supported(path) && source && repo_read_index(the_repository) >= 0) { gettimeofday(&refreshed_at, NULL); - if (repo_refresh_and_write_index(the_repository, - REFRESH_QUIET | REFRESH_REALLY, - SKIP_IF_UNCHANGED, 1, - NULL, NULL, NULL) >= 0) { - strvec_pushf(&cp.env, "%s=%s", - GIT_WORKTREE_COPY_SOURCE, source); - strvec_pushf(&cp.env, "%s=%s", - GIT_WORKTREE_COPY_SOURCE_INDEX, - absolute_path(repo_get_index_file(the_repository))); - strvec_pushf(&cp.env, "%s=%s", - GIT_WORKTREE_COPY_SOURCE_GIT_DIR, - absolute_path(repo_get_git_dir(the_repository))); - strvec_pushf(&cp.env, "%s=%"PRIuMAX, - GIT_WORKTREE_COPY_SOURCE_TIME, - (uintmax_t)refreshed_at.tv_sec * - 1000000000 + - refreshed_at.tv_usec * 1000); - } + + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE, source); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE_INDEX, + absolute_path(repo_get_index_file(the_repository))); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE_GIT_DIR, + absolute_path(repo_get_git_dir(the_repository))); + strvec_pushf(&cp.env, "%s=%"PRIuMAX, + GIT_WORKTREE_COPY_SOURCE_TIME, + (uintmax_t)refreshed_at.tv_sec * + 1000000000 + + refreshed_at.tv_usec * 1000); } return run_command(&cp); } diff --git a/entry.c b/entry.c index 0cd062b44ee8b7..c7a3b4985b7754 100644 --- a/entry.c +++ b/entry.c @@ -2,6 +2,7 @@ #include "git-compat-util.h" #include "odb.h" +#include "object-file.h" #include "odb/streaming.h" #include "dir.h" #include "environment.h" @@ -147,6 +148,8 @@ static int conversion_attrs_equal(const struct conv_attrs *a, !strcmp(a->working_tree_encoding, b->working_tree_encoding)); } +#define COPY_ON_WRITE_MIN_BYTES (160 * 1024) + static struct cache_entry *copy_source_entry(const struct cache_entry *ce, const struct conv_attrs *ca, const struct checkout *state) @@ -158,7 +161,7 @@ static struct cache_entry *copy_source_entry(const struct cache_entry *ce, if (!state->copy_source || !ca || !S_ISREG(ce->ce_mode) || ca->drv || ca->ident || ca->working_tree_encoding || - ca->crlf_action != CRLF_BINARY) + (ca->crlf_action != CRLF_BINARY && auto_crlf != AUTO_CRLF_FALSE)) return NULL; istate = state->copy_source->istate; @@ -167,6 +170,7 @@ static struct cache_entry *copy_source_entry(const struct cache_entry *ce, return NULL; source = istate->cache[pos]; if (!S_ISREG(source->ce_mode) || ce_skip_worktree(source) || + source->ce_stat_data.sd_size <= COPY_ON_WRITE_MIN_BYTES || !oideq(&source->oid, &ce->oid)) return NULL; convert_attrs(istate, &source_ca, ce->name); @@ -188,8 +192,79 @@ static int source_is_uptodate(const struct checkout_copy_source *source, return mtime < source->refreshed_at; } +/* Verify only clone candidates instead of refreshing the entire source index. + * Matching non-racy stat data is the index's clean-file guarantee; racy files + * require an object hash. EOL conversion must also reproduce the exact bytes. */ +static int clone_source_matches(int fd, off_t source_size, + const struct cache_entry *ce, + const struct conv_attrs *ca, + const struct checkout *state) +{ + struct strbuf raw = STRBUF_INIT, normalized = STRBUF_INIT; + struct strbuf checkout = STRBUF_INIT; + struct object_id oid; + struct object_info oi = OBJECT_INFO_INIT; + enum object_type type; + size_t blob_size, len; + const char *data; + int racy = is_racy_timestamp(state->copy_source->istate, ce); + int have_size = 0, matches = 0; + + if (ca->crlf_action == CRLF_BINARY && !racy) + return 1; + if (!racy) { + oi.typep = &type; + oi.sizep = &blob_size; + have_size = !odb_read_object_info_extended(the_repository->objects, + &ce->oid, &oi, OBJECT_INFO_LOOKUP_REPLACE | + OBJECT_INFO_SKIP_FETCH_OBJECT) && type == OBJ_BLOB; + /* LF checkout does not expand data, and clean EOL normalization + * only shrinks it. Equal sizes therefore prove identity. */ + if (have_size && (ca->crlf_action == CRLF_TEXT_INPUT || + ca->crlf_action == CRLF_AUTO_INPUT || + (ca->crlf_action == CRLF_AUTO && core_eol == EOL_LF))) + return blob_size == source_size; + } + if (strbuf_read(&raw, fd, source_size) < 0) + goto done; + data = raw.buf; + len = raw.len; + if (racy || !have_size) { + hash_object_file(the_repository->hash_algo, data, len, OBJ_BLOB, &oid); + if (oideq(&oid, &ce->oid)) + goto checkout; + } + if (ca->crlf_action != CRLF_BINARY && + (!have_size || len != blob_size || racy) && + convert_to_git(state->copy_source->istate, ce->name, + raw.buf, raw.len, &normalized, CONV_EOL_RENORMALIZE)) { + data = normalized.buf; + len = normalized.len; + } else if (racy || !have_size) { + goto done; + } + if (racy || !have_size) { + hash_object_file(the_repository->hash_algo, data, len, OBJ_BLOB, &oid); + if (!oideq(&oid, &ce->oid)) + goto done; + } else if (len != blob_size) { + goto done; + } +checkout: + if (convert_to_working_tree_ca(ca, ce->name, data, len, &checkout, NULL)) { + data = checkout.buf; + len = checkout.len; + } + matches = len == raw.len && !memcmp(data, raw.buf, len); +done: + strbuf_release(&checkout); + strbuf_release(&normalized); + strbuf_release(&raw); + return matches; +} static int try_copy_on_write(const struct cache_entry *ce, const struct cache_entry *source_ce, char *path, + const struct conv_attrs *ca, const struct checkout *state, int *fstat_done, struct stat *statbuf) { @@ -204,7 +279,9 @@ static int try_copy_on_write(const struct cache_entry *ce, strbuf_addf(&source, "%s/%s", state->copy_source->worktree, ce->name); src_fd = open_nofollow_stat(source.buf, &st); if (src_fd < 0 || !S_ISREG(st.st_mode) || - !source_is_uptodate(state->copy_source, source_ce, &st)) + st.st_size <= COPY_ON_WRITE_MIN_BYTES || + !source_is_uptodate(state->copy_source, source_ce, &st) || + !clone_source_matches(src_fd, st.st_size, source_ce, ca, state)) goto done; dst_fd = open_output_fd(path, ce, state, 0); @@ -434,7 +511,7 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca struct stream_filter *filter; if (source_ce) { - int cloned = try_copy_on_write(ce, source_ce, path, state, + int cloned = try_copy_on_write(ce, source_ce, path, ca, state, &fstat_done, &st); if (cloned < 0) return -2; /* retry after checking a collision */ @@ -636,7 +713,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, struct cache_entry *source_ce = NULL; struct checkout slow_state; const struct checkout *write_state = state; - int fast_path = 0, ret; + int fast_path = 0, source_checked = 0, ret; if (ce->ce_flags & CE_WT_REMOVE) { if (topath) @@ -668,7 +745,9 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } source_ce = copy_source_entry(ce, ca, state); - fast_path = !!source_ce; + source_checked = 1; + fast_path = source_ce || (!ca->drv && state->force && + !state->not_new && parallel_checkout_status() != PC_RUNNING); } slow_path: @@ -749,7 +828,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - if (!source_ce) + if (!source_checked) source_ce = copy_source_entry(ce, ca, state); if (!source_ce && !enqueue_checkout(ce, ca, nr_checkouts)) return 0; diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh index 325031ee549961..ddfc84d109620d 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -166,11 +166,12 @@ test_expect_success COPY_ON_WRITE 'worktree add uses copy-on-write' ' test_when_finished "git reset --hard $old_head" && test_when_finished "git worktree remove --force copy-on-write" && echo "*.crlf text eol=crlf" >.gitattributes && - echo clean >clean && + test-tool genrandom clone-above-threshold 163841 >clean && + test-tool genrandom clone-at-threshold 163840 >boundary && echo clean >clean.crlf && echo clean >modified.crlf && echo clean >unconverted.crlf && - git add .gitattributes clean clean.crlf modified.crlf unconverted.crlf && + git add .gitattributes clean boundary clean.crlf modified.crlf unconverted.crlf && git commit -m "copy-on-write test files" && rm clean.crlf modified.crlf && git checkout -- clean.crlf modified.crlf && @@ -179,6 +180,7 @@ test_expect_success COPY_ON_WRITE 'worktree add uses copy-on-write' ' GIT_TRACE2_EVENT="$PWD/trace" \ git worktree add --detach copy-on-write main && test_grep "\"key\":\"copy_on_write\",\"value\":\"clean\"" trace && + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"boundary\"" trace && test_grep ! "\"key\":\"copy_on_write\",\"value\":\"clean.crlf\"" trace && test_grep ! "\"key\":\"copy_on_write\",\"value\":\"unconverted.crlf\"" trace && test_grep ! "\"value\":\"init.t\"" trace && @@ -189,6 +191,160 @@ test_expect_success COPY_ON_WRITE 'worktree add uses copy-on-write' ' test_cmp expect-crlf copy-on-write/unconverted.crlf ' +test_expect_success COPY_ON_WRITE 'autocrlf=false clones only the exact checkout representation' ' + test_create_repo eol-source && + ( + cd eol-source && + git config core.autocrlf false && + git config core.eol lf && + echo "* text=auto" >.gitattributes && + awk "BEGIN { for (i = 0; i < 20000; i++) print \"line endings\" }" >text && + git add .gitattributes text && + git commit -m "large automatic text" && + cp text ../expected-lf && + awk "{ printf \"%s\\r\\n\", \$0 }" ../expected-lf >../expected-crlf && + for eol in lf crlf + do + git config core.eol "$eol" && + cp ../expected-$eol text && + git add text && + git diff --cached --exit-code && + rm -f ../eol-trace && + GIT_TRACE2_EVENT="$PWD/../eol-trace" \ + git worktree add --detach ../eol-clone HEAD && + test_grep "\"key\":\"copy_on_write\",\"value\":\"text\"" ../eol-trace && + test_cmp_bin ../expected-$eol ../eol-clone/text && + git -C ../eol-clone diff --exit-code && + git worktree remove --force ../eol-clone || return 1 + done && + for eol in lf crlf + do + git config core.eol "$eol" && + if test "$eol" = lf; then other=crlf; else other=lf; fi && + cp ../expected-$other text && + git add text && + git diff --cached --exit-code && + rm -f ../eol-trace && + GIT_TRACE2_EVENT="$PWD/../eol-trace" \ + git worktree add --detach ../eol-clone HEAD && + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"text\"" ../eol-trace && + test_cmp_bin ../expected-$eol ../eol-clone/text && + git -C ../eol-clone diff --exit-code && + git worktree remove --force ../eol-clone || return 1 + done + ) +' +test_expect_success COPY_ON_WRITE 'racy clone candidates are checked by content' ' + test_create_repo racy-source && + ( + cd racy-source && + git config core.autocrlf false && + git config core.trustctime false && + git config core.checkstat minimal && + echo "* -text" >.gitattributes && + test-tool genrandom racy-original 163841 >binary && + test-tool chmtime =-10 binary && + mtime=$(test-tool chmtime --get binary) && + cp binary ../expected-binary && + git add .gitattributes binary && + git commit -m "racy binary candidate" && + test-tool chmtime "=$((mtime - 1))" .git/index && + GIT_TRACE2_EVENT="$PWD/../racy-clean-trace" \ + git worktree add --detach ../racy-clone HEAD && + test_grep "\"key\":\"copy_on_write\",\"value\":\"binary\"" ../racy-clean-trace && + test_cmp_bin ../expected-binary ../racy-clone/binary && + git worktree remove --force ../racy-clone && + test-tool genrandom racy-modified 163841 >binary && + test-tool chmtime "=$mtime" binary && + GIT_TRACE2_EVENT="$PWD/../racy-dirty-trace" \ + git worktree add --detach ../racy-clone HEAD && + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"binary\"" ../racy-dirty-trace && + test_cmp_bin ../expected-binary ../racy-clone/binary && + git -C ../racy-clone diff --exit-code && + git worktree remove --force ../racy-clone + ) +' +test_expect_success COPY_ON_WRITE 'explicit CRLF overrides core.eol=lf with autocrlf=false' ' + test_create_repo explicit-eol-source && + ( + cd explicit-eol-source && + git config core.autocrlf false && + git config core.eol lf && + echo "* text eol=crlf" >.gitattributes && + awk "BEGIN { for (i = 0; i < 20000; i++) print \"line endings\" }" >text && + cp text ../explicit-lf && + awk "{ printf \"%s\\r\\n\", \$0 }" text >../explicit-crlf && + git add .gitattributes text && + git commit -m "explicit CRLF text" && + for eol in lf crlf + do + cp ../explicit-$eol text && + git add text && + git diff --cached --exit-code && + rm -f ../explicit-trace && + GIT_TRACE2_EVENT="$PWD/../explicit-trace" \ + git worktree add --detach ../explicit-clone HEAD && + if test "$eol" = crlf + then + test_grep "\"key\":\"copy_on_write\",\"value\":\"text\"" ../explicit-trace + else + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"text\"" ../explicit-trace + fi && + test_cmp_bin ../explicit-crlf ../explicit-clone/text && + git -C ../explicit-clone diff --exit-code && + git worktree remove --force ../explicit-clone || return 1 + done + ) +' + +test_expect_success COPY_ON_WRITE 'automatic text preserves legacy CRLF blobs' ' + test_create_repo legacy-eol-source && + ( + cd legacy-eol-source && + git config core.autocrlf false && + echo "* text=auto" >.gitattributes && + awk "BEGIN { for (i = 0; i < 20000; i++) printf \"line endings\\r\\n\" }" >text && + test-tool chmtime =-10 text && + git add .gitattributes && + oid=$(git hash-object -w --no-filters text) && + git update-index --add --cacheinfo "100644,$oid,text" && + git commit -m "legacy CRLF blob" && + git update-index --refresh && + for eol in lf crlf + do + git config core.eol "$eol" && + rm -f ../legacy-trace && + GIT_TRACE2_EVENT="$PWD/../legacy-trace" \ + git worktree add --detach ../legacy-clone HEAD && + test_grep "\"key\":\"copy_on_write\",\"value\":\"text\"" ../legacy-trace && + test_cmp_bin text ../legacy-clone/text && + git -C ../legacy-clone diff --exit-code && + git worktree remove --force ../legacy-clone || return 1 + done + ) +' +test_expect_success MINGW 'fresh worktree resolves a collision after a filtered file' ' + test_create_repo collision-source && + ( + cd collision-source && + git config core.autocrlf false && + git config core.ignorecase false && + git config core.fscache true && + git config filter.case.smudge cat && + echo "folder/Case.filtered filter=case" >.gitattributes && + git add .gitattributes && + first=$(printf "first\n" | git hash-object -w --stdin) && + second=$(printf "second\n" | git hash-object -w --stdin) && + git update-index --add --cacheinfo "100644,$first,folder/0-first" && + git update-index --add --cacheinfo "100644,$first,folder/Case.filtered" && + git update-index --add --cacheinfo "100644,$second,folder/case.filtered" && + git commit -m "case collision after filter" && + git worktree add --detach ../collision-target HEAD && + printf "second\n" >../expected-collision && + test_cmp_bin ../expected-collision ../collision-target/folder/case.filtered && + git worktree remove --force ../collision-target + ) +' test_expect_success COPY_ON_WRITE 'worktree.copyOnWrite disables optimization' ' test_when_finished "git worktree remove --force no-copy-on-write" && test_config worktree.copyOnWrite false &&