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 66c26eab62fc12..96ae27286f78f5 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,7 +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) +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]; @@ -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.copy_source = copy_source; init_checkout_metadata(&opts.meta, ref, oid, NULL); if (!quiet) opts.verbose_update = 1; @@ -336,6 +339,53 @@ 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 || + !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); + 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; + /* The linked worktree is created before reset, so it contains .git. */ + { + DIR *dir = opendir(repo_get_work_tree(the_repository)); + struct dirent *de; + + if (dir) { + source->fresh_worktree = 1; + while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) + if (strcmp(de->d_name, ".git")) { + source->fresh_worktree = 0; + break; + } + closedir(dir); + } + } + return 1; +} + int cmd_reset(int argc, const char **argv, const char *prefix, @@ -343,6 +393,10 @@ 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; int patch_mode = 0, pathspec_file_nul = 0, unborn; const char *rev; char *pathspec_from_file = NULL; @@ -495,6 +549,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 @@ -535,9 +592,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, + copy_source_ptr); if (reset_type == KEEP && !err) - err = reset_index(ref, &oid, MIXED, quiet); + err = reset_index(ref, &oid, MIXED, quiet, + copy_source_ptr); if (err) die(_("Could not reset index file to revision '%s'."), rev); free(ref); @@ -561,6 +620,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 77ecd0f71f4247..d59916107a7685 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -395,14 +395,35 @@ 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 (file_copy_on_write_supported(path) && source && + repo_read_index(the_repository) >= 0) { + gettimeofday(&refreshed_at, NULL); + + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE, source); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE_INDEX, + absolute_path(repo_get_index_file(the_repository))); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORKTREE_COPY_SOURCE_GIT_DIR, + absolute_path(repo_get_git_dir(the_repository))); + strvec_pushf(&cp.env, "%s=%"PRIuMAX, + GIT_WORKTREE_COPY_SOURCE_TIME, + (uintmax_t)refreshed_at.tv_sec * + 1000000000 + + refreshed_at.tv_usec * 1000); + } return run_command(&cp); } @@ -589,7 +610,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 fd014eef498e55..e9f1d7ab5b2f0e 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -33,9 +33,177 @@ #include #define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056) - #define HCAST(type, handle) ((type)(intptr_t)handle) +static int set_copy_on_write_file_size(HANDLE handle, off_t size) +{ + FILE_END_OF_FILE_INFO end; + + end.EndOfFile.QuadPart = size; + if (!SetFileInformationByHandle(handle, FileEndOfFileInfo, + &end, sizeof(end))) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + return 0; +} + +/* Zero means unprobed; -1 disables cloning for this process. */ +static int copy_on_write_alignments[256]; +/* Re-resolve relative paths after mingw_chdir(). */ +static int copy_on_write_current_drive = -2; + +static int copy_on_write_drive(const char *path) +{ + int drive; + + if (is_dir_sep(path[0]) && is_dir_sep(path[1]) && + (path[2] == '?' || path[2] == '.') && is_dir_sep(path[3])) + path += 4; + if (has_dos_drive_prefix(path)) { + drive = tolower((unsigned char)path[0]) - 'a'; + return drive >= 0 && drive < 26 ? drive : -1; + } + /* UNC paths have no drive entry in this conservative cache. */ + if (is_dir_sep(path[0]) && is_dir_sep(path[1])) + return -1; + return copy_on_write_current_drive; +} + +static int resolve_copy_on_write_current_drive(void) +{ + WCHAR cwd[MAX_LONG_PATH]; + const WCHAR *current = cwd; + DWORD length; + int drive; + + length = GetCurrentDirectoryW(ARRAY_SIZE(cwd), cwd); + if (!length || length >= ARRAY_SIZE(cwd)) + return copy_on_write_current_drive = -1; + if (length > 5 && cwd[0] == L'\\' && cwd[1] == L'\\' && + (cwd[2] == L'?' || cwd[2] == L'.') && cwd[3] == L'\\') + current += 4; + drive = current[1] == L':' ? towlower(current[0]) - L'a' : -1; + copy_on_write_current_drive = drive >= 0 && drive < 26 ? drive : -1; + return copy_on_write_current_drive; +} + +static int resolve_copy_on_write_drive(const char *path) +{ + int drive = copy_on_write_drive(path); + + return drive == -2 ? resolve_copy_on_write_current_drive() : drive; +} + +static void set_copy_on_write_alignment(int drive, int alignment) +{ + copy_on_write_alignments['A' + drive] = alignment; + copy_on_write_alignments['a' + drive] = alignment; +} + +static int probe_copy_on_write_alignment(int drive) +{ + WCHAR root[] = L"A:\\", filesystem[16]; + DWORD sectors, bytes, free_clusters, clusters; + + root[0] += drive; + if (!GetVolumeInformationW(root, NULL, 0, NULL, NULL, NULL, + filesystem, ARRAY_SIZE(filesystem)) || + wcscmp(filesystem, L"ReFS") || + !GetDiskFreeSpaceW(root, §ors, &bytes, &free_clusters, + &clusters) || + !sectors || !bytes || sectors > 65536 / bytes) + return -1; + /* Only the cluster size is used; free-space results are ignored. */ + return sectors * bytes == 4096 || sectors * bytes == 65536 ? + sectors * bytes : -1; +} + +static int prepare_copy_on_write_alignment(const char *path) +{ + int drive, alignment; + + /* A cached drive-letter path needs no parsing or case conversion. */ + if (path[0] && path[1] == ':' && + (alignment = copy_on_write_alignments[(unsigned char)path[0]])) + return alignment; + drive = resolve_copy_on_write_drive(path); + if (drive < 0) + return -1; + alignment = copy_on_write_alignments['A' + drive]; + if (alignment) + return alignment; + alignment = probe_copy_on_write_alignment(drive); + set_copy_on_write_alignment(drive, alignment); + trace2_data_intmax("checkout", NULL, "copy_on_write_alignment", + alignment); + return alignment; +} + +int mingw_copy_on_write_supported(const char *path) +{ + return prepare_copy_on_write_alignment(path) > 0; +} + +int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, + const char *path) +{ + DUPLICATE_EXTENTS_DATA data = { + .FileHandle = HCAST(HANDLE, _get_osfhandle(src_fd)), + }; + HANDLE dst = HCAST(HANDLE, _get_osfhandle(dst_fd)); + int drive, alignment = prepare_copy_on_write_alignment(path); + LONGLONG max_range; + DWORD bytes_returned; + int saved_errno; + + if (alignment < 0) { + errno = ENOSYS; + return -1; + } + /* Ranges must be cluster-aligned and smaller than 4 GiB. */ + max_range = (4LL * 1024 * 1024 * 1024) - alignment; + if (set_copy_on_write_file_size(dst, size) < 0) + goto failed; + if (!size) + return 0; + + 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)) { + errno = ENOSYS; + goto failed; + } + data.SourceFileOffset.QuadPart += data.ByteCount.QuadPart; + data.TargetFileOffset = data.SourceFileOffset; + } + + data.ByteCount.QuadPart = + DIV_ROUND_UP(size - data.SourceFileOffset.QuadPart, alignment) * + alignment; + if (!DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE, + &data, sizeof(data), NULL, 0, + &bytes_returned, NULL)) { + errno = ENOSYS; + goto failed; + } + return 0; + +failed: + /* A file-specific failure also disables later volume attempts. */ + saved_errno = errno; + drive = resolve_copy_on_write_drive(path); + if (drive >= 0) { + set_copy_on_write_alignment(drive, -1); + trace2_data_intmax("checkout", NULL, + "copy_on_write_disabled", drive); + } + errno = saved_errno; + return -1; +} + void open_in_gdb(void) { static struct child_process cp = CHILD_PROCESS_INIT; @@ -1206,6 +1374,7 @@ int mingw_chdir(const char *dirname) } result = _wchdir(normalize_ntpath(wdirname)); + copy_on_write_current_drive = -2; current_directory_len = GetCurrentDirectoryW(0, NULL); return result; } @@ -1417,31 +1586,112 @@ int mingw_lstat(const char *file_name, struct stat *buf) int (*lstat)(const char *file_name, struct stat *buf) = mingw_lstat; -static int get_file_info_by_handle(HANDLE hnd, struct stat *buf) +static void fill_stat_from_file_info(HANDLE hnd, + const BY_HANDLE_FILE_INFORMATION *fdata, + struct stat *buf) { - BY_HANDLE_FILE_INFORMATION fdata; - - if (!GetFileInformationByHandle(hnd, &fdata)) { - errno = err_win_to_posix(GetLastError()); - return -1; - } - buf->st_ino = 0; buf->st_gid = 0; buf->st_uid = 0; buf->st_nlink = 1; - buf->st_mode = file_attr_to_st_mode(fdata.dwFileAttributes, 0, NULL); - buf->st_size = fdata.nFileSizeLow | - (((off_t)fdata.nFileSizeHigh)<<32); + buf->st_mode = file_attr_to_st_mode(fdata->dwFileAttributes, 0, NULL); + buf->st_size = fdata->nFileSizeLow | + (((off_t)fdata->nFileSizeHigh)<<32); buf->st_dev = buf->st_rdev = 0; /* not used by Git */ - filetime_to_timespec(&(fdata.ftLastAccessTime), &(buf->st_atim)); - filetime_to_timespec(&(fdata.ftLastWriteTime), &(buf->st_mtim)); - filetime_to_timespec(&(fdata.ftCreationTime), &(buf->st_ctim)); + filetime_to_timespec(&(fdata->ftLastAccessTime), &(buf->st_atim)); + filetime_to_timespec(&(fdata->ftLastWriteTime), &(buf->st_mtim)); + filetime_to_timespec(&(fdata->ftCreationTime), &(buf->st_ctim)); if (are_wsl_compatible_mode_bits_enabled()) get_wsl_mode_bits_by_handle(hnd, &buf->st_mode); +} + +static int get_file_info_by_handle(HANDLE hnd, struct stat *buf) +{ + BY_HANDLE_FILE_INFORMATION fdata; + + if (!GetFileInformationByHandle(hnd, &fdata)) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + fill_stat_from_file_info(hnd, &fdata, buf); return 0; } +int mingw_open_nofollow_stat(const char *path, struct stat *st) +{ + wchar_t wpath[MAX_LONG_PATH]; + BY_HANDLE_FILE_INFORMATION info; + HANDLE handle; + int fd; + + if (!is_valid_win32_path(path, 0)) { + errno = ENOENT; + return -1; + } + if (xutftowcs_long_path(wpath, path) < 0) + return -1; + /* Inspect the opened object so replacement of the path cannot fool us. */ + handle = CreateFileW(wpath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, + NULL); + if (handle == INVALID_HANDLE_VALUE) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + if (!GetFileInformationByHandle(handle, &info)) { + errno = err_win_to_posix(GetLastError()); + goto failed; + } + if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { + errno = ELOOP; + goto failed; + } + fill_stat_from_file_info(handle, &info, st); + fd = _open_osfhandle((intptr_t)handle, O_RDONLY | O_BINARY); + if (fd >= 0) + return fd; +failed: + CloseHandle(handle); + return -1; +} + +int mingw_create_file_nofollow(const char *path, unsigned int mode) +{ + wchar_t wpath[MAX_LONG_PATH]; + HANDLE handle; + int fd; + + if (!is_valid_win32_path(path, 0)) { + errno = EINVAL; + return -1; + } + if (xutftowcs_long_path(wpath, path) < 0) + return -1; + /* CREATE_NEW must reject even a dangling reparse point. */ + handle = CreateFileW(wpath, GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, CREATE_NEW, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, + NULL); + if (handle == INVALID_HANDLE_VALUE) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + fd = _open_osfhandle((intptr_t)handle, O_WRONLY | O_BINARY); + if (fd < 0) { + CloseHandle(handle); + return -1; + } + if (are_wsl_compatible_mode_bits_enabled()) { + _mode_t wsl_mode = S_IFREG | (mode & 0777); + set_wsl_mode_bits_by_handle((HANDLE)_get_osfhandle(fd), wsl_mode); + } + if (needs_hiding(path) && set_hidden_flag(wpath, 1)) + warning("could not mark '%s' as hidden.", path); + return fd; +} + int mingw_stat(const char *file_name, struct stat *buf) { wchar_t wfile_name[MAX_LONG_PATH]; diff --git a/compat/mingw.h b/compat/mingw.h index 379d7f5d079165..3a64cb92ed508a 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -7,6 +7,14 @@ 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 +#define copy_on_write_supported 1 +int mingw_copy_on_write_supported(const char *path); +#define file_copy_on_write_supported mingw_copy_on_write_supported +int mingw_file_copy_on_write(int dst_fd, int src_fd, off_t size, + const char *path); +#define file_copy_on_write mingw_file_copy_on_write +int mingw_open_nofollow_stat(const char *path, struct stat *st); +int mingw_create_file_nofollow(const char *path, unsigned int mode); #ifndef NO_OPENSSL #include diff --git a/entry.c b/entry.c index 5fd4b08991c6ed..c7a3b4985b7754 100644 --- a/entry.c +++ b/entry.c @@ -2,6 +2,7 @@ #include "git-compat-util.h" #include "odb.h" +#include "object-file.h" #include "odb/streaming.h" #include "dir.h" #include "environment.h" @@ -15,6 +16,8 @@ #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, const struct checkout *state) @@ -96,9 +99,13 @@ static void remove_subtree(struct strbuf *path) die_errno("cannot rmdir '%s'", path->buf); } -static int create_file(const char *path, unsigned int mode) +static int create_file(const char *path, unsigned int mode, int fresh_worktree) { mode = (mode & 0100) ? 0777 : 0666; +#ifdef GIT_WINDOWS_NATIVE + if (fresh_worktree) + return mingw_create_file_nofollow(path, mode); +#endif return open(path, O_WRONLY | O_CREAT | O_EXCL, mode); } @@ -116,7 +123,8 @@ void *read_blob_entry(const struct cache_entry *ce, size_t *size) return NULL; } -static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempfile) +static int open_output_fd(char *path, const struct cache_entry *ce, + const struct checkout *state, int to_tempfile) { int symlink = (ce->ce_mode & S_IFMT) != S_IFREG; if (to_tempfile) { @@ -124,8 +132,194 @@ static int open_output_fd(char *path, const struct cache_entry *ce, int to_tempf symlink ? ".merge_link_XXXXXX" : ".merge_file_XXXXXX"); return mkstemp(path); } else { - return create_file(path, !symlink ? ce->ce_mode : 0666); + return create_file(path, !symlink ? ce->ce_mode : 0666, + state->fresh_worktree && !symlink); + } +} + +static int conversion_attrs_equal(const struct conv_attrs *a, + const struct conv_attrs *b) +{ + 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)); +} + +#define COPY_ON_WRITE_MIN_BYTES (160 * 1024) + +static struct cache_entry *copy_source_entry(const struct cache_entry *ce, + const struct conv_attrs *ca, + const struct checkout *state) +{ + 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) || + ca->drv || ca->ident || ca->working_tree_encoding || + (ca->crlf_action != CRLF_BINARY && auto_crlf != AUTO_CRLF_FALSE)) + 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) || + source->ce_stat_data.sd_size <= COPY_ON_WRITE_MIN_BYTES || + !oideq(&source->oid, &ce->oid)) + return NULL; + convert_attrs(istate, &source_ca, ce->name); + 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; +} + +/* Verify only clone candidates instead of refreshing the entire source index. + * Matching non-racy stat data is the index's clean-file guarantee; racy files + * require an object hash. EOL conversion must also reproduce the exact bytes. */ +static int clone_source_matches(int fd, off_t source_size, + const struct cache_entry *ce, + const struct conv_attrs *ca, + const struct checkout *state) +{ + struct strbuf raw = STRBUF_INIT, normalized = STRBUF_INIT; + struct strbuf checkout = STRBUF_INIT; + struct object_id oid; + struct object_info oi = OBJECT_INFO_INIT; + enum object_type type; + size_t blob_size, len; + const char *data; + int racy = is_racy_timestamp(state->copy_source->istate, ce); + int have_size = 0, matches = 0; + + if (ca->crlf_action == CRLF_BINARY && !racy) + return 1; + if (!racy) { + oi.typep = &type; + oi.sizep = &blob_size; + have_size = !odb_read_object_info_extended(the_repository->objects, + &ce->oid, &oi, OBJECT_INFO_LOOKUP_REPLACE | + OBJECT_INFO_SKIP_FETCH_OBJECT) && type == OBJ_BLOB; + /* LF checkout does not expand data, and clean EOL normalization + * only shrinks it. Equal sizes therefore prove identity. */ + if (have_size && (ca->crlf_action == CRLF_TEXT_INPUT || + ca->crlf_action == CRLF_AUTO_INPUT || + (ca->crlf_action == CRLF_AUTO && core_eol == EOL_LF))) + return blob_size == source_size; + } + if (strbuf_read(&raw, fd, source_size) < 0) + goto done; + data = raw.buf; + len = raw.len; + if (racy || !have_size) { + hash_object_file(the_repository->hash_algo, data, len, OBJ_BLOB, &oid); + if (oideq(&oid, &ce->oid)) + goto checkout; + } + if (ca->crlf_action != CRLF_BINARY && + (!have_size || len != blob_size || racy) && + convert_to_git(state->copy_source->istate, ce->name, + raw.buf, raw.len, &normalized, CONV_EOL_RENORMALIZE)) { + data = normalized.buf; + len = normalized.len; + } else if (racy || !have_size) { + goto done; } + if (racy || !have_size) { + hash_object_file(the_repository->hash_algo, data, len, OBJ_BLOB, &oid); + if (!oideq(&oid, &ce->oid)) + goto done; + } else if (len != blob_size) { + goto done; + } +checkout: + if (convert_to_working_tree_ca(ca, ce->name, data, len, &checkout, NULL)) { + data = checkout.buf; + len = checkout.len; + } + matches = len == raw.len && !memcmp(data, raw.buf, len); +done: + strbuf_release(&checkout); + strbuf_release(&normalized); + strbuf_release(&raw); + return matches; +} +static int try_copy_on_write(const struct cache_entry *ce, + const struct cache_entry *source_ce, char *path, + const struct conv_attrs *ca, + const struct checkout *state, + int *fstat_done, struct stat *statbuf) +{ + struct strbuf source = STRBUF_INIT; + struct stat st, st_after; + int src_fd = -1, dst_fd = -1; + int ret = 0; + + if (!file_copy_on_write_supported(path)) + return 0; + + strbuf_addf(&source, "%s/%s", state->copy_source->worktree, ce->name); + src_fd = open_nofollow_stat(source.buf, &st); + if (src_fd < 0 || !S_ISREG(st.st_mode) || + st.st_size <= COPY_ON_WRITE_MIN_BYTES || + !source_is_uptodate(state->copy_source, source_ce, &st) || + !clone_source_matches(src_fd, st.st_size, source_ce, ca, state)) + goto done; + + dst_fd = open_output_fd(path, ce, state, 0); + if (dst_fd < 0) { + if (state->fresh_worktree) { + ret = -1; + goto done; + } + goto done; + } + if (file_copy_on_write(dst_fd, src_fd, st.st_size, path) || + fstat(src_fd, &st_after) || + !source_is_uptodate(state->copy_source, source_ce, &st_after)) { + 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); + trace2_data_string("checkout", the_repository, + "copy_on_write", ce->name); + 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) @@ -146,7 +340,7 @@ static int streaming_write_entry(const struct cache_entry *ce, char *path, int result = 0; int fd; - fd = open_output_fd(path, ce, to_tempfile); + fd = open_output_fd(path, ce, state, to_tempfile); if (fd < 0) return -1; @@ -294,6 +488,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) { @@ -313,7 +508,18 @@ 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 (source_ce) { + int cloned = try_copy_on_write(ce, source_ce, path, ca, state, + &fstat_done, &st); + if (cloned < 0) + return -2; /* retry after checking a collision */ + if (cloned) + goto finish; + } + + filter = get_stream_filter_ca(ca, &ce->oid); if (filter && !streaming_write_entry(ce, path, filter, state, to_tempfile, @@ -391,9 +597,11 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca */ write_file_entry: - fd = open_output_fd(path, ce, to_tempfile); + fd = open_output_fd(path, ce, state, to_tempfile); if (fd < 0) { free(new_blob); + if (state->fresh_worktree) + return -2; return error_errno("unable to create file %s", path); } @@ -502,6 +710,10 @@ 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; + struct checkout slow_state; + const struct checkout *write_state = state; + int fast_path = 0, source_checked = 0, ret; if (ce->ce_flags & CE_WT_REMOVE) { if (topath) @@ -519,14 +731,28 @@ 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); strbuf_add(&path, state->base_dir, state->base_dir_len); strbuf_add(&path, ce->name, ce_namelen(ce)); - if (!check_path(path.buf, path.len, &st, state->base_dir_len)) { + if (state->fresh_worktree && S_ISREG(ce->ce_mode)) { + if (!ca) { + convert_attrs(state->istate, &ca_buf, ce->name); + ca = &ca_buf; + } + source_ce = copy_source_entry(ce, ca, state); + source_checked = 1; + fast_path = source_ce || (!ca->drv && state->force && + !state->not_new && parallel_checkout_status() != PC_RUNNING); + } + +slow_path: + if (!fast_path && + !check_path(path.buf, path.len, &st, state->base_dir_len)) { const struct submodule *sub; unsigned changed = ie_match_stat(state->istate, ce, &st, CE_MATCH_IGNORE_VALID | CE_MATCH_IGNORE_SKIP_WORKTREE); @@ -592,7 +818,7 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, remove_subtree(&path); } else if (unlink(path.buf)) return error_errno("unable to unlink old '%s'", path.buf); - } else if (state->not_new) + } else if (!fast_path && state->not_new) return 0; create_directories(path.buf, path.len, state); @@ -602,10 +828,22 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, ca = &ca_buf; } - if (!enqueue_checkout(ce, ca, nr_checkouts)) + if (!source_checked) + 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); + ret = write_entry(ce, path.buf, ca, source_ce, write_state, 0, + nr_checkouts); + if (ret == -2 && fast_path) { + /* Resolve any failed create with the original path checks. */ + fast_path = 0; + slow_state = *state; + slow_state.fresh_worktree = 0; + write_state = &slow_state; + goto slow_path; + } + return ret; } void unlink_entry(const struct cache_entry *ce, const char *super_prefix) diff --git a/entry.h b/entry.h index ca3ed35bc08654..48585ef4393499 100644 --- a/entry.h +++ b/entry.h @@ -6,6 +6,13 @@ struct cache_entry; struct index_state; +struct checkout_copy_source { + struct index_state *istate; + const char *worktree; + uint64_t refreshed_at; + int fresh_worktree; +}; + struct checkout { struct index_state *istate; const char *base_dir; @@ -13,11 +20,13 @@ struct checkout { const char *super_prefix; struct delayed_checkout *delayed_checkout; struct checkout_metadata meta; + struct checkout_copy_source *copy_source; unsigned force:1, quiet:1, not_new:1, clone:1, - refresh_cache:1; + refresh_cache:1, + fresh_worktree:1; }; #define CHECKOUT_INIT { .base_dir = "" } diff --git a/environment.h b/environment.h index e7ec5b0437342d..33d301903c8305 100644 --- a/environment.h +++ b/environment.h @@ -9,6 +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_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/git-compat-util.h b/git-compat-util.h index 10d3eec3c74ba5..7b0a15fffdcd9c 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -223,6 +223,18 @@ static inline int noop_core_config(const char *var UNUSED, #define platform_core_config noop_core_config #endif +#ifndef copy_on_write_supported +#define copy_on_write_supported 0 +#endif + +#ifndef file_copy_on_write_supported +#define file_copy_on_write_supported(path) copy_on_write_supported +#endif + +#ifndef file_copy_on_write +#define file_copy_on_write(dst_fd, src_fd, size, path) (errno = ENOSYS, -1) +#endif + #ifndef has_dos_drive_prefix static inline int git_has_dos_drive_prefix(const char *path UNUSED) { diff --git a/t/helper/test-copy-on-write.c b/t/helper/test-copy-on-write.c new file mode 100644 index 00000000000000..ae6fc164e0b924 --- /dev/null +++ b/t/helper/test-copy-on-write.c @@ -0,0 +1,81 @@ +#include "test-tool.h" + +static int copy_one(const char *source, const char *target, int fail_cache) +{ + struct stat st; + int src_fd, dst_fd; + int ret; + + src_fd = open(source, O_RDONLY); + if (src_fd < 0 || fstat(src_fd, &st)) + return 1; + if (fail_cache) { + /* A read-only destination forces a real resize failure. */ + ret = file_copy_on_write(src_fd, src_fd, st.st_size, source); + close(src_fd); + return !ret || file_copy_on_write_supported(target); + } + dst_fd = open(target, O_WRONLY | O_CREAT | O_EXCL, 0666); + if (dst_fd < 0) { + close(src_fd); + return 1; + } + + ret = file_copy_on_write(dst_fd, src_fd, st.st_size, target); + if (close(dst_fd)) + ret = -1; + close(src_fd); + if (ret) + unlink(target); + return !!ret; +} + +int cmd__copy_on_write(int argc, const char **argv) +{ + int i; + struct stat st; + int fd; + +#ifdef GIT_WINDOWS_NATIVE + if (argc == 3 && !strcmp(argv[1], "--create-nofollow")) { + fd = mingw_create_file_nofollow(argv[2], 0666); + if (fd < 0) + return 1; + return !!close(fd); + } +#endif + + if (argc == 3 && !strcmp(argv[1], "--open-nofollow-stat")) { + fd = open_nofollow_stat(argv[2], &st); + if (fd < 0) + return 1; + close(fd); + return !S_ISREG(st.st_mode); + } + + if (argc >= 4 && !(argc % 2) && !strcmp(argv[1], "--probe-path")) { + for (i = 2; i < argc; i += 2) + if (!!file_copy_on_write_supported(argv[i]) != + !!atoi(argv[i + 1])) + return 1; + return 0; + } + if (argc >= 4 && !(argc % 2) && !strcmp(argv[1], "--probe-cwd")) { + for (i = 2; i < argc; i += 2) { + if (chdir(argv[i]) || + !!file_copy_on_write_supported("relative-file") != + !!atoi(argv[i + 1])) + return 1; + } + return 0; + } + if (argc == 4 && !strcmp(argv[1], "--fail-cache")) + return copy_one(argv[2], argv[3], 1); + if (argc >= 4 && !(argc % 2) && !strcmp(argv[1], "--batch")) { + for (i = 2; i < argc; i += 2) + if (copy_one(argv[i], argv[i + 1], 0)) + return 1; + return 0; + } + return argc == 3 ? copy_one(argv[1], argv[2], 0) : 1; +} diff --git a/t/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 bdcca97633bc10..ddfc84d109620d 100755 --- a/t/t2400-worktree-add.sh +++ b/t/t2400-worktree-add.sh @@ -5,11 +5,101 @@ test_description='test git worktree add' GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME +GIT_TRACE2_EVENT_NESTING=10 +export GIT_TRACE2_EVENT_NESTING + TEST_CREATE_REPO_NO_TEMPLATE=1 . ./test-lib.sh . "$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 COPY_ON_WRITE 'copy-on-write preserves sizes and source' ' + test_when_finished "rm -f size-source size-target size-expect" && + for size in 0 1 4095 4096 4097 65535 65536 65537 1048577 + do + test-tool genrandom clone-size $size >size-source && + cp size-source size-expect && + test-tool copy-on-write size-source size-target && + test_cmp size-source size-target && + echo changed >>size-target && + test_cmp size-expect size-source && + rm size-target || return 1 + done +' + +test_expect_success MINGW,SYMLINKS,COPY_ON_WRITE 'source open rejects symlinks' ' + test_when_finished "rm -f clone-regular clone-link" && + echo content >clone-regular && + ln -s clone-regular clone-link && + test-tool copy-on-write --open-nofollow-stat clone-regular && + test_must_fail test-tool copy-on-write --open-nofollow-stat clone-link +' + +test_expect_success MINGW 'exclusive nofollow creation rejects existing files' ' + test_when_finished "rm -f exclusive-target" && + test-tool copy-on-write --create-nofollow exclusive-target && + test_must_fail test-tool copy-on-write --create-nofollow exclusive-target +' + +test_expect_success MINGW,SYMLINKS 'exclusive creation rejects dangling symlinks' ' + test_when_finished "rm -f exclusive-link exclusive-missing" && + ln -s exclusive-missing exclusive-link && + test_must_fail test-tool copy-on-write --create-nofollow exclusive-link && + test_path_is_missing exclusive-missing +' + +test_expect_success MINGW,COPY_ON_WRITE 'clone alignment is cached' ' + test_when_finished "rm -f cache-source cache-one cache-two" && + test-tool genrandom clone-cache 102400 >cache-source && + GIT_TRACE2_EVENT="$PWD/alignment-trace" \ + test-tool copy-on-write --batch cache-source cache-one \ + cache-source cache-two && + test_cmp cache-source cache-one && + test_cmp cache-source cache-two && + grep "\"key\":\"copy_on_write_alignment\"" alignment-trace \ + >alignment-events && + test_line_count = 1 alignment-events && + test_grep ! "\"key\":\"copy_on_write_disabled\"" alignment-trace +' + +test_expect_success MINGW,COPY_ON_WRITE 'drive-letter cache accepts both cases' ' + drive=$(cygpath -m "$PWD" | cut -c1) && + upper=$(printf %s "$drive" | tr "[:lower:]" "[:upper:]") && + lower=$(printf %s "$drive" | tr "[:upper:]" "[:lower:]") && + GIT_TRACE2_EVENT="$PWD/case-trace" \ + test-tool copy-on-write --probe-path \ + "$upper:/clone-probe" 1 "$lower:/clone-probe" 1 && + grep "\"key\":\"copy_on_write_alignment\"" case-trace \ + >case-events && + test_line_count = 1 case-events +' + +test_expect_success MINGW,COPY_ON_WRITE 'clone failure disables drive cache' ' + test_when_finished "rm -f cache-source cache-target cache-expect" && + test-tool genrandom clone-failure 65536 >cache-source && + cp cache-source cache-expect && + GIT_TRACE2_EVENT="$PWD/failure-trace" \ + test-tool copy-on-write --fail-cache cache-source \ + cache-target && + test_path_is_missing cache-target && + test_cmp cache-expect cache-source && + test_grep "\"key\":\"copy_on_write_disabled\"" failure-trace +' + +test_expect_success MINGW,COPY_ON_WRITE 'drive cache follows chdir' ' + test_when_finished "rm -rf cache-dir-one cache-dir-two" && + mkdir cache-dir-one cache-dir-two && + test-tool copy-on-write --probe-cwd "$PWD/cache-dir-one" 1 \ + "$PWD/cache-dir-two" 1 +' + test_expect_success 'setup' ' test_commit init ' @@ -71,6 +161,198 @@ 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 && + test-tool genrandom clone-above-threshold 163841 >clean && + test-tool genrandom clone-at-threshold 163840 >boundary && + echo clean >clean.crlf && + echo clean >modified.crlf && + echo clean >unconverted.crlf && + git add .gitattributes clean boundary clean.crlf modified.crlf unconverted.crlf && + git commit -m "copy-on-write test files" && + rm clean.crlf modified.crlf && + git checkout -- clean.crlf modified.crlf && + 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\":\"boundary\"" trace && + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"clean.crlf\"" trace && + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"unconverted.crlf\"" trace && + test_grep ! "\"value\":\"init.t\"" trace && + test_grep ! "\"value\":\"modified.crlf\"" trace && + git -C copy-on-write diff --exit-code && + printf "clean\r\n" >expect-crlf && + test_cmp expect-crlf copy-on-write/clean.crlf && + test_cmp expect-crlf copy-on-write/unconverted.crlf +' + +test_expect_success COPY_ON_WRITE 'autocrlf=false clones only the exact checkout representation' ' + test_create_repo eol-source && + ( + cd eol-source && + git config core.autocrlf false && + git config core.eol lf && + echo "* text=auto" >.gitattributes && + awk "BEGIN { for (i = 0; i < 20000; i++) print \"line endings\" }" >text && + git add .gitattributes text && + git commit -m "large automatic text" && + cp text ../expected-lf && + awk "{ printf \"%s\\r\\n\", \$0 }" ../expected-lf >../expected-crlf && + for eol in lf crlf + do + git config core.eol "$eol" && + cp ../expected-$eol text && + git add text && + git diff --cached --exit-code && + rm -f ../eol-trace && + GIT_TRACE2_EVENT="$PWD/../eol-trace" \ + git worktree add --detach ../eol-clone HEAD && + test_grep "\"key\":\"copy_on_write\",\"value\":\"text\"" ../eol-trace && + test_cmp_bin ../expected-$eol ../eol-clone/text && + git -C ../eol-clone diff --exit-code && + git worktree remove --force ../eol-clone || return 1 + done && + for eol in lf crlf + do + git config core.eol "$eol" && + if test "$eol" = lf; then other=crlf; else other=lf; fi && + cp ../expected-$other text && + git add text && + git diff --cached --exit-code && + rm -f ../eol-trace && + GIT_TRACE2_EVENT="$PWD/../eol-trace" \ + git worktree add --detach ../eol-clone HEAD && + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"text\"" ../eol-trace && + test_cmp_bin ../expected-$eol ../eol-clone/text && + git -C ../eol-clone diff --exit-code && + git worktree remove --force ../eol-clone || return 1 + done + ) +' +test_expect_success COPY_ON_WRITE 'racy clone candidates are checked by content' ' + test_create_repo racy-source && + ( + cd racy-source && + git config core.autocrlf false && + git config core.trustctime false && + git config core.checkstat minimal && + echo "* -text" >.gitattributes && + test-tool genrandom racy-original 163841 >binary && + test-tool chmtime =-10 binary && + mtime=$(test-tool chmtime --get binary) && + cp binary ../expected-binary && + git add .gitattributes binary && + git commit -m "racy binary candidate" && + test-tool chmtime "=$((mtime - 1))" .git/index && + GIT_TRACE2_EVENT="$PWD/../racy-clean-trace" \ + git worktree add --detach ../racy-clone HEAD && + test_grep "\"key\":\"copy_on_write\",\"value\":\"binary\"" ../racy-clean-trace && + test_cmp_bin ../expected-binary ../racy-clone/binary && + git worktree remove --force ../racy-clone && + test-tool genrandom racy-modified 163841 >binary && + test-tool chmtime "=$mtime" binary && + GIT_TRACE2_EVENT="$PWD/../racy-dirty-trace" \ + git worktree add --detach ../racy-clone HEAD && + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"binary\"" ../racy-dirty-trace && + test_cmp_bin ../expected-binary ../racy-clone/binary && + git -C ../racy-clone diff --exit-code && + git worktree remove --force ../racy-clone + ) +' +test_expect_success COPY_ON_WRITE 'explicit CRLF overrides core.eol=lf with autocrlf=false' ' + test_create_repo explicit-eol-source && + ( + cd explicit-eol-source && + git config core.autocrlf false && + git config core.eol lf && + echo "* text eol=crlf" >.gitattributes && + awk "BEGIN { for (i = 0; i < 20000; i++) print \"line endings\" }" >text && + cp text ../explicit-lf && + awk "{ printf \"%s\\r\\n\", \$0 }" text >../explicit-crlf && + git add .gitattributes text && + git commit -m "explicit CRLF text" && + for eol in lf crlf + do + cp ../explicit-$eol text && + git add text && + git diff --cached --exit-code && + rm -f ../explicit-trace && + GIT_TRACE2_EVENT="$PWD/../explicit-trace" \ + git worktree add --detach ../explicit-clone HEAD && + if test "$eol" = crlf + then + test_grep "\"key\":\"copy_on_write\",\"value\":\"text\"" ../explicit-trace + else + test_grep ! "\"key\":\"copy_on_write\",\"value\":\"text\"" ../explicit-trace + fi && + test_cmp_bin ../explicit-crlf ../explicit-clone/text && + git -C ../explicit-clone diff --exit-code && + git worktree remove --force ../explicit-clone || return 1 + done + ) +' + +test_expect_success COPY_ON_WRITE 'automatic text preserves legacy CRLF blobs' ' + test_create_repo legacy-eol-source && + ( + cd legacy-eol-source && + git config core.autocrlf false && + echo "* text=auto" >.gitattributes && + awk "BEGIN { for (i = 0; i < 20000; i++) printf \"line endings\\r\\n\" }" >text && + test-tool chmtime =-10 text && + git add .gitattributes && + oid=$(git hash-object -w --no-filters text) && + git update-index --add --cacheinfo "100644,$oid,text" && + git commit -m "legacy CRLF blob" && + git update-index --refresh && + for eol in lf crlf + do + git config core.eol "$eol" && + rm -f ../legacy-trace && + GIT_TRACE2_EVENT="$PWD/../legacy-trace" \ + git worktree add --detach ../legacy-clone HEAD && + test_grep "\"key\":\"copy_on_write\",\"value\":\"text\"" ../legacy-trace && + test_cmp_bin text ../legacy-clone/text && + git -C ../legacy-clone diff --exit-code && + git worktree remove --force ../legacy-clone || return 1 + done + ) +' +test_expect_success MINGW 'fresh worktree resolves a collision after a filtered file' ' + test_create_repo collision-source && + ( + cd collision-source && + git config core.autocrlf false && + git config core.ignorecase false && + git config core.fscache true && + git config filter.case.smudge cat && + echo "folder/Case.filtered filter=case" >.gitattributes && + git add .gitattributes && + first=$(printf "first\n" | git hash-object -w --stdin) && + second=$(printf "second\n" | git hash-object -w --stdin) && + git update-index --add --cacheinfo "100644,$first,folder/0-first" && + git update-index --add --cacheinfo "100644,$first,folder/Case.filtered" && + git update-index --add --cacheinfo "100644,$second,folder/case.filtered" && + git commit -m "case collision after filter" && + git worktree add --detach ../collision-target HEAD && + printf "second\n" >../expected-collision && + test_cmp_bin ../expected-collision ../collision-target/folder/case.filtered && + git worktree remove --force ../collision-target + ) +' +test_expect_success COPY_ON_WRITE 'worktree.copyOnWrite disables optimization' ' + test_when_finished "git worktree remove --force no-copy-on-write" && + test_config worktree.copyOnWrite false && + GIT_TRACE2_EVENT="$PWD/disabled-trace" \ + git worktree add --detach no-copy-on-write main && + test_grep ! "\"key\":\"copy_on_write\"" disabled-trace +' + 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..c86b8038c3a42b 100644 --- a/unpack-trees.c +++ b/unpack-trees.c @@ -437,6 +437,8 @@ 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.fresh_worktree = o->copy_source && o->copy_source->fresh_worktree; 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..1faaa648abbb0d 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,6 +75,8 @@ struct unpack_trees_options { enum unpack_trees_reset_type reset; const char *prefix; const char *super_prefix; + /* Caller-owned source and index, valid for this unpack operation. */ + struct checkout_copy_source *copy_source; struct pathspec *pathspec; merge_fn_t fn; diff --git a/wrapper.c b/wrapper.c index 561f9ee9c99fc1..89f71ef80e8be8 100644 --- a/wrapper.c +++ b/wrapper.c @@ -860,6 +860,24 @@ int open_nofollow(const char *path, int flags) #endif } +int open_nofollow_stat(const char *path, struct stat *st) +{ +#ifdef GIT_WINDOWS_NATIVE + return mingw_open_nofollow_stat(path, st); +#else + int fd = open_nofollow(path, O_RDONLY); + + if (fd >= 0 && fstat(fd, st)) { + int saved_errno = errno; + + close(fd); + errno = saved_errno; + return -1; + } + return fd; +#endif +} + int csprng_bytes(void *buf, size_t len, MAYBE_UNUSED unsigned flags) { #if defined(HAVE_ARC4RANDOM) || defined(HAVE_ARC4RANDOM_LIBBSD) diff --git a/wrapper.h b/wrapper.h index a6287d7f4d11be..39dfae16740dcc 100644 --- a/wrapper.h +++ b/wrapper.h @@ -138,6 +138,7 @@ int warn_on_fopen_errors(const char *path); * simultaneously create paths. */ int open_nofollow(const char *path, int flags); +int open_nofollow_stat(const char *path, struct stat *st); void sleep_millisec(int millisec);