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
11 changes: 11 additions & 0 deletions Documentation/config/worktree.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
for the new branch. If no such match can be found, it falls
back to creating a new branch from the current `HEAD`.

`worktree.useReflink`::
When populating a new worktree, try to create working tree
files as lightweight copy-on-write clones (reflinks) of the
corresponding files in the main worktree whenever they are
known to have identical content. This shares disk blocks
between worktrees and avoids reading file contents from the
object database. It requires support from the filesystem
(e.g. Btrfs or XFS on Linux); where unsupported, `git worktree
add` silently falls back to a regular checkout. This currently
only has an effect on Linux. Defaults to "`true`".

`worktree.useRelativePaths`::
Link worktrees using relative paths (when "`true`") or absolute
paths (when "`false`"). This is particularly useful for setups
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,7 @@ TEST_BUILTINS_OBJS += test-read-cache.o
TEST_BUILTINS_OBJS += test-read-graph.o
TEST_BUILTINS_OBJS += test-read-midx.o
TEST_BUILTINS_OBJS += test-ref-store.o
TEST_BUILTINS_OBJS += test-reflink.o
TEST_BUILTINS_OBJS += test-reftable.o
TEST_BUILTINS_OBJS += test-regex.o
TEST_BUILTINS_OBJS += test-rot13-filter.o
Expand Down
35 changes: 35 additions & 0 deletions builtin/worktree.c
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,39 @@ static void setup_alternate_ref_dir(struct worktree *wt, const char *wt_git_path
strbuf_release(&sb);
}

static void push_reflink_source_env(struct strvec *child_env)
{
struct worktree **worktrees;
int use_reflink = 1;

repo_config_get_bool(the_repository, "worktree.usereflink",
&use_reflink);
if (!use_reflink)
return;

worktrees = get_worktrees();
if (worktrees[0] && !worktrees[0]->is_bare) {
struct child_process cp = CHILD_PROCESS_INIT;

cp.git_cmd = 1;
cp.dir = worktrees[0]->path;
/*
* Clear any repo-local vars (e.g. GIT_DIR, GIT_WORK_TREE)
* inherited from the calling process so the child rediscovers
* the repository from cp.dir instead of reusing whichever
* worktree we happen to have been invoked from.
*/
strvec_pushv(&cp.env, (const char **)local_repo_env);
strvec_pushl(&cp.args, "update-index", "-q", "--refresh", NULL);
/* non-fatal: racy donor files just won't reflink */
run_command(&cp);
strvec_pushf(child_env, "%s=%s",
GIT_WORKTREE_REFLINK_SOURCE_ENVIRONMENT,
worktrees[0]->path);
}
free_worktrees(worktrees);
}

static int add_worktree(const char *path, const char *refname,
const struct add_opts *opts)
{
Expand Down Expand Up @@ -584,6 +617,8 @@ static int add_worktree(const char *path, const char *refname,

strvec_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf);
strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path);
if (opts->checkout && !opts->orphan)
push_reflink_source_env(&child_env);

if (opts->orphan &&
(ret = make_worktree_orphan(refname, opts, &child_env)))
Expand Down
153 changes: 152 additions & 1 deletion entry.c
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
#define USE_THE_REPOSITORY_VARIABLE

#include "git-compat-util.h"
#include "abspath.h"
#include "convert.h"
#include "odb.h"
#include "odb/streaming.h"
#include "dir.h"
#include "environment.h"
#include "gettext.h"
#include "hex.h"
#include "name-hash.h"
#include "path.h"
#include "read-cache-ll.h"
#include "repository.h"
#include "sparse-index.h"
#include "submodule.h"
#include "symlinks.h"
#include "progress.h"
#include "trace2.h"
#include "fsmonitor.h"
#include "wrapper.h"
#include "entry.h"
#include "parallel-checkout.h"

Expand Down Expand Up @@ -146,6 +153,145 @@ static int streaming_write_entry(const struct cache_entry *ce, char *path,
return result;
}

/*
* Copy-on-write population of a new worktree: when "git worktree add"
* points us at the primary worktree via GIT_WORKTREE_REFLINK_SOURCE,
* try to clone (reflink) files from it instead of writing blob
* contents from the object database. The donor's index tells us
* which of its files are known to be unmodified.
*
* Note that entries handled by parallel checkout (checkout.workers >
* 1) do not come through this path and are simply written normally.
*/
struct reflink_donor {
int state; /* -1 = not checked yet, 0 = disabled, 1 = active */
const char *root;
struct index_state istate;
};
static struct reflink_donor reflink_donor = { .state = -1 };

static int reflink_donor_active(void)
{
if (reflink_donor.state < 0) {
const char *root;
char *index_path;

reflink_donor.state = 0;
root = getenv(GIT_WORKTREE_REFLINK_SOURCE_ENVIRONMENT);
if (!root || !is_absolute_path(root))
return 0;

index_state_init(&reflink_donor.istate, the_repository);
index_path = repo_common_path(the_repository, "index");
if (read_index_from(&reflink_donor.istate, index_path,
repo_get_common_dir(the_repository)) > 0) {
reflink_donor.root = xstrdup(root);
reflink_donor.state = 1;
}
free(index_path);
}
return reflink_donor.state;
}

/*
* The checkout-direction (smudge) CRLF conversion is the identity for
* these actions: crlf_to_worktree() bails out immediately unless the
* action's output_eol() is EOL_CRLF, which is true only for the
* "*_CRLF" actions. For CRLF_BINARY and the input-style actions a
* blob therefore checks out byte-for-byte as its own content, so the
* worktree bytes of a fresh checkout equal the blob bytes.
*/
static int crlf_action_is_identity_smudge(enum convert_crlf_action action)
{
switch (action) {
case CRLF_BINARY:
case CRLF_TEXT_INPUT:
case CRLF_AUTO_INPUT:
return 1;
default:
return 0;
}
}

/*
* Try to create "path" as a reflink of the same file in the donor
* worktree. A zero return means the file is in place; any nonzero
* return means the caller must write it the normal way.
*/
static int try_reflink_entry(const struct cache_entry *ce,
const struct conv_attrs *ca,
const char *path)
{
const struct cache_entry *dce;
struct stat st;
struct strbuf donor_path = STRBUF_INIT;
size_t blob_size;
int ret = -1;

/*
* Only blobs whose worktree representation is identical to
* their content are eligible; anything that smudges is left
* to the regular code path. Input-style CRLF actions are
* admitted because their smudge is the identity (see
* crlf_action_is_identity_smudge()); the clean direction only
* ever removes CR bytes, so a donor whose on-disk size equals
* the blob size cannot have diverged from it -- the size gate
* below is the backstop that makes byte-divergent donors
* impossible.
*/
if (ca->drv || ca->ident || ca->working_tree_encoding ||
!crlf_action_is_identity_smudge(ca->crlf_action))
return -1;

if (!reflink_donor_active())
return -1;

dce = index_file_exists(&reflink_donor.istate, ce->name,
strlen(ce->name), 0);
if (!dce || ce_skip_worktree(dce) ||
dce->ce_mode != ce->ce_mode || !oideq(&dce->oid, &ce->oid))
return -1;

strbuf_addf(&donor_path, "%s/%s", reflink_donor.root, ce->name);

/*
* The donor file must be stat-clean with respect to the
* donor's index; racily clean entries are conservatively
* treated as dirty. For an eligible (conversion-free) entry
* the file size must then also match the blob size, which
* guards against a donor checkout that smudged the file under
* attribute rules we no longer see.
*/
if (lstat(donor_path.buf, &st) || !S_ISREG(st.st_mode))
goto out;
if (ie_match_stat(&reflink_donor.istate, dce, &st,
CE_MATCH_IGNORE_VALID | CE_MATCH_IGNORE_FSMONITOR |
CE_MATCH_RACY_IS_DIRTY))
goto out;
if (odb_read_object_info(the_repository->objects, &ce->oid,
&blob_size) < 0 ||
(uintmax_t)st.st_size != (uintmax_t)blob_size)
goto out;

trace2_counter_add(TRACE2_COUNTER_ID_REFLINK_ATTEMPTS, 1);
if (reflink_file(donor_path.buf, path,
(ce->ce_mode & 0100) ? 0777 : 0666)) {
/*
* These errnos mean the filesystem cannot reflink at
* all, so do not keep trying for every file.
*/
if (errno == EOPNOTSUPP || errno == ENOTTY ||
errno == EXDEV || errno == ENOSYS)
reflink_donor.state = 0;
goto out;
}
trace2_counter_add(TRACE2_COUNTER_ID_REFLINK_HITS, 1);
ret = 0;
out:
strbuf_release(&donor_path);
return ret;
}

void enable_delayed_checkout(struct checkout *state)
{
if (!state->delayed_checkout) {
Expand Down Expand Up @@ -300,7 +446,12 @@ static int write_entry(struct cache_entry *ce, char *path, struct conv_attrs *ca
clone_checkout_metadata(&meta, &state->meta, &ce->oid);

if (ce_mode_s_ifmt == S_IFREG) {
struct stream_filter *filter = get_stream_filter_ca(ca, &ce->oid);
struct stream_filter *filter;

if (!to_tempfile && !try_reflink_entry(ce, ca, path))
goto finish;

filter = get_stream_filter_ca(ca, &ce->oid);
if (filter &&
!streaming_write_entry(ce, path, filter,
state, to_tempfile,
Expand Down
2 changes: 2 additions & 0 deletions environment.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ const char * const local_repo_env[] = {
GIT_DIR_ENVIRONMENT,
GIT_WORK_TREE_ENVIRONMENT,
GIT_IMPLICIT_WORK_TREE_ENVIRONMENT,
/* internal; set by "git worktree add" for its checkout child */
GIT_WORKTREE_REFLINK_SOURCE_ENVIRONMENT,
GRAFT_ENVIRONMENT,
INDEX_ENVIRONMENT,
NO_REPLACE_OBJECTS_ENVIRONMENT,
Expand Down
6 changes: 6 additions & 0 deletions environment.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
#define GIT_NAMESPACE_ENVIRONMENT "GIT_NAMESPACE"
#define GIT_WORK_TREE_ENVIRONMENT "GIT_WORK_TREE"
#define GIT_PREFIX_ENVIRONMENT "GIT_PREFIX"
/*
* Internal, set by "git worktree add" for the checkout it spawns:
* absolute path of the primary worktree, whose clean files may be
* reflinked instead of being written from the object database.
*/
#define GIT_WORKTREE_REFLINK_SOURCE_ENVIRONMENT "GIT_WORKTREE_REFLINK_SOURCE"
#define DEFAULT_GIT_DIR_ENVIRONMENT ".git"
#define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY"
#define INDEX_ENVIRONMENT "GIT_INDEX_FILE"
Expand Down
1 change: 1 addition & 0 deletions t/helper/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ test_tool_sources = [
'test-read-graph.c',
'test-read-midx.c',
'test-ref-store.c',
'test-reflink.c',
'test-reftable.c',
'test-regex.c',
'test-repository.c',
Expand Down
13 changes: 13 additions & 0 deletions t/helper/test-reflink.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#include "test-tool.h"
#include "wrapper.h"

int cmd__reflink(int argc, const char **argv)
{
if (argc != 3)
usage("test-tool reflink <src> <dst>");
if (reflink_file(argv[1], argv[2], 0666)) {
fprintf(stderr, "reflink failed: %s\n", strerror(errno));
return 1;
}
return 0;
}
1 change: 1 addition & 0 deletions t/helper/test-tool.c
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ static struct test_cmd cmds[] = {
{ "read-graph", cmd__read_graph },
{ "read-midx", cmd__read_midx },
{ "ref-store", cmd__ref_store },
{ "reflink", cmd__reflink },
{ "rot13-filter", cmd__rot13_filter },
{ "regex", cmd__regex },
{ "repository", cmd__repository },
Expand Down
1 change: 1 addition & 0 deletions t/helper/test-tool.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ int cmd__read_cache(int argc, const char **argv);
int cmd__read_graph(int argc, const char **argv);
int cmd__read_midx(int argc, const char **argv);
int cmd__ref_store(int argc, const char **argv);
int cmd__reflink(int argc, const char **argv);
int cmd__rot13_filter(int argc, const char **argv);
int cmd__regex(int argc, const char **argv);
int cmd__repository(int argc, const char **argv);
Expand Down
Loading
Loading