Git fork

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>

authored by

Caleb White and committed by
Junio C Hamano
b7016344 4dac9e3c

+95 -49
+10
Documentation/config/worktree.txt
··· 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.
··· 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.
+5
Documentation/git-worktree.txt
··· 216 This can also be set up as the default behaviour by using the 217 `worktree.guessRemote` config option. 218 219 --[no-]track:: 220 When creating a new branch, if `<commit-ish>` is a branch, 221 mark it as "upstream" from the new branch. This is the
··· 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
+10 -9
builtin/worktree.c
··· 120 int quiet; 121 int checkout; 122 int orphan; 123 const char *keep_locked; 124 }; 125 126 static int show_only; 127 static int verbose; 128 static int guess_remote; 129 static timestamp_t expire; 130 131 static int git_worktree_config(const char *var, const char *value, ··· 133 { 134 if (!strcmp(var, "worktree.guessremote")) { 135 guess_remote = git_config_bool(var, value); 136 return 0; 137 } 138 ··· 414 const struct add_opts *opts) 415 { 416 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; 419 const char *name; 420 struct strvec child_env = STRVEC_INIT; 421 unsigned int counter = 0; ··· 491 492 strbuf_reset(&sb); 493 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 strbuf_reset(&sb); 499 strbuf_addf(&sb, "%s/commondir", sb_repo.buf); 500 write_file(sb.buf, "../.."); ··· 578 579 strvec_clear(&child_env); 580 strbuf_release(&sb); 581 - strbuf_release(&sb_tmp); 582 strbuf_release(&symref); 583 strbuf_release(&sb_repo); 584 - strbuf_release(&sb_repo_realpath); 585 strbuf_release(&sb_git); 586 - strbuf_release(&sb_path_realpath); 587 strbuf_release(&sb_name); 588 free_worktree(wt); 589 return ret; ··· 796 PARSE_OPT_NOARG | PARSE_OPT_OPTARG), 797 OPT_BOOL(0, "guess-remote", &guess_remote, 798 N_("try to match the new branch name with a remote-tracking branch")), 799 OPT_END() 800 }; 801 int ret; 802 803 memset(&opts, 0, sizeof(opts)); 804 opts.checkout = 1; 805 ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0); 806 if (!!opts.detach + !!new_branch + !!new_branch_force > 1) 807 die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
··· 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, ··· 135 { 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 ··· 419 const struct add_opts *opts) 420 { 421 struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT; 422 + struct strbuf sb = STRBUF_INIT; 423 const char *name; 424 struct strvec child_env = STRVEC_INIT; 425 unsigned int counter = 0; ··· 495 496 strbuf_reset(&sb); 497 strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); 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, "../.."); ··· 579 580 strvec_clear(&child_env); 581 strbuf_release(&sb); 582 strbuf_release(&symref); 583 strbuf_release(&sb_repo); 584 strbuf_release(&sb_git); 585 strbuf_release(&sb_name); 586 free_worktree(wt); 587 return ret; ··· 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");
+46
t/t2400-worktree-add.sh
··· 1207 git -C project-clone -c submodule.recurse worktree add ../project-5 1208 ' 1209 1210 test_done
··· 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
+2 -1
t/t2401-worktree-prune.sh
··· 120 ! test -d .git/worktrees/wt 121 ' 122 123 - test_expect_success 'not prune proper worktrees when run inside linked worktree' ' 124 test_when_finished rm -rf repo wt_ext && 125 git init repo && 126 ( 127 cd repo && 128 echo content >file && 129 git add file && 130 git commit -m msg &&
··· 120 ! test -d .git/worktrees/wt 121 ' 122 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 &&
+22
t/t2402-worktree-list.sh
··· 261 ' 262 263 test_expect_success 'linked worktrees are sorted' ' 264 mkdir sorted && 265 git init sorted/main && 266 ( ··· 268 test_tick && 269 test_commit new && 270 git worktree add ../first && 271 git worktree add ../second && 272 git worktree list --porcelain >out && 273 grep ^worktree out >actual
··· 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 ( ··· 269 test_tick && 270 test_commit new && 271 git worktree add ../first && 272 + git worktree add ../second && 273 + git worktree list --porcelain >out && 274 + grep ^worktree out >actual 275 + ) && 276 + cat >expected <<-EOF && 277 + worktree $(pwd)/sorted/main 278 + worktree $(pwd)/sorted/first 279 + worktree $(pwd)/sorted/second 280 + EOF 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
-39
t/t2408-worktree-relative.sh
··· 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
···