Git fork

Merge branch 'cw/worktree-extension'

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

+334 -141
+6
Documentation/config/extensions.txt
··· 63 63 linkgit:git-clone[1]. Trying to change it after initialization will not 64 64 work and will produce hard-to-diagnose issues. 65 65 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 + 66 72 worktreeConfig:: 67 73 If enabled, then worktrees will load config settings from the 68 74 `$GIT_DIR/config.worktree` file in addition to the
+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.
+8
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 + + 224 + With `repair`, the linking files will be updated if there's an absolute/relative 225 + mismatch, even if the links are correct. 226 + 219 227 --[no-]track:: 220 228 When creating a new branch, if `<commit-ish>` is a branch, 221 229 mark it as "upstream" from the new branch. This is the
+17 -12
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 ··· 415 420 const struct add_opts *opts) 416 421 { 417 422 struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT; 418 - struct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT; 419 - struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT; 423 + struct strbuf sb = STRBUF_INIT; 420 424 const char *name; 421 425 struct strvec child_env = STRVEC_INIT; 422 426 unsigned int counter = 0; ··· 492 496 493 497 strbuf_reset(&sb); 494 498 strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); 495 - strbuf_realpath(&sb_path_realpath, path, 1); 496 - strbuf_realpath(&sb_repo_realpath, sb_repo.buf, 1); 497 - write_file(sb.buf, "%s/.git", relative_path(sb_path_realpath.buf, sb_repo_realpath.buf, &sb_tmp)); 498 - write_file(sb_git.buf, "gitdir: %s", relative_path(sb_repo_realpath.buf, sb_path_realpath.buf, &sb_tmp)); 499 + write_worktree_linking_files(sb_git, sb, opts->relative_paths); 499 500 strbuf_reset(&sb); 500 501 strbuf_addf(&sb, "%s/commondir", sb_repo.buf); 501 502 write_file(sb.buf, "../.."); ··· 579 580 580 581 strvec_clear(&child_env); 581 582 strbuf_release(&sb); 582 - strbuf_release(&sb_tmp); 583 583 strbuf_release(&symref); 584 584 strbuf_release(&sb_repo); 585 - strbuf_release(&sb_repo_realpath); 586 585 strbuf_release(&sb_git); 587 - strbuf_release(&sb_path_realpath); 588 586 strbuf_release(&sb_name); 589 587 free_worktree(wt); 590 588 return ret; ··· 798 796 PARSE_OPT_NOARG | PARSE_OPT_OPTARG), 799 797 OPT_BOOL(0, "guess-remote", &guess_remote, 800 798 N_("try to match the new branch name with a remote-tracking branch")), 799 + OPT_BOOL(0, "relative-paths", &opts.relative_paths, 800 + N_("use relative paths for worktrees")), 801 801 OPT_END() 802 802 }; 803 803 int ret; 804 804 805 805 memset(&opts, 0, sizeof(opts)); 806 806 opts.checkout = 1; 807 + opts.relative_paths = use_relative_paths; 807 808 ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0); 808 809 if (!!opts.detach + !!new_branch + !!new_branch_force > 1) 809 810 die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach"); ··· 1195 1196 OPT__FORCE(&force, 1196 1197 N_("force move even if worktree is dirty or locked"), 1197 1198 PARSE_OPT_NOCOMPLETE), 1199 + OPT_BOOL(0, "relative-paths", &use_relative_paths, 1200 + N_("use relative paths for worktrees")), 1198 1201 OPT_END() 1199 1202 }; 1200 1203 struct worktree **worktrees, *wt; ··· 1247 1250 if (rename(wt->path, dst.buf) == -1) 1248 1251 die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf); 1249 1252 1250 - update_worktree_location(wt, dst.buf); 1253 + update_worktree_location(wt, dst.buf, use_relative_paths); 1251 1254 1252 1255 strbuf_release(&dst); 1253 1256 free_worktrees(worktrees); ··· 1390 1393 const char **p; 1391 1394 const char *self[] = { ".", NULL }; 1392 1395 struct option options[] = { 1396 + OPT_BOOL(0, "relative-paths", &use_relative_paths, 1397 + N_("use relative paths for worktrees")), 1393 1398 OPT_END() 1394 1399 }; 1395 1400 int rc = 0; ··· 1397 1402 ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0); 1398 1403 p = ac > 0 ? av : self; 1399 1404 for (; *p; p++) 1400 - repair_worktree_at_path(*p, report_repair, &rc); 1401 - repair_worktrees(report_repair, &rc); 1405 + repair_worktree_at_path(*p, report_repair, &rc, use_relative_paths); 1406 + repair_worktrees(report_repair, &rc, use_relative_paths); 1402 1407 return rc; 1403 1408 } 1404 1409
+1
repository.c
··· 283 283 repo_set_compat_hash_algo(repo, format.compat_hash_algo); 284 284 repo_set_ref_storage_format(repo, format.ref_storage_format); 285 285 repo->repository_format_worktree_config = format.worktree_config; 286 + repo->repository_format_relative_worktrees = format.relative_worktrees; 286 287 287 288 /* take ownership of format.partial_clone */ 288 289 repo->repository_format_partial_clone = format.partial_clone;
+1
repository.h
··· 150 150 151 151 /* Configurations */ 152 152 int repository_format_worktree_config; 153 + int repository_format_relative_worktrees; 153 154 154 155 /* Indicate if a repository has a different 'commondir' from 'gitdir' */ 155 156 unsigned different_commondir:1;
+30 -9
setup.c
··· 683 683 "extensions.refstorage", value); 684 684 data->ref_storage_format = format; 685 685 return EXTENSION_OK; 686 + } else if (!strcmp(ext, "relativeworktrees")) { 687 + data->relative_worktrees = git_config_bool(var, value); 688 + return EXTENSION_OK; 686 689 } 687 690 return EXTENSION_UNKNOWN; 688 691 } ··· 1854 1857 repo_fmt.ref_storage_format); 1855 1858 the_repository->repository_format_worktree_config = 1856 1859 repo_fmt.worktree_config; 1860 + the_repository->repository_format_relative_worktrees = 1861 + repo_fmt.relative_worktrees; 1857 1862 /* take ownership of repo_fmt.partial_clone */ 1858 1863 the_repository->repository_format_partial_clone = 1859 1864 repo_fmt.partial_clone; ··· 1950 1955 fmt->ref_storage_format); 1951 1956 the_repository->repository_format_worktree_config = 1952 1957 fmt->worktree_config; 1958 + the_repository->repository_format_relative_worktrees = 1959 + fmt->relative_worktrees; 1953 1960 the_repository->repository_format_partial_clone = 1954 1961 xstrdup_or_null(fmt->partial_clone); 1955 1962 clear_repository_format(&repo_fmt); ··· 2204 2211 enum ref_storage_format ref_storage_format, 2205 2212 int reinit) 2206 2213 { 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; 2209 2216 2210 2217 /* 2211 2218 * Note that we initialize the repository version to 1 when the ref ··· 2216 2223 */ 2217 2224 if (hash_algo != GIT_HASH_SHA1 || 2218 2225 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; 2225 2227 2226 2228 if (hash_algo != GIT_HASH_SHA1 && hash_algo != GIT_HASH_UNKNOWN) 2227 2229 git_config_set("extensions.objectformat", ··· 2234 2236 ref_storage_format_to_name(ref_storage_format)); 2235 2237 else if (reinit) 2236 2238 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); 2237 2258 } 2238 2259 2239 2260 static int is_reinit(void) ··· 2333 2354 adjust_shared_perm(repo_get_git_dir(the_repository)); 2334 2355 } 2335 2356 2336 - initialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, 0); 2357 + initialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, reinit); 2337 2358 2338 2359 /* Check filemode trustability */ 2339 2360 path = git_path_buf(&buf, "config");
+1
setup.h
··· 129 129 int precious_objects; 130 130 char *partial_clone; /* value of extensions.partialclone */ 131 131 int worktree_config; 132 + int relative_worktrees; 132 133 int is_bare; 133 134 int hash_algo; 134 135 int compat_hash_algo;
+18 -4
t/t0001-init.sh
··· 433 433 sep_git_dir_worktree () { 434 434 test_when_finished "rm -rf mainwt linkwt seprepo" && 435 435 git init mainwt && 436 + if test "relative" = $2 437 + then 438 + test_config -C mainwt worktree.useRelativePaths true 439 + else 440 + test_config -C mainwt worktree.useRelativePaths false 441 + fi 436 442 test_commit -C mainwt gumby && 437 443 git -C mainwt worktree add --detach ../linkwt && 438 444 git -C "$1" init --separate-git-dir ../seprepo && ··· 441 447 test_cmp expect actual 442 448 } 443 449 444 - test_expect_success 're-init to move gitdir with linked worktrees' ' 445 - sep_git_dir_worktree mainwt 450 + test_expect_success 're-init to move gitdir with linked worktrees (absolute)' ' 451 + sep_git_dir_worktree mainwt absolute 446 452 ' 447 453 448 - test_expect_success 're-init to move gitdir within linked worktree' ' 449 - sep_git_dir_worktree linkwt 454 + test_expect_success 're-init to move gitdir within linked worktree (absolute)' ' 455 + sep_git_dir_worktree linkwt absolute 456 + ' 457 + 458 + test_expect_success 're-init to move gitdir with linked worktrees (relative)' ' 459 + sep_git_dir_worktree mainwt relative 460 + ' 461 + 462 + test_expect_success 're-init to move gitdir within linked worktree (relative)' ' 463 + sep_git_dir_worktree linkwt relative 450 464 ' 451 465 452 466 test_expect_success MINGW '.git hidden' '
+46
t/t2400-worktree-add.sh
··· 1206 1206 git -C project-clone -c submodule.recurse worktree add ../project-5 1207 1207 ' 1208 1208 1209 + test_expect_success 'can create worktrees with relative paths' ' 1210 + test_when_finished "git worktree remove relative" && 1211 + test_config worktree.useRelativePaths false && 1212 + git worktree add --relative-paths ./relative && 1213 + echo "gitdir: ../.git/worktrees/relative" >expect && 1214 + test_cmp expect relative/.git && 1215 + echo "../../../relative/.git" >expect && 1216 + test_cmp expect .git/worktrees/relative/gitdir 1217 + ' 1218 + 1219 + test_expect_success 'can create worktrees with absolute paths' ' 1220 + test_config worktree.useRelativePaths true && 1221 + git worktree add ./relative && 1222 + echo "gitdir: ../.git/worktrees/relative" >expect && 1223 + test_cmp expect relative/.git && 1224 + git worktree add --no-relative-paths ./absolute && 1225 + echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect && 1226 + test_cmp expect absolute/.git && 1227 + echo "$(pwd)/absolute/.git" >expect && 1228 + test_cmp expect .git/worktrees/absolute/gitdir 1229 + ' 1230 + 1231 + test_expect_success 'move repo without breaking relative internal links' ' 1232 + test_when_finished rm -rf repo moved && 1233 + git init repo && 1234 + ( 1235 + cd repo && 1236 + test_commit initial && 1237 + git worktree add --relative-paths wt1 && 1238 + cd .. && 1239 + mv repo moved && 1240 + cd moved/wt1 && 1241 + git worktree list >out 2>err && 1242 + test_must_be_empty err 1243 + ) 1244 + ' 1245 + 1246 + test_expect_success 'relative worktree sets extension config' ' 1247 + test_when_finished "rm -rf repo" && 1248 + git init repo && 1249 + git -C repo commit --allow-empty -m base && 1250 + git -C repo worktree add --relative-paths ./foo && 1251 + test_cmp_config -C repo 1 core.repositoryformatversion && 1252 + test_cmp_config -C repo true extensions.relativeworktrees 1253 + ' 1254 + 1209 1255 test_done
+2 -1
t/t2401-worktree-prune.sh
··· 119 119 ! test -d .git/worktrees/wt 120 120 ' 121 121 122 - test_expect_success 'not prune proper worktrees when run inside linked worktree' ' 122 + test_expect_success 'not prune proper worktrees inside linked worktree with relative paths' ' 123 123 test_when_finished rm -rf repo wt_ext && 124 124 git init repo && 125 125 ( 126 126 cd repo && 127 + git config worktree.useRelativePaths true && 127 128 echo content >file && 128 129 git add file && 129 130 git commit -m msg &&
+22
t/t2402-worktree-list.sh
··· 260 260 ' 261 261 262 262 test_expect_success 'linked worktrees are sorted' ' 263 + test_when_finished "rm -rf sorted" && 263 264 mkdir sorted && 264 265 git init sorted/main && 265 266 ( ··· 267 268 test_tick && 268 269 test_commit new && 269 270 git worktree add ../first && 271 + git worktree add ../second && 272 + git worktree list --porcelain >out && 273 + grep ^worktree out >actual 274 + ) && 275 + cat >expected <<-EOF && 276 + worktree $(pwd)/sorted/main 277 + worktree $(pwd)/sorted/first 278 + worktree $(pwd)/sorted/second 279 + EOF 280 + test_cmp expected sorted/main/actual 281 + ' 282 + 283 + test_expect_success 'linked worktrees with relative paths are shown with absolute paths' ' 284 + test_when_finished "rm -rf sorted" && 285 + mkdir sorted && 286 + git init sorted/main && 287 + ( 288 + cd sorted/main && 289 + test_tick && 290 + test_commit new && 291 + git worktree add --relative-paths ../first && 270 292 git worktree add ../second && 271 293 git worktree list --porcelain >out && 272 294 grep ^worktree out >actual
+25
t/t2403-worktree-move.sh
··· 246 246 ) 247 247 ' 248 248 249 + test_expect_success 'move worktree with absolute path to relative path' ' 250 + test_config worktree.useRelativePaths false && 251 + git worktree add ./absolute && 252 + git worktree move --relative-paths absolute relative && 253 + echo "gitdir: ../.git/worktrees/absolute" >expect && 254 + test_cmp expect relative/.git && 255 + echo "../../../relative/.git" >expect && 256 + test_cmp expect .git/worktrees/absolute/gitdir && 257 + test_config worktree.useRelativePaths true && 258 + git worktree move relative relative2 && 259 + echo "gitdir: ../.git/worktrees/absolute" >expect && 260 + test_cmp expect relative2/.git && 261 + echo "../../../relative2/.git" >expect && 262 + test_cmp expect .git/worktrees/absolute/gitdir 263 + ' 264 + 265 + test_expect_success 'move worktree with relative path to absolute path' ' 266 + test_config worktree.useRelativePaths true && 267 + git worktree move --no-relative-paths relative2 absolute && 268 + echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect && 269 + test_cmp expect absolute/.git && 270 + echo "$(pwd)/absolute/.git" >expect && 271 + test_cmp expect .git/worktrees/absolute/gitdir 272 + ' 273 + 249 274 test_done
+39
t/t2406-worktree-repair.sh
··· 215 215 test_cmp dup/linked.expect dup/linked/.git 216 216 ' 217 217 218 + test_expect_success 'repair worktree with relative path with missing gitfile' ' 219 + test_when_finished "rm -rf main wt" && 220 + test_create_repo main && 221 + git -C main config worktree.useRelativePaths true && 222 + test_commit -C main init && 223 + git -C main worktree add --detach ../wt && 224 + rm wt/.git && 225 + test_path_is_missing wt/.git && 226 + git -C main worktree repair && 227 + echo "gitdir: ../main/.git/worktrees/wt" >expect && 228 + test_cmp expect wt/.git 229 + ' 230 + 231 + test_expect_success 'repair absolute worktree to use relative paths' ' 232 + test_when_finished "rm -rf main side sidemoved" && 233 + test_create_repo main && 234 + test_commit -C main init && 235 + git -C main worktree add --detach ../side && 236 + echo "../../../../sidemoved/.git" >expect-gitdir && 237 + echo "gitdir: ../main/.git/worktrees/side" >expect-gitfile && 238 + mv side sidemoved && 239 + git -C main worktree repair --relative-paths ../sidemoved && 240 + test_cmp expect-gitdir main/.git/worktrees/side/gitdir && 241 + test_cmp expect-gitfile sidemoved/.git 242 + ' 243 + 244 + test_expect_success 'repair relative worktree to use absolute paths' ' 245 + test_when_finished "rm -rf main side sidemoved" && 246 + test_create_repo main && 247 + test_commit -C main init && 248 + git -C main worktree add --relative-paths --detach ../side && 249 + echo "$(pwd)/sidemoved/.git" >expect-gitdir && 250 + echo "gitdir: $(pwd)/main/.git/worktrees/side" >expect-gitfile && 251 + mv side sidemoved && 252 + git -C main worktree repair ../sidemoved && 253 + test_cmp expect-gitdir main/.git/worktrees/side/gitdir && 254 + test_cmp expect-gitfile sidemoved/.git 255 + ' 256 + 218 257 test_done
-38
t/t2408-worktree-relative.sh
··· 1 - #!/bin/sh 2 - 3 - test_description='test worktrees linked with relative paths' 4 - 5 - . ./test-lib.sh 6 - 7 - test_expect_success 'links worktrees with relative paths' ' 8 - test_when_finished rm -rf repo && 9 - git init repo && 10 - ( 11 - cd repo && 12 - test_commit initial && 13 - git worktree add wt1 && 14 - echo "../../../wt1/.git" >expected_gitdir && 15 - cat .git/worktrees/wt1/gitdir >actual_gitdir && 16 - echo "gitdir: ../.git/worktrees/wt1" >expected_git && 17 - cat wt1/.git >actual_git && 18 - test_cmp expected_gitdir actual_gitdir && 19 - test_cmp expected_git actual_git 20 - ) 21 - ' 22 - 23 - test_expect_success 'move repo without breaking relative internal links' ' 24 - test_when_finished rm -rf repo moved && 25 - git init repo && 26 - ( 27 - cd repo && 28 - test_commit initial && 29 - git worktree add wt1 && 30 - cd .. && 31 - mv repo moved && 32 - cd moved/wt1 && 33 - git status >out 2>err && 34 - test_must_be_empty err 35 - ) 36 - ' 37 - 38 - test_done
+3 -3
t/t5504-fetch-receive-strict.sh
··· 170 170 test_must_fail git -c fsck.skipList=does-not-exist -c fsck.missingEmail=ignore fsck 2>err && 171 171 test_grep "could not open.*: does-not-exist" err && 172 172 test_must_fail git -c fsck.skipList=.git/config -c fsck.missingEmail=ignore fsck 2>err && 173 - test_grep "invalid object name: \[core\]" err 173 + test_grep "invalid object name: " err 174 174 ' 175 175 176 176 test_expect_success 'fsck with other accepted skipList input (comments & empty lines)' ' ··· 233 233 test_grep "could not open.*: does-not-exist" err && 234 234 git --git-dir=dst/.git config receive.fsck.skipList config && 235 235 test_must_fail git push --porcelain dst bogus 2>err && 236 - test_grep "invalid object name: \[core\]" err && 236 + test_grep "invalid object name: " err && 237 237 238 238 git --git-dir=dst/.git config receive.fsck.skipList SKIP && 239 239 git push --porcelain dst bogus ··· 262 262 test_grep "could not open.*: does-not-exist" err && 263 263 git --git-dir=dst/.git config fetch.fsck.skipList dst/.git/config && 264 264 test_must_fail git --git-dir=dst/.git fetch "file://$(pwd)" $refspec 2>err && 265 - test_grep "invalid object name: \[core\]" err && 265 + test_grep "invalid object name: " err && 266 266 267 267 git --git-dir=dst/.git config fetch.fsck.skipList dst/.git/SKIP && 268 268 git --git-dir=dst/.git fetch "file://$(pwd)" $refspec
+87 -70
worktree.c
··· 111 111 strbuf_strip_suffix(&worktree_path, "/.git"); 112 112 113 113 if (!is_absolute_path(worktree_path.buf)) { 114 - strbuf_strip_suffix(&path, "gitdir"); 115 - strbuf_addbuf(&path, &worktree_path); 116 - strbuf_realpath_forgiving(&worktree_path, path.buf, 0); 114 + strbuf_strip_suffix(&path, "gitdir"); 115 + strbuf_addbuf(&path, &worktree_path); 116 + strbuf_realpath_forgiving(&worktree_path, path.buf, 0); 117 117 } 118 118 119 119 CALLOC_ARRAY(worktree, 1); ··· 376 376 return ret; 377 377 } 378 378 379 - void update_worktree_location(struct worktree *wt, const char *path_) 379 + void update_worktree_location(struct worktree *wt, const char *path_, 380 + int use_relative_paths) 380 381 { 381 382 struct strbuf path = STRBUF_INIT; 382 - struct strbuf repo = STRBUF_INIT; 383 - struct strbuf file = STRBUF_INIT; 384 - struct strbuf tmp = STRBUF_INIT; 383 + struct strbuf dotgit = STRBUF_INIT; 384 + struct strbuf gitdir = STRBUF_INIT; 385 385 386 386 if (is_main_worktree(wt)) 387 387 BUG("can't relocate main worktree"); 388 388 389 - strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1); 389 + strbuf_realpath(&gitdir, git_common_path("worktrees/%s/gitdir", wt->id), 1); 390 390 strbuf_realpath(&path, path_, 1); 391 + strbuf_addf(&dotgit, "%s/.git", path.buf); 391 392 if (fspathcmp(wt->path, path.buf)) { 392 - strbuf_addf(&file, "%s/gitdir", repo.buf); 393 - write_file(file.buf, "%s/.git", relative_path(path.buf, repo.buf, &tmp)); 394 - strbuf_reset(&file); 395 - strbuf_addf(&file, "%s/.git", path.buf); 396 - write_file(file.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp)); 393 + write_worktree_linking_files(dotgit, gitdir, use_relative_paths); 397 394 398 395 free(wt->path); 399 396 wt->path = strbuf_detach(&path, NULL); 400 397 } 401 398 strbuf_release(&path); 402 - strbuf_release(&repo); 403 - strbuf_release(&file); 404 - strbuf_release(&tmp); 399 + strbuf_release(&dotgit); 400 + strbuf_release(&gitdir); 405 401 } 406 402 407 403 int is_worktree_being_rebased(const struct worktree *wt, ··· 577 573 * pointing at <repo>/worktrees/<id>. 578 574 */ 579 575 static void repair_gitfile(struct worktree *wt, 580 - worktree_repair_fn fn, void *cb_data) 576 + worktree_repair_fn fn, void *cb_data, 577 + int use_relative_paths) 581 578 { 582 579 struct strbuf dotgit = STRBUF_INIT; 580 + struct strbuf gitdir = STRBUF_INIT; 583 581 struct strbuf repo = STRBUF_INIT; 584 582 struct strbuf backlink = STRBUF_INIT; 585 - struct strbuf tmp = STRBUF_INIT; 586 583 char *dotgit_contents = NULL; 587 584 const char *repair = NULL; 588 585 int err; ··· 598 595 599 596 strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1); 600 597 strbuf_addf(&dotgit, "%s/.git", wt->path); 598 + strbuf_addf(&gitdir, "%s/gitdir", repo.buf); 601 599 dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err)); 602 600 603 601 if (dotgit_contents) { ··· 615 613 repair = _(".git file broken"); 616 614 else if (fspathcmp(backlink.buf, repo.buf)) 617 615 repair = _(".git file incorrect"); 616 + else if (use_relative_paths == is_absolute_path(dotgit_contents)) 617 + repair = _(".git file absolute/relative path mismatch"); 618 618 619 619 if (repair) { 620 620 fn(0, wt->path, repair, cb_data); 621 - write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, wt->path, &tmp)); 621 + write_worktree_linking_files(dotgit, gitdir, use_relative_paths); 622 622 } 623 623 624 624 done: 625 625 free(dotgit_contents); 626 626 strbuf_release(&repo); 627 627 strbuf_release(&dotgit); 628 + strbuf_release(&gitdir); 628 629 strbuf_release(&backlink); 629 - strbuf_release(&tmp); 630 630 } 631 631 632 632 static void repair_noop(int iserr UNUSED, ··· 637 637 /* nothing */ 638 638 } 639 639 640 - void repair_worktrees(worktree_repair_fn fn, void *cb_data) 640 + void repair_worktrees(worktree_repair_fn fn, void *cb_data, int use_relative_paths) 641 641 { 642 642 struct worktree **worktrees = get_worktrees_internal(1); 643 643 struct worktree **wt = worktrees + 1; /* +1 skips main worktree */ ··· 645 645 if (!fn) 646 646 fn = repair_noop; 647 647 for (; *wt; wt++) 648 - repair_gitfile(*wt, fn, cb_data); 648 + repair_gitfile(*wt, fn, cb_data, use_relative_paths); 649 649 free_worktrees(worktrees); 650 650 } 651 651 652 652 void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path) 653 653 { 654 - struct strbuf path = STRBUF_INIT; 655 - struct strbuf repo = STRBUF_INIT; 656 654 struct strbuf gitdir = STRBUF_INIT; 657 655 struct strbuf dotgit = STRBUF_INIT; 658 - struct strbuf olddotgit = STRBUF_INIT; 659 - struct strbuf tmp = STRBUF_INIT; 656 + int is_relative_path; 660 657 661 658 if (is_main_worktree(wt)) 662 659 goto done; 663 660 664 - strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1); 665 - strbuf_addf(&gitdir, "%s/gitdir", repo.buf); 661 + strbuf_realpath(&gitdir, git_common_path("worktrees/%s/gitdir", wt->id), 1); 666 662 667 - if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0) 663 + if (strbuf_read_file(&dotgit, gitdir.buf, 0) < 0) 668 664 goto done; 669 665 670 - strbuf_rtrim(&olddotgit); 671 - if (is_absolute_path(olddotgit.buf)) { 672 - strbuf_addbuf(&dotgit, &olddotgit); 673 - } else { 674 - strbuf_addf(&dotgit, "%s/worktrees/%s/%s", old_path, wt->id, olddotgit.buf); 666 + strbuf_rtrim(&dotgit); 667 + is_relative_path = ! is_absolute_path(dotgit.buf); 668 + if (is_relative_path) { 669 + strbuf_insertf(&dotgit, 0, "%s/worktrees/%s/", old_path, wt->id); 675 670 strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0); 676 671 } 677 672 678 673 if (!file_exists(dotgit.buf)) 679 674 goto done; 680 675 681 - strbuf_addbuf(&path, &dotgit); 682 - strbuf_strip_suffix(&path, "/.git"); 683 - 684 - write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp)); 685 - write_file(gitdir.buf, "%s", relative_path(dotgit.buf, repo.buf, &tmp)); 676 + write_worktree_linking_files(dotgit, gitdir, is_relative_path); 686 677 done: 687 - strbuf_release(&path); 688 - strbuf_release(&repo); 689 678 strbuf_release(&gitdir); 690 679 strbuf_release(&dotgit); 691 - strbuf_release(&olddotgit); 692 - strbuf_release(&tmp); 693 680 } 694 681 695 682 void repair_worktrees_after_gitdir_move(const char *old_path) ··· 725 712 * won't know which <repo>/worktrees/<id>/gitdir to repair. However, we may 726 713 * be able to infer the gitdir by manually reading /path/to/worktree/.git, 727 714 * extracting the <id>, and checking if <repo>/worktrees/<id> exists. 715 + * 716 + * Returns -1 on failure and strbuf.len on success. 728 717 */ 729 - static int infer_backlink(const char *gitfile, struct strbuf *inferred) 718 + static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred) 730 719 { 731 720 struct strbuf actual = STRBUF_INIT; 732 721 const char *id; ··· 747 736 goto error; 748 737 749 738 strbuf_release(&actual); 750 - return 1; 751 - 739 + return inferred->len; 752 740 error: 753 741 strbuf_release(&actual); 754 742 strbuf_reset(inferred); /* clear invalid path */ 755 - return 0; 743 + return -1; 756 744 } 757 745 758 746 /* ··· 760 748 * the worktree's path. 761 749 */ 762 750 void repair_worktree_at_path(const char *path, 763 - worktree_repair_fn fn, void *cb_data) 751 + worktree_repair_fn fn, void *cb_data, 752 + int use_relative_paths) 764 753 { 765 754 struct strbuf dotgit = STRBUF_INIT; 766 - struct strbuf realdotgit = STRBUF_INIT; 767 755 struct strbuf backlink = STRBUF_INIT; 768 756 struct strbuf inferred_backlink = STRBUF_INIT; 769 757 struct strbuf gitdir = STRBUF_INIT; 770 758 struct strbuf olddotgit = STRBUF_INIT; 771 - struct strbuf realolddotgit = STRBUF_INIT; 772 - struct strbuf tmp = STRBUF_INIT; 773 759 char *dotgit_contents = NULL; 774 760 const char *repair = NULL; 775 761 int err; ··· 781 767 goto done; 782 768 783 769 strbuf_addf(&dotgit, "%s/.git", path); 784 - if (!strbuf_realpath(&realdotgit, dotgit.buf, 0)) { 770 + if (!strbuf_realpath(&dotgit, dotgit.buf, 0)) { 785 771 fn(1, path, _("not a valid path"), cb_data); 786 772 goto done; 787 773 } 788 774 789 - infer_backlink(realdotgit.buf, &inferred_backlink); 775 + infer_backlink(dotgit.buf, &inferred_backlink); 790 776 strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0); 791 - dotgit_contents = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err)); 777 + dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err)); 792 778 if (dotgit_contents) { 793 779 if (is_absolute_path(dotgit_contents)) { 794 780 strbuf_addstr(&backlink, dotgit_contents); 795 781 } else { 796 - strbuf_addbuf(&backlink, &realdotgit); 782 + strbuf_addbuf(&backlink, &dotgit); 797 783 strbuf_strip_suffix(&backlink, ".git"); 798 784 strbuf_addstr(&backlink, dotgit_contents); 799 785 strbuf_realpath_forgiving(&backlink, backlink.buf, 0); 800 786 } 801 787 } else if (err == READ_GITFILE_ERR_NOT_A_FILE) { 802 - fn(1, realdotgit.buf, _("unable to locate repository; .git is not a file"), cb_data); 788 + fn(1, dotgit.buf, _("unable to locate repository; .git is not a file"), cb_data); 803 789 goto done; 804 790 } else if (err == READ_GITFILE_ERR_NOT_A_REPO) { 805 791 if (inferred_backlink.len) { ··· 812 798 */ 813 799 strbuf_swap(&backlink, &inferred_backlink); 814 800 } else { 815 - fn(1, realdotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data); 801 + fn(1, dotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data); 816 802 goto done; 817 803 } 818 804 } else { 819 - fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data); 805 + fn(1, dotgit.buf, _("unable to locate repository; .git file broken"), cb_data); 820 806 goto done; 821 807 } 822 808 ··· 838 824 * in the "copy" repository. In this case, point the "copy" worktree's 839 825 * .git file at the "copy" repository. 840 826 */ 841 - if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) { 827 + if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) 842 828 strbuf_swap(&backlink, &inferred_backlink); 843 - } 844 829 845 830 strbuf_addf(&gitdir, "%s/gitdir", backlink.buf); 846 831 if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0) 847 832 repair = _("gitdir unreadable"); 833 + else if (use_relative_paths == is_absolute_path(olddotgit.buf)) 834 + repair = _("gitdir absolute/relative path mismatch"); 848 835 else { 849 836 strbuf_rtrim(&olddotgit); 850 - if (is_absolute_path(olddotgit.buf)) { 851 - strbuf_addbuf(&realolddotgit, &olddotgit); 852 - } else { 853 - strbuf_addf(&realolddotgit, "%s/%s", backlink.buf, olddotgit.buf); 854 - strbuf_realpath_forgiving(&realolddotgit, realolddotgit.buf, 0); 837 + if (!is_absolute_path(olddotgit.buf)) { 838 + strbuf_insertf(&olddotgit, 0, "%s/", backlink.buf); 839 + strbuf_realpath_forgiving(&olddotgit, olddotgit.buf, 0); 855 840 } 856 - if (fspathcmp(realolddotgit.buf, realdotgit.buf)) 841 + if (fspathcmp(olddotgit.buf, dotgit.buf)) 857 842 repair = _("gitdir incorrect"); 858 843 } 859 844 860 845 if (repair) { 861 846 fn(0, gitdir.buf, repair, cb_data); 862 - write_file(gitdir.buf, "%s", relative_path(realdotgit.buf, backlink.buf, &tmp)); 847 + write_worktree_linking_files(dotgit, gitdir, use_relative_paths); 863 848 } 864 849 done: 865 850 free(dotgit_contents); 866 851 strbuf_release(&olddotgit); 867 - strbuf_release(&realolddotgit); 868 852 strbuf_release(&backlink); 869 853 strbuf_release(&inferred_backlink); 870 854 strbuf_release(&gitdir); 871 - strbuf_release(&realdotgit); 872 855 strbuf_release(&dotgit); 873 - strbuf_release(&tmp); 874 856 } 875 857 876 858 int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire) ··· 1031 1013 free(main_worktree_file); 1032 1014 return res; 1033 1015 } 1016 + 1017 + void write_worktree_linking_files(struct strbuf dotgit, struct strbuf gitdir, 1018 + int use_relative_paths) 1019 + { 1020 + struct strbuf path = STRBUF_INIT; 1021 + struct strbuf repo = STRBUF_INIT; 1022 + struct strbuf tmp = STRBUF_INIT; 1023 + 1024 + strbuf_addbuf(&path, &dotgit); 1025 + strbuf_strip_suffix(&path, "/.git"); 1026 + strbuf_realpath(&path, path.buf, 1); 1027 + strbuf_addbuf(&repo, &gitdir); 1028 + strbuf_strip_suffix(&repo, "/gitdir"); 1029 + strbuf_realpath(&repo, repo.buf, 1); 1030 + 1031 + if (use_relative_paths && !the_repository->repository_format_relative_worktrees) { 1032 + if (upgrade_repository_format(1) < 0) 1033 + die(_("unable to upgrade repository format to support relative worktrees")); 1034 + if (git_config_set_gently("extensions.relativeWorktrees", "true")) 1035 + die(_("unable to set extensions.relativeWorktrees setting")); 1036 + the_repository->repository_format_relative_worktrees = 1; 1037 + } 1038 + 1039 + if (use_relative_paths) { 1040 + write_file(gitdir.buf, "%s/.git", relative_path(path.buf, repo.buf, &tmp)); 1041 + write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp)); 1042 + } else { 1043 + write_file(gitdir.buf, "%s/.git", path.buf); 1044 + write_file(dotgit.buf, "gitdir: %s", repo.buf); 1045 + } 1046 + 1047 + strbuf_release(&path); 1048 + strbuf_release(&repo); 1049 + strbuf_release(&tmp); 1050 + }
+18 -4
worktree.h
··· 117 117 /* 118 118 * Update worktrees/xxx/gitdir with the new path. 119 119 */ 120 - void update_worktree_location(struct worktree *wt, 121 - const char *path_); 120 + void update_worktree_location(struct worktree *wt, const char *path_, 121 + int use_relative_paths); 122 122 123 123 typedef void (* worktree_repair_fn)(int iserr, const char *path, 124 124 const char *msg, void *cb_data); ··· 129 129 * function, if non-NULL, is called with the path of the worktree and a 130 130 * description of the repair or error, along with the callback user-data. 131 131 */ 132 - void repair_worktrees(worktree_repair_fn, void *cb_data); 132 + void repair_worktrees(worktree_repair_fn, void *cb_data, int use_relative_paths); 133 133 134 134 /* 135 135 * Repair the linked worktrees after the gitdir has been moved. ··· 151 151 * worktree and a description of the repair or error, along with the callback 152 152 * user-data. 153 153 */ 154 - void repair_worktree_at_path(const char *, worktree_repair_fn, void *cb_data); 154 + void repair_worktree_at_path(const char *, worktree_repair_fn, 155 + void *cb_data, int use_relative_paths); 155 156 156 157 /* 157 158 * Free up the memory for a worktree. ··· 214 215 * if any of these steps fail. 215 216 */ 216 217 int init_worktree_config(struct repository *r); 218 + 219 + /** 220 + * Write the .git file and gitdir file that links the worktree to the repository. 221 + * 222 + * The `dotgit` parameter is the path to the worktree's .git file, and `gitdir` 223 + * is the path to the repository's `gitdir` file. 224 + * 225 + * Example 226 + * dotgit: "/path/to/foo/.git" 227 + * gitdir: "/path/to/repo/worktrees/foo/gitdir" 228 + */ 229 + void write_worktree_linking_files(struct strbuf dotgit, struct strbuf gitdir, 230 + int use_relative_paths); 217 231 218 232 #endif