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 7 such a branch exists, it is checked out and set as "upstream" 8 8 for the new branch. If no such match can be found, it falls 9 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 216 This can also be set up as the default behaviour by using the 217 217 `worktree.guessRemote` config option. 218 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 + 219 224 --[no-]track:: 220 225 When creating a new branch, if `<commit-ish>` is a branch, 221 226 mark it as "upstream" from the new branch. This is the
+10 -9
builtin/worktree.c
··· 120 120 int quiet; 121 121 int checkout; 122 122 int orphan; 123 + int relative_paths; 123 124 const char *keep_locked; 124 125 }; 125 126 126 127 static int show_only; 127 128 static int verbose; 128 129 static int guess_remote; 130 + static int use_relative_paths; 129 131 static timestamp_t expire; 130 132 131 133 static int git_worktree_config(const char *var, const char *value, ··· 133 135 { 134 136 if (!strcmp(var, "worktree.guessremote")) { 135 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); 136 141 return 0; 137 142 } 138 143 ··· 414 419 const struct add_opts *opts) 415 420 { 416 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; 419 423 const char *name; 420 424 struct strvec child_env = STRVEC_INIT; 421 425 unsigned int counter = 0; ··· 491 495 492 496 strbuf_reset(&sb); 493 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); 498 499 strbuf_reset(&sb); 499 500 strbuf_addf(&sb, "%s/commondir", sb_repo.buf); 500 501 write_file(sb.buf, "../.."); ··· 578 579 579 580 strvec_clear(&child_env); 580 581 strbuf_release(&sb); 581 - strbuf_release(&sb_tmp); 582 582 strbuf_release(&symref); 583 583 strbuf_release(&sb_repo); 584 - strbuf_release(&sb_repo_realpath); 585 584 strbuf_release(&sb_git); 586 - strbuf_release(&sb_path_realpath); 587 585 strbuf_release(&sb_name); 588 586 free_worktree(wt); 589 587 return ret; ··· 796 794 PARSE_OPT_NOARG | PARSE_OPT_OPTARG), 797 795 OPT_BOOL(0, "guess-remote", &guess_remote, 798 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 799 OPT_END() 800 800 }; 801 801 int ret; 802 802 803 803 memset(&opts, 0, sizeof(opts)); 804 804 opts.checkout = 1; 805 + opts.relative_paths = use_relative_paths; 805 806 ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0); 806 807 if (!!opts.detach + !!new_branch + !!new_branch_force > 1) 807 808 die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
+46
t/t2400-worktree-add.sh
··· 1207 1207 git -C project-clone -c submodule.recurse worktree add ../project-5 1208 1208 ' 1209 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 + 1210 1256 test_done
+2 -1
t/t2401-worktree-prune.sh
··· 120 120 ! test -d .git/worktrees/wt 121 121 ' 122 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 124 test_when_finished rm -rf repo wt_ext && 125 125 git init repo && 126 126 ( 127 127 cd repo && 128 + git config worktree.useRelativePaths true && 128 129 echo content >file && 129 130 git add file && 130 131 git commit -m msg &&
+22
t/t2402-worktree-list.sh
··· 261 261 ' 262 262 263 263 test_expect_success 'linked worktrees are sorted' ' 264 + test_when_finished "rm -rf sorted" && 264 265 mkdir sorted && 265 266 git init sorted/main && 266 267 ( ··· 268 269 test_tick && 269 270 test_commit new && 270 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 && 271 293 git worktree add ../second && 272 294 git worktree list --porcelain >out && 273 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