Skip to content

Commit afe4040

Browse files
committed
Merge branch 'cw/worktree-extension' into seen
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 1fa32ad + 298327f commit afe4040

18 files changed

+331
-142
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ 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+
Overrides the `worktree.useRelativePaths` config option, see
221+
linkgit:git-config[1].
222+
+
223+
With `repair`, the linking files will be updated if there's an absolute/relative
224+
mismatch, even if the links are correct.
225+
219226
--[no-]track::
220227
When creating a new branch, if `<commit-ish>` is a branch,
221228
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);
@@ -414,8 +419,7 @@ static int add_worktree(const char *path, const char *refname,
414419
const struct add_opts *opts)
415420
{
416421
struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
417-
struct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT;
418-
struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT;
422+
struct strbuf sb = STRBUF_INIT;
419423
const char *name;
420424
struct strvec child_env = STRVEC_INIT;
421425
unsigned int counter = 0;
@@ -491,10 +495,7 @@ static int add_worktree(const char *path, const char *refname,
491495

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

579580
strvec_clear(&child_env);
580581
strbuf_release(&sb);
581-
strbuf_release(&sb_tmp);
582582
strbuf_release(&symref);
583583
strbuf_release(&sb_repo);
584-
strbuf_release(&sb_repo_realpath);
585584
strbuf_release(&sb_git);
586-
strbuf_release(&sb_path_realpath);
587585
strbuf_release(&sb_name);
588586
free_worktree(wt);
589587
return ret;
@@ -796,12 +794,15 @@ static int add(int ac, const char **av, const char *prefix)
796794
PARSE_OPT_NOARG | PARSE_OPT_OPTARG),
797795
OPT_BOOL(0, "guess-remote", &guess_remote,
798796
N_("try to match the new branch name with a remote-tracking branch")),
797+
OPT_BOOL(0, "relative-paths", &opts.relative_paths,
798+
N_("use relative paths for worktrees")),
799799
OPT_END()
800800
};
801801
int ret;
802802

803803
memset(&opts, 0, sizeof(opts));
804804
opts.checkout = 1;
805+
opts.relative_paths = use_relative_paths;
805806
ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);
806807
if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
807808
die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
@@ -1189,6 +1190,8 @@ static int move_worktree(int ac, const char **av, const char *prefix)
11891190
OPT__FORCE(&force,
11901191
N_("force move even if worktree is dirty or locked"),
11911192
PARSE_OPT_NOCOMPLETE),
1193+
OPT_BOOL(0, "relative-paths", &use_relative_paths,
1194+
N_("use relative paths for worktrees")),
11921195
OPT_END()
11931196
};
11941197
struct worktree **worktrees, *wt;
@@ -1241,7 +1244,7 @@ static int move_worktree(int ac, const char **av, const char *prefix)
12411244
if (rename(wt->path, dst.buf) == -1)
12421245
die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf);
12431246

1244-
update_worktree_location(wt, dst.buf);
1247+
update_worktree_location(wt, dst.buf, use_relative_paths);
12451248

12461249
strbuf_release(&dst);
12471250
free_worktrees(worktrees);
@@ -1382,15 +1385,17 @@ static int repair(int ac, const char **av, const char *prefix)
13821385
const char **p;
13831386
const char *self[] = { ".", NULL };
13841387
struct option options[] = {
1388+
OPT_BOOL(0, "relative-paths", &use_relative_paths,
1389+
N_("use relative paths for worktrees")),
13851390
OPT_END()
13861391
};
13871392
int rc = 0;
13881393

13891394
ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0);
13901395
p = ac > 0 ? av : self;
13911396
for (; *p; p++)
1392-
repair_worktree_at_path(*p, report_repair, &rc);
1393-
repair_worktrees(report_repair, &rc);
1397+
repair_worktree_at_path(*p, report_repair, &rc, use_relative_paths);
1398+
repair_worktrees(report_repair, &rc, use_relative_paths);
13941399
return rc;
13951400
}
13961401

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
@@ -434,6 +434,12 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '
434434
sep_git_dir_worktree () {
435435
test_when_finished "rm -rf mainwt linkwt seprepo" &&
436436
git init mainwt &&
437+
if test "relative" = $2
438+
then
439+
git -C mainwt config worktree.useRelativePaths true
440+
else
441+
git -C mainwt config worktree.useRelativePaths false
442+
fi
437443
test_commit -C mainwt gumby &&
438444
git -C mainwt worktree add --detach ../linkwt &&
439445
git -C "$1" init --separate-git-dir ../seprepo &&
@@ -442,12 +448,20 @@ sep_git_dir_worktree () {
442448
test_cmp expect actual
443449
}
444450

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

449-
test_expect_success 're-init to move gitdir within linked worktree' '
450-
sep_git_dir_worktree linkwt
463+
test_expect_success 're-init to move gitdir within linked worktree (relative)' '
464+
sep_git_dir_worktree linkwt relative
451465
'
452466

453467
test_expect_success MINGW '.git hidden' '

t/t2400-worktree-add.sh

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

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

0 commit comments

Comments
 (0)