Skip to content

Commit 3b11c91

Browse files
committed
Merge branch 'cw/worktree-extension'
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
2 parents cd0a222 + 2037ca8 commit 3b11c91

18 files changed

+334
-141
lines changed

Documentation/config/extensions.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ Note that this setting should only be set by linkgit:git-init[1] or
6363
linkgit:git-clone[1]. Trying to change it after initialization will not
6464
work and will produce hard-to-diagnose issues.
6565

66+
relativeWorktrees::
67+
If enabled, indicates at least one worktree has been linked with
68+
relative paths. Automatically set if a worktree has been created or
69+
repaired with either the `--relative-paths` option or with the
70+
`worktree.useRelativePaths` config set to `true`.
71+
6672
worktreeConfig::
6773
If enabled, then worktrees will load config settings from the
6874
`$GIT_DIR/config.worktree` file in addition to the

Documentation/config/worktree.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ worktree.guessRemote::
77
such a branch exists, it is checked out and set as "upstream"
88
for the new branch. If no such match can be found, it falls
99
back to creating a new branch from the current HEAD.
10+
11+
worktree.useRelativePaths::
12+
Link worktrees using relative paths (when "true") or absolute
13+
paths (when "false"). This is particularly useful for setups
14+
where the repository and worktrees may be moved between
15+
different locations or environments. Defaults to "false".
16+
+
17+
Note that setting `worktree.useRelativePaths` to "true" implies enabling the
18+
`extension.relativeWorktrees` config (see linkgit:git-config[1]),
19+
thus making it incompatible with older versions of Git.

Documentation/git-worktree.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ To remove a locked worktree, specify `--force` twice.
216216
This can also be set up as the default behaviour by using the
217217
`worktree.guessRemote` config option.
218218

219+
--[no-]relative-paths::
220+
Link worktrees using relative paths or absolute paths (default).
221+
Overrides the `worktree.useRelativePaths` config option, see
222+
linkgit:git-config[1].
223+
+
224+
With `repair`, the linking files will be updated if there's an absolute/relative
225+
mismatch, even if the links are correct.
226+
219227
--[no-]track::
220228
When creating a new branch, if `<commit-ish>` is a branch,
221229
mark it as "upstream" from the new branch. This is the

builtin/worktree.c

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,14 @@ struct add_opts {
120120
int quiet;
121121
int checkout;
122122
int orphan;
123+
int relative_paths;
123124
const char *keep_locked;
124125
};
125126

126127
static int show_only;
127128
static int verbose;
128129
static int guess_remote;
130+
static int use_relative_paths;
129131
static timestamp_t expire;
130132

131133
static int git_worktree_config(const char *var, const char *value,
@@ -134,6 +136,9 @@ static int git_worktree_config(const char *var, const char *value,
134136
if (!strcmp(var, "worktree.guessremote")) {
135137
guess_remote = git_config_bool(var, value);
136138
return 0;
139+
} else if (!strcmp(var, "worktree.userelativepaths")) {
140+
use_relative_paths = git_config_bool(var, value);
141+
return 0;
137142
}
138143

139144
return git_default_config(var, value, ctx, cb);
@@ -415,8 +420,7 @@ static int add_worktree(const char *path, const char *refname,
415420
const struct add_opts *opts)
416421
{
417422
struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
418-
struct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT;
419-
struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT;
423+
struct strbuf sb = STRBUF_INIT;
420424
const char *name;
421425
struct strvec child_env = STRVEC_INIT;
422426
unsigned int counter = 0;
@@ -492,10 +496,7 @@ static int add_worktree(const char *path, const char *refname,
492496

493497
strbuf_reset(&sb);
494498
strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
495-
strbuf_realpath(&sb_path_realpath, path, 1);
496-
strbuf_realpath(&sb_repo_realpath, sb_repo.buf, 1);
497-
write_file(sb.buf, "%s/.git", relative_path(sb_path_realpath.buf, sb_repo_realpath.buf, &sb_tmp));
498-
write_file(sb_git.buf, "gitdir: %s", relative_path(sb_repo_realpath.buf, sb_path_realpath.buf, &sb_tmp));
499+
write_worktree_linking_files(sb_git, sb, opts->relative_paths);
499500
strbuf_reset(&sb);
500501
strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
501502
write_file(sb.buf, "../..");
@@ -579,12 +580,9 @@ static int add_worktree(const char *path, const char *refname,
579580

580581
strvec_clear(&child_env);
581582
strbuf_release(&sb);
582-
strbuf_release(&sb_tmp);
583583
strbuf_release(&symref);
584584
strbuf_release(&sb_repo);
585-
strbuf_release(&sb_repo_realpath);
586585
strbuf_release(&sb_git);
587-
strbuf_release(&sb_path_realpath);
588586
strbuf_release(&sb_name);
589587
free_worktree(wt);
590588
return ret;
@@ -798,12 +796,15 @@ static int add(int ac, const char **av, const char *prefix,
798796
PARSE_OPT_NOARG | PARSE_OPT_OPTARG),
799797
OPT_BOOL(0, "guess-remote", &guess_remote,
800798
N_("try to match the new branch name with a remote-tracking branch")),
799+
OPT_BOOL(0, "relative-paths", &opts.relative_paths,
800+
N_("use relative paths for worktrees")),
801801
OPT_END()
802802
};
803803
int ret;
804804

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

1250-
update_worktree_location(wt, dst.buf);
1253+
update_worktree_location(wt, dst.buf, use_relative_paths);
12511254

12521255
strbuf_release(&dst);
12531256
free_worktrees(worktrees);
@@ -1390,15 +1393,17 @@ static int repair(int ac, const char **av, const char *prefix,
13901393
const char **p;
13911394
const char *self[] = { ".", NULL };
13921395
struct option options[] = {
1396+
OPT_BOOL(0, "relative-paths", &use_relative_paths,
1397+
N_("use relative paths for worktrees")),
13931398
OPT_END()
13941399
};
13951400
int rc = 0;
13961401

13971402
ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0);
13981403
p = ac > 0 ? av : self;
13991404
for (; *p; p++)
1400-
repair_worktree_at_path(*p, report_repair, &rc);
1401-
repair_worktrees(report_repair, &rc);
1405+
repair_worktree_at_path(*p, report_repair, &rc, use_relative_paths);
1406+
repair_worktrees(report_repair, &rc, use_relative_paths);
14021407
return rc;
14031408
}
14041409

repository.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ int repo_init(struct repository *repo,
283283
repo_set_compat_hash_algo(repo, format.compat_hash_algo);
284284
repo_set_ref_storage_format(repo, format.ref_storage_format);
285285
repo->repository_format_worktree_config = format.worktree_config;
286+
repo->repository_format_relative_worktrees = format.relative_worktrees;
286287

287288
/* take ownership of format.partial_clone */
288289
repo->repository_format_partial_clone = format.partial_clone;

repository.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ struct repository {
150150

151151
/* Configurations */
152152
int repository_format_worktree_config;
153+
int repository_format_relative_worktrees;
153154

154155
/* Indicate if a repository has a different 'commondir' from 'gitdir' */
155156
unsigned different_commondir:1;

setup.c

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,9 @@ static enum extension_result handle_extension(const char *var,
683683
"extensions.refstorage", value);
684684
data->ref_storage_format = format;
685685
return EXTENSION_OK;
686+
} else if (!strcmp(ext, "relativeworktrees")) {
687+
data->relative_worktrees = git_config_bool(var, value);
688+
return EXTENSION_OK;
686689
}
687690
return EXTENSION_UNKNOWN;
688691
}
@@ -1854,6 +1857,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
18541857
repo_fmt.ref_storage_format);
18551858
the_repository->repository_format_worktree_config =
18561859
repo_fmt.worktree_config;
1860+
the_repository->repository_format_relative_worktrees =
1861+
repo_fmt.relative_worktrees;
18571862
/* take ownership of repo_fmt.partial_clone */
18581863
the_repository->repository_format_partial_clone =
18591864
repo_fmt.partial_clone;
@@ -1950,6 +1955,8 @@ void check_repository_format(struct repository_format *fmt)
19501955
fmt->ref_storage_format);
19511956
the_repository->repository_format_worktree_config =
19521957
fmt->worktree_config;
1958+
the_repository->repository_format_relative_worktrees =
1959+
fmt->relative_worktrees;
19531960
the_repository->repository_format_partial_clone =
19541961
xstrdup_or_null(fmt->partial_clone);
19551962
clear_repository_format(&repo_fmt);
@@ -2204,8 +2211,8 @@ void initialize_repository_version(int hash_algo,
22042211
enum ref_storage_format ref_storage_format,
22052212
int reinit)
22062213
{
2207-
char repo_version_string[10];
2208-
int repo_version = GIT_REPO_VERSION;
2214+
struct strbuf repo_version = STRBUF_INIT;
2215+
int target_version = GIT_REPO_VERSION;
22092216

22102217
/*
22112218
* Note that we initialize the repository version to 1 when the ref
@@ -2216,12 +2223,7 @@ void initialize_repository_version(int hash_algo,
22162223
*/
22172224
if (hash_algo != GIT_HASH_SHA1 ||
22182225
ref_storage_format != REF_STORAGE_FORMAT_FILES)
2219-
repo_version = GIT_REPO_VERSION_READ;
2220-
2221-
/* This forces creation of new config file */
2222-
xsnprintf(repo_version_string, sizeof(repo_version_string),
2223-
"%d", repo_version);
2224-
git_config_set("core.repositoryformatversion", repo_version_string);
2226+
target_version = GIT_REPO_VERSION_READ;
22252227

22262228
if (hash_algo != GIT_HASH_SHA1 && hash_algo != GIT_HASH_UNKNOWN)
22272229
git_config_set("extensions.objectformat",
@@ -2234,6 +2236,25 @@ void initialize_repository_version(int hash_algo,
22342236
ref_storage_format_to_name(ref_storage_format));
22352237
else if (reinit)
22362238
git_config_set_gently("extensions.refstorage", NULL);
2239+
2240+
if (reinit) {
2241+
struct strbuf config = STRBUF_INIT;
2242+
struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;
2243+
2244+
strbuf_git_common_path(&config, the_repository, "config");
2245+
read_repository_format(&repo_fmt, config.buf);
2246+
2247+
if (repo_fmt.v1_only_extensions.nr)
2248+
target_version = GIT_REPO_VERSION_READ;
2249+
2250+
strbuf_release(&config);
2251+
clear_repository_format(&repo_fmt);
2252+
}
2253+
2254+
strbuf_addf(&repo_version, "%d", target_version);
2255+
git_config_set("core.repositoryformatversion", repo_version.buf);
2256+
2257+
strbuf_release(&repo_version);
22372258
}
22382259

22392260
static int is_reinit(void)
@@ -2333,7 +2354,7 @@ static int create_default_files(const char *template_path,
23332354
adjust_shared_perm(repo_get_git_dir(the_repository));
23342355
}
23352356

2336-
initialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, 0);
2357+
initialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, reinit);
23372358

23382359
/* Check filemode trustability */
23392360
path = git_path_buf(&buf, "config");

setup.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ struct repository_format {
129129
int precious_objects;
130130
char *partial_clone; /* value of extensions.partialclone */
131131
int worktree_config;
132+
int relative_worktrees;
132133
int is_bare;
133134
int hash_algo;
134135
int compat_hash_algo;

t/t0001-init.sh

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,12 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '
433433
sep_git_dir_worktree () {
434434
test_when_finished "rm -rf mainwt linkwt seprepo" &&
435435
git init mainwt &&
436+
if test "relative" = $2
437+
then
438+
test_config -C mainwt worktree.useRelativePaths true
439+
else
440+
test_config -C mainwt worktree.useRelativePaths false
441+
fi
436442
test_commit -C mainwt gumby &&
437443
git -C mainwt worktree add --detach ../linkwt &&
438444
git -C "$1" init --separate-git-dir ../seprepo &&
@@ -441,12 +447,20 @@ sep_git_dir_worktree () {
441447
test_cmp expect actual
442448
}
443449

444-
test_expect_success 're-init to move gitdir with linked worktrees' '
445-
sep_git_dir_worktree mainwt
450+
test_expect_success 're-init to move gitdir with linked worktrees (absolute)' '
451+
sep_git_dir_worktree mainwt absolute
452+
'
453+
454+
test_expect_success 're-init to move gitdir within linked worktree (absolute)' '
455+
sep_git_dir_worktree linkwt absolute
456+
'
457+
458+
test_expect_success 're-init to move gitdir with linked worktrees (relative)' '
459+
sep_git_dir_worktree mainwt relative
446460
'
447461

448-
test_expect_success 're-init to move gitdir within linked worktree' '
449-
sep_git_dir_worktree linkwt
462+
test_expect_success 're-init to move gitdir within linked worktree (relative)' '
463+
sep_git_dir_worktree linkwt relative
450464
'
451465

452466
test_expect_success MINGW '.git hidden' '

t/t2400-worktree-add.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,4 +1206,50 @@ test_expect_success '"add" with initialized submodule, with submodule.recurse se
12061206
git -C project-clone -c submodule.recurse worktree add ../project-5
12071207
'
12081208

1209+
test_expect_success 'can create worktrees with relative paths' '
1210+
test_when_finished "git worktree remove relative" &&
1211+
test_config worktree.useRelativePaths false &&
1212+
git worktree add --relative-paths ./relative &&
1213+
echo "gitdir: ../.git/worktrees/relative" >expect &&
1214+
test_cmp expect relative/.git &&
1215+
echo "../../../relative/.git" >expect &&
1216+
test_cmp expect .git/worktrees/relative/gitdir
1217+
'
1218+
1219+
test_expect_success 'can create worktrees with absolute paths' '
1220+
test_config worktree.useRelativePaths true &&
1221+
git worktree add ./relative &&
1222+
echo "gitdir: ../.git/worktrees/relative" >expect &&
1223+
test_cmp expect relative/.git &&
1224+
git worktree add --no-relative-paths ./absolute &&
1225+
echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect &&
1226+
test_cmp expect absolute/.git &&
1227+
echo "$(pwd)/absolute/.git" >expect &&
1228+
test_cmp expect .git/worktrees/absolute/gitdir
1229+
'
1230+
1231+
test_expect_success 'move repo without breaking relative internal links' '
1232+
test_when_finished rm -rf repo moved &&
1233+
git init repo &&
1234+
(
1235+
cd repo &&
1236+
test_commit initial &&
1237+
git worktree add --relative-paths wt1 &&
1238+
cd .. &&
1239+
mv repo moved &&
1240+
cd moved/wt1 &&
1241+
git worktree list >out 2>err &&
1242+
test_must_be_empty err
1243+
)
1244+
'
1245+
1246+
test_expect_success 'relative worktree sets extension config' '
1247+
test_when_finished "rm -rf repo" &&
1248+
git init repo &&
1249+
git -C repo commit --allow-empty -m base &&
1250+
git -C repo worktree add --relative-paths ./foo &&
1251+
test_cmp_config -C repo 1 core.repositoryformatversion &&
1252+
test_cmp_config -C repo true extensions.relativeworktrees
1253+
'
1254+
12091255
test_done

0 commit comments

Comments
 (0)