worktree: add relative cli/config options to `add` command
This introduces the `--[no-]relative-paths` CLI option and `worktree.useRelativePaths` configuration setting to the `worktree add` command. When enabled these options allow worktrees to be linked using relative paths, enhancing portability across environments where absolute paths may differ (e.g., containerized setups, shared network drives). Git still creates absolute paths by default, but these options allow users to opt-in to relative paths if desired. The t2408 test file is removed and more comprehensive tests are written for the various worktree operations in their own files. Signed-off-by: Caleb White <cdwhite3@pm.me> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Caleb White committed
Nov 29, 2024 at 22:22 UTC
b7016344f1d4ef8457821e0436f335ec388b8dac
7 files changed
+95
-49
Documentation/config/worktree.txt
+10
@@ -7,3 +7,13 @@ worktree.guessRemote::
7
such a branch exists, it is checked out and set as "upstream"
8
for the new branch. If no such match can be found, it falls
9
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
+5
@@ -216,6 +216,11 @@ To remove a locked worktree, specify `--force` twice.
216
This can also be set up as the default behaviour by using the
217
`worktree.guessRemote` config option.
218
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
--[no-]track::
225
When creating a new branch, if `<commit-ish>` is a branch,
226
mark it as "upstream" from the new branch. This is the
builtin/worktree.c
+10
-9
@@ -120,12 +120,14 @@ struct add_opts {
120
int quiet;
121
int checkout;
122
int orphan;
123
+ int relative_paths;
124
const char *keep_locked;
125
};
126
127
static int show_only;
128
static int verbose;
129
static int guess_remote;
130
+static int use_relative_paths;
131
static timestamp_t expire;
132
133
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,
136
if (!strcmp(var, "worktree.guessremote")) {
137
guess_remote = git_config_bool(var, value);
138
return 0;
139
+ } else if (!strcmp(var, "worktree.userelativepaths")) {
140
+ use_relative_paths = git_config_bool(var, value);
141
+ return 0;
142
}
143
144
return git_default_config(var, value, ctx, cb);
@@ -414,8 +419,7 @@ static int add_worktree(const char *path, const char *refname,
419
const struct add_opts *opts)
420
{
421
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;
423
const char *name;
424
struct strvec child_env = STRVEC_INIT;
425
unsigned int counter = 0;
@@ -491,10 +495,7 @@ static int add_worktree(const char *path, const char *refname,
495
496
strbuf_reset(&sb);
497
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);
499
strbuf_reset(&sb);
500
strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
501
write_file(sb.buf, "../..");
@@ -578,12 +579,9 @@ done:
579
580
strvec_clear(&child_env);
581
strbuf_release(&sb);
581
- strbuf_release(&sb_tmp);
582
strbuf_release(&symref);
583
strbuf_release(&sb_repo);
584
- strbuf_release(&sb_repo_realpath);
584
strbuf_release(&sb_git);
586
- strbuf_release(&sb_path_realpath);
585
strbuf_release(&sb_name);
586
free_worktree(wt);
587
return ret;
@@ -796,12 +794,15 @@ static int add(int ac, const char **av, const char *prefix)
794
PARSE_OPT_NOARG | PARSE_OPT_OPTARG),
795
OPT_BOOL(0, "guess-remote", &guess_remote,
796
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")),
799
OPT_END()
800
};
801
int ret;
802
803
memset(&opts, 0, sizeof(opts));
804
opts.checkout = 1;
805
+ opts.relative_paths = use_relative_paths;
806
ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);
807
if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
808
die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
t/t2400-worktree-add.sh
+46
@@ -1207,4 +1207,50 @@ test_expect_success '"add" with initialized submodule, with submodule.recurse se
1207
git -C project-clone -c submodule.recurse worktree add ../project-5
1208
'
1209
1210
+test_expect_success 'can create worktrees with relative paths' '
1211
+ test_when_finished "git worktree remove relative" &&
1212
+ test_config worktree.useRelativePaths false &&
1213
+ git worktree add --relative-paths ./relative &&
1214
+ echo "gitdir: ../.git/worktrees/relative" >expect &&
1215
+ test_cmp expect relative/.git &&
1216
+ echo "../../../relative/.git" >expect &&
1217
+ test_cmp expect .git/worktrees/relative/gitdir
1218
+'
1219
+
1220
+test_expect_success 'can create worktrees with absolute paths' '
1221
+ test_config worktree.useRelativePaths true &&
1222
+ git worktree add ./relative &&
1223
+ echo "gitdir: ../.git/worktrees/relative" >expect &&
1224
+ test_cmp expect relative/.git &&
1225
+ git worktree add --no-relative-paths ./absolute &&
1226
+ echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect &&
1227
+ test_cmp expect absolute/.git &&
1228
+ echo "$(pwd)/absolute/.git" >expect &&
1229
+ test_cmp expect .git/worktrees/absolute/gitdir
1230
+'
1231
+
1232
+test_expect_success 'move repo without breaking relative internal links' '
1233
+ test_when_finished rm -rf repo moved &&
1234
+ git init repo &&
1235
+ (
1236
+ cd repo &&
1237
+ test_commit initial &&
1238
+ git worktree add --relative-paths wt1 &&
1239
+ cd .. &&
1240
+ mv repo moved &&
1241
+ cd moved/wt1 &&
1242
+ git worktree list >out 2>err &&
1243
+ test_must_be_empty err
1244
+ )
1245
+'
1246
+
1247
+test_expect_success 'relative worktree sets extension config' '
1248
+ test_when_finished "rm -rf repo" &&
1249
+ git init repo &&
1250
+ git -C repo commit --allow-empty -m base &&
1251
+ git -C repo worktree add --relative-paths ./foo &&
1252
+ test_cmp_config -C repo 1 core.repositoryformatversion &&
1253
+ test_cmp_config -C repo true extensions.relativeworktrees
1254
+'
1255
+
1256
test_done
t/t2401-worktree-prune.sh
+2
-1
@@ -120,11 +120,12 @@ test_expect_success 'prune duplicate (main/linked)' '
120
! test -d .git/worktrees/wt
121
'
122
123
-test_expect_success 'not prune proper worktrees when run inside linked worktree' '
123
+test_expect_success 'not prune proper worktrees inside linked worktree with relative paths' '
124
test_when_finished rm -rf repo wt_ext &&
125
git init repo &&
126
(
127
cd repo &&
128
+ git config worktree.useRelativePaths true &&
129
echo content >file &&
130
git add file &&
131
git commit -m msg &&
t/t2402-worktree-list.sh
+22
@@ -261,6 +261,7 @@ test_expect_success 'broken main worktree still at the top' '
261
'
262
263
test_expect_success 'linked worktrees are sorted' '
264
+ test_when_finished "rm -rf sorted" &&
265
mkdir sorted &&
266
git init sorted/main &&
267
(
@@ -280,6 +281,27 @@ test_expect_success 'linked worktrees are sorted' '
281
test_cmp expected sorted/main/actual
282
'
283
284
+test_expect_success 'linked worktrees with relative paths are shown with absolute paths' '
285
+ test_when_finished "rm -rf sorted" &&
286
+ mkdir sorted &&
287
+ git init sorted/main &&
288
+ (
289
+ cd sorted/main &&
290
+ test_tick &&
291
+ test_commit new &&
292
+ git worktree add --relative-paths ../first &&
293
+ git worktree add ../second &&
294
+ git worktree list --porcelain >out &&
295
+ grep ^worktree out >actual
296
+ ) &&
297
+ cat >expected <<-EOF &&
298
+ worktree $(pwd)/sorted/main
299
+ worktree $(pwd)/sorted/first
300
+ worktree $(pwd)/sorted/second
301
+ EOF
302
+ test_cmp expected sorted/main/actual
303
+'
304
+
305
test_expect_success 'worktree path when called in .git directory' '
306
git worktree list >list1 &&
307
git -C .git worktree list >list2 &&
t/t2408-worktree-relative.sh
deleted
-39
@@ -1,39 +0,0 @@
1
-#!/bin/sh
2
-
3
-test_description='test worktrees linked with relative paths'
4
-
5
-TEST_PASSES_SANITIZE_LEAK=true
6
-. ./test-lib.sh
7
-
8
-test_expect_success 'links worktrees with relative paths' '
9
- test_when_finished rm -rf repo &&
10
- git init repo &&
11
- (
12
- cd repo &&
13
- test_commit initial &&
14
- git worktree add wt1 &&
15
- echo "../../../wt1/.git" >expected_gitdir &&
16
- cat .git/worktrees/wt1/gitdir >actual_gitdir &&
17
- echo "gitdir: ../.git/worktrees/wt1" >expected_git &&
18
- cat wt1/.git >actual_git &&
19
- test_cmp expected_gitdir actual_gitdir &&
20
- test_cmp expected_git actual_git
21
- )
22
-'
23
-
24
-test_expect_success 'move repo without breaking relative internal links' '
25
- test_when_finished rm -rf repo moved &&
26
- git init repo &&
27
- (
28
- cd repo &&
29
- test_commit initial &&
30
- git worktree add wt1 &&
31
- cd .. &&
32
- mv repo moved &&
33
- cd moved/wt1 &&
34
- git status >out 2>err &&
35
- test_must_be_empty err
36
- )
37
-'
38
-
39
-test_done