Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Documentation/config/worktree.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 48 additions & 3 deletions builtin/reset.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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];
Expand All @@ -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;
Expand Down Expand Up @@ -336,13 +339,49 @@ 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;
return 1;
}

int cmd_reset(int argc,
const char **argv,
const char *prefix,
struct repository *repo UNUSED)
{
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;
Expand Down Expand Up @@ -495,6 +534,9 @@ int cmd_reset(int argc,

if (repo_read_index(the_repository) < 0)
die(_("index file corrupt"));
if (reset_type == HARD &&
init_copy_source(&copy_source, &copy_source_index))
copy_source_ptr = &copy_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
Expand Down Expand Up @@ -535,9 +577,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);
Expand All @@ -561,6 +605,7 @@ int cmd_reset(int argc,
discard_index(the_repository->index);

cleanup:
release_index(&copy_source_index);
clear_pathspec(&pathspec);
free(pathspec_from_file);
return update_ref_status;
Expand Down
25 changes: 25 additions & 0 deletions builtin/worktree.c
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,36 @@ static int checkout_worktree(const struct add_opts *opts,
struct strvec *child_env)
{
struct child_process cp = CHILD_PROCESS_INIT;
const char *source = repo_get_work_tree(the_repository);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unlikely to be enough. A repository might be bare, after all. And even if it is not bare, you need to have a look at the Git index to see which files are eligible for block cloning. It might actually be quite prohibitive to provide a list of those files to the reset child process. A better idea would probably to make the checks here whether it is a non-bare repository, to refresh the Git index if so, and most crucially, to record a timestamp. That way, the child process could just look up the file in the Git index, and if it is up to date, block clone it. That would also resolve the issue where you needlessly exclude filtered files from block-cloning (which, in the case of Git LFS, would hurt particularly much).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 484f7b7. Bare repositories omit source metadata; non-bare sources record time before a gentle index refresh/write and pass the source index to reset. Checkout validates matching index/stat/timestamp state, allowing clean converted and LFS-managed files without hashing their working-tree bytes.

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 (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,
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);
}

Expand Down
53 changes: 52 additions & 1 deletion compat/mingw.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,60 @@
#include <winternl.h>

#define STATUS_DELETE_PENDING ((NTSTATUS) 0xC0000056)

#define HCAST(type, handle) ((type)(intptr_t)handle)

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));
/*
* 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.
*/
Comment on lines +44 to +48

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you try 1GiB chunks until hitting EOF, and then immediately drop to 64KiB chunks? So if you block-clone a 1GiB-1 file, you chunk it in fine-grained 64KiB chunks? That sounds super inefficient. I don't know the first thing about block-cloning, but I would have expected multiples of the cluster size to be valid values for FSCTL_DUPLICATE_EXTENTS_TO_FILE.

I wouldn't know because you totally failed on writing a useful commit message. Out of the "context, intent, justification, implementation unless obvious from the diff" parts, I miss pretty much all of them. Fix that via an amend! commit. Or commit_s_.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 50ede48. Full requests now use 4 GiB - 64 KiB; only the final tail is rounded once, trying the two alternative ReFS cluster alignments. The replacement commit message documents the context, intent, justification, alignment rules, and final truncation.

const LONGLONG max_range = (4LL * 1024 * 1024 * 1024) - (64 * 1024);
const LONGLONG tail_alignments[] = { 64 * 1024, 4 * 1024 };
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)
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;
return -1;
}
data.SourceFileOffset.QuadPart += data.ByteCount.QuadPart;
data.TargetFileOffset = data.SourceFileOffset;
}

for (i = 0; i < ARRAY_SIZE(tail_alignments); i++) {
LONGLONG remaining = size - data.SourceFileOffset.QuadPart;

data.ByteCount.QuadPart =
DIV_ROUND_UP(remaining, tail_alignments[i]) *
tail_alignments[i];
if (DeviceIoControl(dst, FSCTL_DUPLICATE_EXTENTS_TO_FILE,
&data, sizeof(data), NULL, 0,
&bytes_returned, NULL)) {
/* Discard any allocation-unit padding in the tail. */
return ftruncate(dst_fd, size);
}
}

errno = ENOSYS;
return -1;
}

void open_in_gdb(void)
{
static struct child_process cp = CHILD_PROCESS_INIT;
Expand Down
3 changes: 3 additions & 0 deletions compat/mingw.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +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
#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 <openssl/ssl.h>
Expand Down
Loading