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