Skip to content

Commit

Permalink
Merge branch 'cw/worktree-extension' into seen
Browse files Browse the repository at this point in the history
Introduce a new repository extension to prevent older Git versions
from mis-interpreting worktrees created with relative paths.

* cw/worktree-extension:
  worktree: refactor `repair_worktree_after_gitdir_move()`
  worktree: add relative cli/config options to `repair` command
  worktree: add relative cli/config options to `move` command
  worktree: add relative cli/config options to `add` command
  worktree: add `write_worktree_linking_files()` function
  worktree: refactor infer_backlink return
  worktree: add `relativeWorktrees` extension
  setup: correctly reinitialize repository version
  • Loading branch information
gitster committed Nov 26, 2024
2 parents 49f0efa + 28eb7f6 commit 3cdca37
Show file tree
Hide file tree
Showing 18 changed files with 333 additions and 141 deletions.
6 changes: 6 additions & 0 deletions Documentation/config/extensions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ Note that this setting should only be set by linkgit:git-init[1] or
linkgit:git-clone[1]. Trying to change it after initialization will not
work and will produce hard-to-diagnose issues.

relativeWorktrees::
If enabled, indicates at least one worktree has been linked with
relative paths. Automatically set if a worktree has been created or
repaired with either the `--relative-paths` option or with the
`worktree.useRelativePaths` config set to `true`.

worktreeConfig::
If enabled, then worktrees will load config settings from the
`$GIT_DIR/config.worktree` file in addition to the
Expand Down
10 changes: 10 additions & 0 deletions Documentation/config/worktree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ worktree.guessRemote::
such a branch exists, it is checked out and set as "upstream"
for the new branch. If no such match can be found, it falls
back to creating a new branch from the current HEAD.

worktree.useRelativePaths::
Link worktrees using relative paths (when "true") or absolute
paths (when "false"). This is particularly useful for setups
where the repository and worktrees may be moved between
different locations or environments. Defaults to "false".
+
Note that setting `worktree.useRelativePaths` to "true" implies enabling the
`extension.relativeWorktrees` config (see linkgit:git-config[1]),
thus making it incompatible with older versions of Git.
8 changes: 8 additions & 0 deletions Documentation/git-worktree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ To remove a locked worktree, specify `--force` twice.
This can also be set up as the default behaviour by using the
`worktree.guessRemote` config option.

--[no-]relative-paths::
Link worktrees using relative paths or absolute paths (default).
Overrides the `worktree.useRelativePaths` config option, see
linkgit:git-config[1].
+
With `repair`, the linking files will be updated if there's an absolute/relative
mismatch, even if the links are correct.

--[no-]track::
When creating a new branch, if `<commit-ish>` is a branch,
mark it as "upstream" from the new branch. This is the
Expand Down
29 changes: 17 additions & 12 deletions builtin/worktree.c
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,14 @@ struct add_opts {
int quiet;
int checkout;
int orphan;
int relative_paths;
const char *keep_locked;
};

static int show_only;
static int verbose;
static int guess_remote;
static int use_relative_paths;
static timestamp_t expire;

static int git_worktree_config(const char *var, const char *value,
Expand All @@ -134,6 +136,9 @@ static int git_worktree_config(const char *var, const char *value,
if (!strcmp(var, "worktree.guessremote")) {
guess_remote = git_config_bool(var, value);
return 0;
} else if (!strcmp(var, "worktree.userelativepaths")) {
use_relative_paths = git_config_bool(var, value);
return 0;
}

return git_default_config(var, value, ctx, cb);
Expand Down Expand Up @@ -415,8 +420,7 @@ static int add_worktree(const char *path, const char *refname,
const struct add_opts *opts)
{
struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
struct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT;
struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT;
struct strbuf sb = STRBUF_INIT;
const char *name;
struct strvec child_env = STRVEC_INIT;
unsigned int counter = 0;
Expand Down Expand Up @@ -492,10 +496,7 @@ static int add_worktree(const char *path, const char *refname,

strbuf_reset(&sb);
strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
strbuf_realpath(&sb_path_realpath, path, 1);
strbuf_realpath(&sb_repo_realpath, sb_repo.buf, 1);
write_file(sb.buf, "%s/.git", relative_path(sb_path_realpath.buf, sb_repo_realpath.buf, &sb_tmp));
write_file(sb_git.buf, "gitdir: %s", relative_path(sb_repo_realpath.buf, sb_path_realpath.buf, &sb_tmp));
write_worktree_linking_files(sb_git, sb, opts->relative_paths);
strbuf_reset(&sb);
strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
write_file(sb.buf, "../..");
Expand Down Expand Up @@ -579,12 +580,9 @@ static int add_worktree(const char *path, const char *refname,

strvec_clear(&child_env);
strbuf_release(&sb);
strbuf_release(&sb_tmp);
strbuf_release(&symref);
strbuf_release(&sb_repo);
strbuf_release(&sb_repo_realpath);
strbuf_release(&sb_git);
strbuf_release(&sb_path_realpath);
strbuf_release(&sb_name);
free_worktree(wt);
return ret;
Expand Down Expand Up @@ -798,12 +796,15 @@ static int add(int ac, const char **av, const char *prefix,
PARSE_OPT_NOARG | PARSE_OPT_OPTARG),
OPT_BOOL(0, "guess-remote", &guess_remote,
N_("try to match the new branch name with a remote-tracking branch")),
OPT_BOOL(0, "relative-paths", &opts.relative_paths,
N_("use relative paths for worktrees")),
OPT_END()
};
int ret;

memset(&opts, 0, sizeof(opts));
opts.checkout = 1;
opts.relative_paths = use_relative_paths;
ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);
if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
Expand Down Expand Up @@ -1195,6 +1196,8 @@ static int move_worktree(int ac, const char **av, const char *prefix,
OPT__FORCE(&force,
N_("force move even if worktree is dirty or locked"),
PARSE_OPT_NOCOMPLETE),
OPT_BOOL(0, "relative-paths", &use_relative_paths,
N_("use relative paths for worktrees")),
OPT_END()
};
struct worktree **worktrees, *wt;
Expand Down Expand Up @@ -1247,7 +1250,7 @@ static int move_worktree(int ac, const char **av, const char *prefix,
if (rename(wt->path, dst.buf) == -1)
die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf);

update_worktree_location(wt, dst.buf);
update_worktree_location(wt, dst.buf, use_relative_paths);

strbuf_release(&dst);
free_worktrees(worktrees);
Expand Down Expand Up @@ -1390,15 +1393,17 @@ static int repair(int ac, const char **av, const char *prefix,
const char **p;
const char *self[] = { ".", NULL };
struct option options[] = {
OPT_BOOL(0, "relative-paths", &use_relative_paths,
N_("use relative paths for worktrees")),
OPT_END()
};
int rc = 0;

ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0);
p = ac > 0 ? av : self;
for (; *p; p++)
repair_worktree_at_path(*p, report_repair, &rc);
repair_worktrees(report_repair, &rc);
repair_worktree_at_path(*p, report_repair, &rc, use_relative_paths);
repair_worktrees(report_repair, &rc, use_relative_paths);
return rc;
}

Expand Down
1 change: 1 addition & 0 deletions repository.c
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ int repo_init(struct repository *repo,
repo_set_compat_hash_algo(repo, format.compat_hash_algo);
repo_set_ref_storage_format(repo, format.ref_storage_format);
repo->repository_format_worktree_config = format.worktree_config;
repo->repository_format_relative_worktrees = format.relative_worktrees;
if (is_bare > 0)
repo->is_bare_cfg = is_bare;

Expand Down
1 change: 1 addition & 0 deletions repository.h
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ struct repository {

/* Configurations */
int repository_format_worktree_config;
int repository_format_relative_worktrees;

/* Indicate if a repository has a different 'commondir' from 'gitdir' */
unsigned different_commondir:1;
Expand Down
39 changes: 30 additions & 9 deletions setup.c
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,9 @@ static enum extension_result handle_extension(const char *var,
"extensions.refstorage", value);
data->ref_storage_format = format;
return EXTENSION_OK;
} else if (!strcmp(ext, "relativeworktrees")) {
data->relative_worktrees = git_config_bool(var, value);
return EXTENSION_OK;
}
return EXTENSION_UNKNOWN;
}
Expand Down Expand Up @@ -1861,6 +1864,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
repo_fmt.ref_storage_format);
the_repository->repository_format_worktree_config =
repo_fmt.worktree_config;
the_repository->repository_format_relative_worktrees =
repo_fmt.relative_worktrees;
/* take ownership of repo_fmt.partial_clone */
the_repository->repository_format_partial_clone =
repo_fmt.partial_clone;
Expand Down Expand Up @@ -1957,6 +1962,8 @@ void check_repository_format(struct repository_format *fmt)
fmt->ref_storage_format);
the_repository->repository_format_worktree_config =
fmt->worktree_config;
the_repository->repository_format_relative_worktrees =
fmt->relative_worktrees;
the_repository->repository_format_partial_clone =
xstrdup_or_null(fmt->partial_clone);
clear_repository_format(&repo_fmt);
Expand Down Expand Up @@ -2211,8 +2218,8 @@ void initialize_repository_version(int hash_algo,
enum ref_storage_format ref_storage_format,
int reinit)
{
char repo_version_string[10];
int repo_version = GIT_REPO_VERSION;
struct strbuf repo_version = STRBUF_INIT;
int target_version = GIT_REPO_VERSION;

/*
* Note that we initialize the repository version to 1 when the ref
Expand All @@ -2223,12 +2230,7 @@ void initialize_repository_version(int hash_algo,
*/
if (hash_algo != GIT_HASH_SHA1 ||
ref_storage_format != REF_STORAGE_FORMAT_FILES)
repo_version = GIT_REPO_VERSION_READ;

/* This forces creation of new config file */
xsnprintf(repo_version_string, sizeof(repo_version_string),
"%d", repo_version);
git_config_set("core.repositoryformatversion", repo_version_string);
target_version = GIT_REPO_VERSION_READ;

if (hash_algo != GIT_HASH_SHA1 && hash_algo != GIT_HASH_UNKNOWN)
git_config_set("extensions.objectformat",
Expand All @@ -2241,6 +2243,25 @@ void initialize_repository_version(int hash_algo,
ref_storage_format_to_name(ref_storage_format));
else if (reinit)
git_config_set_gently("extensions.refstorage", NULL);

if (reinit) {
struct strbuf config = STRBUF_INIT;
struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;

strbuf_git_common_path(&config, the_repository, "config");
read_repository_format(&repo_fmt, config.buf);

if (repo_fmt.v1_only_extensions.nr)
target_version = GIT_REPO_VERSION_READ;

strbuf_release(&config);
clear_repository_format(&repo_fmt);
}

strbuf_addf(&repo_version, "%d", target_version);
git_config_set("core.repositoryformatversion", repo_version.buf);

strbuf_release(&repo_version);
}

static int is_reinit(void)
Expand Down Expand Up @@ -2340,7 +2361,7 @@ static int create_default_files(const char *template_path,
adjust_shared_perm(repo_get_git_dir(the_repository));
}

initialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, 0);
initialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, reinit);

/* Check filemode trustability */
path = git_path_buf(&buf, "config");
Expand Down
1 change: 1 addition & 0 deletions setup.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ struct repository_format {
int precious_objects;
char *partial_clone; /* value of extensions.partialclone */
int worktree_config;
int relative_worktrees;
int is_bare;
int hash_algo;
int compat_hash_algo;
Expand Down
22 changes: 18 additions & 4 deletions t/t0001-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,12 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '
sep_git_dir_worktree () {
test_when_finished "rm -rf mainwt linkwt seprepo" &&
git init mainwt &&
if test "relative" = $2
then
test_config -C mainwt worktree.useRelativePaths true
else
test_config -C mainwt worktree.useRelativePaths false
fi
test_commit -C mainwt gumby &&
git -C mainwt worktree add --detach ../linkwt &&
git -C "$1" init --separate-git-dir ../seprepo &&
Expand All @@ -441,12 +447,20 @@ sep_git_dir_worktree () {
test_cmp expect actual
}

test_expect_success 're-init to move gitdir with linked worktrees' '
sep_git_dir_worktree mainwt
test_expect_success 're-init to move gitdir with linked worktrees (absolute)' '
sep_git_dir_worktree mainwt absolute
'

test_expect_success 're-init to move gitdir within linked worktree (absolute)' '
sep_git_dir_worktree linkwt absolute
'

test_expect_success 're-init to move gitdir with linked worktrees (relative)' '
sep_git_dir_worktree mainwt relative
'

test_expect_success 're-init to move gitdir within linked worktree' '
sep_git_dir_worktree linkwt
test_expect_success 're-init to move gitdir within linked worktree (relative)' '
sep_git_dir_worktree linkwt relative
'

test_expect_success MINGW '.git hidden' '
Expand Down
45 changes: 45 additions & 0 deletions t/t2400-worktree-add.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1206,4 +1206,49 @@ test_expect_success '"add" with initialized submodule, with submodule.recurse se
git -C project-clone -c submodule.recurse worktree add ../project-5
'

test_expect_success 'can create worktrees with relative paths' '
test_when_finished "git worktree remove relative" &&
test_config worktree.useRelativePaths false &&
git worktree add --relative-paths ./relative &&
echo "gitdir: ../.git/worktrees/relative" >expect &&
test_cmp expect relative/.git &&
echo "../../../relative/.git" >expect &&
test_cmp expect .git/worktrees/relative/gitdir
'

test_expect_success 'can create worktrees with absolute paths' '
test_config worktree.useRelativePaths true &&
git worktree add ./relative &&
echo "gitdir: ../.git/worktrees/relative" >expect &&
test_cmp expect relative/.git &&
git worktree add --no-relative-paths ./absolute &&
echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect &&
test_cmp expect absolute/.git &&
echo "$(pwd)/absolute/.git" >expect &&
test_cmp expect .git/worktrees/absolute/gitdir
'

test_expect_success 'move repo without breaking relative internal links' '
test_when_finished rm -rf repo moved &&
git init repo &&
(
cd repo &&
test_commit initial &&
git worktree add --relative-paths wt1 &&
cd .. &&
mv repo moved &&
cd moved/wt1 &&
git worktree list >out 2>err &&
test_must_be_empty err
)
'

test_expect_success 'relative worktree sets extension config' '
test_when_finished "rm -rf repo" &&
git init repo &&
git -C repo commit --allow-empty -m base &&
git -C repo worktree add --relative-paths ./foo &&
test_cmp_config -C repo 1 core.repositoryformatversion
'

test_done
3 changes: 2 additions & 1 deletion t/t2401-worktree-prune.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,12 @@ test_expect_success 'prune duplicate (main/linked)' '
! test -d .git/worktrees/wt
'

test_expect_success 'not prune proper worktrees when run inside linked worktree' '
test_expect_success 'not prune proper worktrees inside linked worktree with relative paths' '
test_when_finished rm -rf repo wt_ext &&
git init repo &&
(
cd repo &&
git config worktree.useRelativePaths true &&
echo content >file &&
git add file &&
git commit -m msg &&
Expand Down
Loading

0 comments on commit 3cdca37

Please sign in to comment.