From 22bc067af90abfd69d6041779f3c78ffa1560980 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/18] Initial plan From 9940edd2e8f4b87352be2e86512d9c6441ee800a 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/18] 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 90959927c71c4ee3bcfe0b9b207581de664c15c3 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/18] 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 4cbcb87187085ff1e3f1be937a68eae1508486b7 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/18] 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 cdb2b1a024f29d0d8df0d99a57777b200362547e 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/18] 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 22988a194f4dbaedf888c2118b92c5811bd98a8a 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/18] 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 50ede482cd3cfd376f2b26a1d5814fab4fbca9ac 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/18] 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 210736858204d260b6378494c82022f4eb979e55 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/18] amend! compat: add a block-cloning primitive Co-authored-by: dscho <127790+dscho@users.noreply.github.com> --- 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 ecfc290a1e192e71ae08b27a7ede734604cc21eb 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/18] 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 484f7b72c6cc2ee3b52a362871cd7b69815c6253 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/18] 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 60cb518f524186f34f142845287d79bb8c73af09 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/18] 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 ff7e710d8f1063d845652e55504fbeafe7d81583 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/18] 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 d078954539192ddf5f1dfae569b0d1fe290f811b 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/18] 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 67237842468f366b4628b3d43fac3209f33349ac 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/18] 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 3c97d72df6f4617e9005fe25a27d85d408ca2d93 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/18] 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 aac02f0d83a4cbbaeebe9da9e67d51b71f8c8a01 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/18] 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 7de068da5decffa64bb6253d6faeca7bd915f2ba 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/18] 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 979e245fdfafff2f2a5ff5ce4b44ef7094b4ead5 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/18] 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)