worktree: link worktrees with relative paths

Git currently stores absolute paths to both the main repository and linked worktrees. However, this causes problems when moving repositories or working in containerized environments where absolute paths differ between systems. The worktree links break, and users are required to manually execute `worktree repair` to repair them, leading to workflow disruptions. Additionally, mapping repositories inside of containerized environments renders the repository unusable inside the containers, and this is not repairable as repairing the worktrees inside the containers will result in them being broken outside the containers. To address this, this patch makes Git always write relative paths when linking worktrees. Relative paths increase the resilience of the worktree links across various systems and environments, particularly when the worktrees are self-contained inside the main repository (such as when using a bare repository with worktrees). This improves portability, workflow efficiency, and reduces overall breakages. Although Git now writes relative paths, existing repositories with absolute paths are still supported. There are no breaking changes to workflows based on absolute paths, ensuring backward compatibility. At a low level, the changes involve modifying functions in `worktree.c` and `builtin/worktree.c` to use `relative_path()` when writing the worktree’s `.git` file and the main repository’s `gitdir` reference. Instead of hardcoding absolute paths, Git now computes the relative path between the worktree and the repository, ensuring that these links are portable. Locations where these respective file are read have also been updated to properly handle both absolute and relative paths. Generally, relative paths are always resolved into absolute paths before any operations or comparisons are performed. Additionally, `repair_worktrees_after_gitdir_move()` has been introduced to address the case where both the `<worktree>/.git` and `<repo>/worktrees/<id>/gitdir` links are broken after the gitdir is moved (such as during a re-initialization). This function repairs both sides of the worktree link using the old gitdir path to reestablish the correct paths after a move. The `worktree.path` struct member has also been updated to always store the absolute path of a worktree. This ensures that worktree consumers never have to worry about trying to resolve the absolute path themselves. Signed-off-by: Caleb White <cdwhite3@pm.me> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Caleb White committed Oct 7, 2024 at 22:12 UTC 717af916cd69d2565aa2a7b7d73d895aa92ff392
5 files changed +223 -51
builtin/worktree.c
+9 -7
@@ -414,7 +414,8 @@ static int add_worktree(const char *path, const char *refname,
414 const struct add_opts *opts)
415 {
416 struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
417 - struct strbuf sb = STRBUF_INIT, realpath = 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;
@@ -490,11 +491,10 @@ static int add_worktree(const char *path, const char *refname,
491
492 strbuf_reset(&sb);
493 strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
493 - strbuf_realpath(&realpath, sb_git.buf, 1);
494 - write_file(sb.buf, "%s", realpath.buf);
495 - strbuf_realpath(&realpath, repo_get_common_dir(the_repository), 1);
496 - write_file(sb_git.buf, "gitdir: %s/worktrees/%s",
497 - realpath.buf, name);
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,11 +578,13 @@ done:
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);
585 - strbuf_release(&realpath);
588 free_worktree(wt);
589 return ret;
590 }
setup.c
+1 -1
@@ -2420,7 +2420,7 @@ static void separate_git_dir(const char *git_dir, const char *git_link)
2420
2421 if (rename(src, git_dir))
2422 die_errno(_("unable to move %s to %s"), src, git_dir);
2423 - repair_worktrees(NULL, NULL);
2423 + repair_worktrees_after_gitdir_move(src);
2424 }
2425
2426 write_file(git_link, "gitdir: %s", git_dir);
t/t2408-worktree-relative.sh new
+39
@@ -0,0 +1,39 @@
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
worktree.c
+164 -43
@@ -110,6 +110,12 @@ struct worktree *get_linked_worktree(const char *id,
110 strbuf_rtrim(&worktree_path);
111 strbuf_strip_suffix(&worktree_path, "/.git");
112
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);
117 + }
118 +
119 CALLOC_ARRAY(worktree, 1);
120 worktree->repo = the_repository;
121 worktree->path = strbuf_detach(&worktree_path, NULL);
@@ -373,18 +379,29 @@ done:
379 void update_worktree_location(struct worktree *wt, const char *path_)
380 {
381 struct strbuf path = STRBUF_INIT;
382 + struct strbuf repo = STRBUF_INIT;
383 + struct strbuf file = STRBUF_INIT;
384 + struct strbuf tmp = STRBUF_INIT;
385
386 if (is_main_worktree(wt))
387 BUG("can't relocate main worktree");
388
389 + strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
390 strbuf_realpath(&path, path_, 1);
391 if (fspathcmp(wt->path, path.buf)) {
382 - write_file(git_common_path("worktrees/%s/gitdir", wt->id),
383 - "%s/.git", 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));
397 +
398 free(wt->path);
399 wt->path = strbuf_detach(&path, NULL);
400 }
401 strbuf_release(&path);
402 + strbuf_release(&repo);
403 + strbuf_release(&file);
404 + strbuf_release(&tmp);
405 }
406
407 int is_worktree_being_rebased(const struct worktree *wt,
@@ -564,38 +581,52 @@ static void repair_gitfile(struct worktree *wt,
581 {
582 struct strbuf dotgit = STRBUF_INIT;
583 struct strbuf repo = STRBUF_INIT;
567 - char *backlink;
584 + struct strbuf backlink = STRBUF_INIT;
585 + struct strbuf tmp = STRBUF_INIT;
586 + char *dotgit_contents = NULL;
587 const char *repair = NULL;
588 int err;
589
590 /* missing worktree can't be repaired */
591 if (!file_exists(wt->path))
573 - return;
592 + goto done;
593
594 if (!is_directory(wt->path)) {
595 fn(1, wt->path, _("not a directory"), cb_data);
577 - return;
596 + goto done;
597 }
598
599 strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
600 strbuf_addf(&dotgit, "%s/.git", wt->path);
582 - backlink = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
601 + dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
602 +
603 + if (dotgit_contents) {
604 + if (is_absolute_path(dotgit_contents)) {
605 + strbuf_addstr(&backlink, dotgit_contents);
606 + } else {
607 + strbuf_addf(&backlink, "%s/%s", wt->path, dotgit_contents);
608 + strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
609 + }
610 + }
611
612 if (err == READ_GITFILE_ERR_NOT_A_FILE)
613 fn(1, wt->path, _(".git is not a file"), cb_data);
614 else if (err)
615 repair = _(".git file broken");
588 - else if (fspathcmp(backlink, repo.buf))
616 + else if (fspathcmp(backlink.buf, repo.buf))
617 repair = _(".git file incorrect");
618
619 if (repair) {
620 fn(0, wt->path, repair, cb_data);
593 - write_file(dotgit.buf, "gitdir: %s", repo.buf);
621 + write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, wt->path, &tmp));
622 }
623
596 - free(backlink);
624 +done:
625 + free(dotgit_contents);
626 strbuf_release(&repo);
627 strbuf_release(&dotgit);
628 + strbuf_release(&backlink);
629 + strbuf_release(&tmp);
630 }
631
632 static void repair_noop(int iserr UNUSED,
@@ -618,6 +649,59 @@ void repair_worktrees(worktree_repair_fn fn, void *cb_data)
649 free_worktrees(worktrees);
650 }
651
652 +void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path)
653 +{
654 + struct strbuf path = STRBUF_INIT;
655 + struct strbuf repo = STRBUF_INIT;
656 + struct strbuf gitdir = STRBUF_INIT;
657 + struct strbuf dotgit = STRBUF_INIT;
658 + struct strbuf olddotgit = STRBUF_INIT;
659 + struct strbuf tmp = STRBUF_INIT;
660 +
661 + if (is_main_worktree(wt))
662 + goto done;
663 +
664 + strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
665 + strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
666 +
667 + if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0)
668 + goto done;
669 +
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);
675 + strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0);
676 + }
677 +
678 + if (!file_exists(dotgit.buf))
679 + goto done;
680 +
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));
686 +done:
687 + strbuf_release(&path);
688 + strbuf_release(&repo);
689 + strbuf_release(&gitdir);
690 + strbuf_release(&dotgit);
691 + strbuf_release(&olddotgit);
692 + strbuf_release(&tmp);
693 +}
694 +
695 +void repair_worktrees_after_gitdir_move(const char *old_path)
696 +{
697 + struct worktree **worktrees = get_worktrees_internal(1);
698 + struct worktree **wt = worktrees + 1; /* +1 skips main worktree */
699 +
700 + for (; *wt; wt++)
701 + repair_worktree_after_gitdir_move(*wt, old_path);
702 + free_worktrees(worktrees);
703 +}
704 +
705 static int is_main_worktree_path(const char *path)
706 {
707 struct strbuf target = STRBUF_INIT;
@@ -684,6 +768,8 @@ void repair_worktree_at_path(const char *path,
768 struct strbuf inferred_backlink = STRBUF_INIT;
769 struct strbuf gitdir = STRBUF_INIT;
770 struct strbuf olddotgit = STRBUF_INIT;
771 + struct strbuf realolddotgit = STRBUF_INIT;
772 + struct strbuf tmp = STRBUF_INIT;
773 char *dotgit_contents = NULL;
774 const char *repair = NULL;
775 int err;
@@ -701,9 +787,17 @@ void repair_worktree_at_path(const char *path,
787 }
788
789 infer_backlink(realdotgit.buf, &inferred_backlink);
790 + strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);
791 dotgit_contents = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err));
792 if (dotgit_contents) {
706 - strbuf_addstr(&backlink, dotgit_contents);
793 + if (is_absolute_path(dotgit_contents)) {
794 + strbuf_addstr(&backlink, dotgit_contents);
795 + } else {
796 + strbuf_addbuf(&backlink, &realdotgit);
797 + strbuf_strip_suffix(&backlink, ".git");
798 + strbuf_addstr(&backlink, dotgit_contents);
799 + strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
800 + }
801 } 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);
803 goto done;
@@ -721,7 +815,7 @@ void repair_worktree_at_path(const char *path,
815 fn(1, realdotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data);
816 goto done;
817 }
724 - } else if (err) {
818 + } else {
819 fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data);
820 goto done;
821 }
@@ -753,90 +847,117 @@ void repair_worktree_at_path(const char *path,
847 repair = _("gitdir unreadable");
848 else {
849 strbuf_rtrim(&olddotgit);
756 - if (fspathcmp(olddotgit.buf, realdotgit.buf))
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);
855 + }
856 + if (fspathcmp(realolddotgit.buf, realdotgit.buf))
857 repair = _("gitdir incorrect");
858 }
859
860 if (repair) {
861 fn(0, gitdir.buf, repair, cb_data);
762 - write_file(gitdir.buf, "%s", realdotgit.buf);
862 + write_file(gitdir.buf, "%s", relative_path(realdotgit.buf, backlink.buf, &tmp));
863 }
864 done:
865 free(dotgit_contents);
866 strbuf_release(&olddotgit);
867 + strbuf_release(&realolddotgit);
868 strbuf_release(&backlink);
869 strbuf_release(&inferred_backlink);
870 strbuf_release(&gitdir);
871 strbuf_release(&realdotgit);
872 strbuf_release(&dotgit);
873 + strbuf_release(&tmp);
874 }
875
876 int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
877 {
878 struct stat st;
777 - char *path;
879 + struct strbuf dotgit = STRBUF_INIT;
880 + struct strbuf gitdir = STRBUF_INIT;
881 + struct strbuf repo = STRBUF_INIT;
882 + struct strbuf file = STRBUF_INIT;
883 + char *path = NULL;
884 + int rc = 0;
885 int fd;
886 size_t len;
887 ssize_t read_result;
888
889 *wtpath = NULL;
783 - if (!is_directory(git_path("worktrees/%s", id))) {
890 + strbuf_realpath(&repo, git_common_path("worktrees/%s", id), 1);
891 + strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
892 + if (!is_directory(repo.buf)) {
893 strbuf_addstr(reason, _("not a valid directory"));
785 - return 1;
894 + rc = 1;
895 + goto done;
896 }
787 - if (file_exists(git_path("worktrees/%s/locked", id)))
788 - return 0;
789 - if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
897 + strbuf_addf(&file, "%s/locked", repo.buf);
898 + if (file_exists(file.buf)) {
899 + goto done;
900 + }
901 + if (stat(gitdir.buf, &st)) {
902 strbuf_addstr(reason, _("gitdir file does not exist"));
791 - return 1;
903 + rc = 1;
904 + goto done;
905 }
793 - fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
906 + fd = open(gitdir.buf, O_RDONLY);
907 if (fd < 0) {
908 strbuf_addf(reason, _("unable to read gitdir file (%s)"),
909 strerror(errno));
797 - return 1;
910 + rc = 1;
911 + goto done;
912 }
913 len = xsize_t(st.st_size);
914 path = xmallocz(len);
915
916 read_result = read_in_full(fd, path, len);
917 + close(fd);
918 if (read_result < 0) {
919 strbuf_addf(reason, _("unable to read gitdir file (%s)"),
920 strerror(errno));
806 - close(fd);
807 - free(path);
808 - return 1;
809 - }
810 - close(fd);
811 -
812 - if (read_result != len) {
921 + rc = 1;
922 + goto done;
923 + } else if (read_result != len) {
924 strbuf_addf(reason,
925 _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
926 (uintmax_t)len, (uintmax_t)read_result);
816 - free(path);
817 - return 1;
927 + rc = 1;
928 + goto done;
929 }
930 while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
931 len--;
932 if (!len) {
933 strbuf_addstr(reason, _("invalid gitdir file"));
823 - free(path);
824 - return 1;
934 + rc = 1;
935 + goto done;
936 }
937 path[len] = '\0';
827 - if (!file_exists(path)) {
828 - if (stat(git_path("worktrees/%s/index", id), &st) ||
829 - st.st_mtime <= expire) {
938 + if (is_absolute_path(path)) {
939 + strbuf_addstr(&dotgit, path);
940 + } else {
941 + strbuf_addf(&dotgit, "%s/%s", repo.buf, path);
942 + strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0);
943 + }
944 + if (!file_exists(dotgit.buf)) {
945 + strbuf_reset(&file);
946 + strbuf_addf(&file, "%s/index", repo.buf);
947 + if (stat(file.buf, &st) || st.st_mtime <= expire) {
948 strbuf_addstr(reason, _("gitdir file points to non-existent location"));
831 - free(path);
832 - return 1;
833 - } else {
834 - *wtpath = path;
835 - return 0;
949 + rc = 1;
950 + goto done;
951 }
952 }
838 - *wtpath = path;
839 - return 0;
953 + *wtpath = strbuf_detach(&dotgit, NULL);
954 +done:
955 + free(path);
956 + strbuf_release(&dotgit);
957 + strbuf_release(&gitdir);
958 + strbuf_release(&repo);
959 + strbuf_release(&file);
960 + return rc;
961 }
962
963 static int move_config_setting(const char *key, const char *value,
worktree.h
+10
@@ -131,6 +131,16 @@ typedef void (* worktree_repair_fn)(int iserr, const char *path,
131 */
132 void repair_worktrees(worktree_repair_fn, void *cb_data);
133
134 +/*
135 + * Repair the linked worktrees after the gitdir has been moved.
136 + */
137 +void repair_worktrees_after_gitdir_move(const char *old_path);
138 +
139 +/*
140 + * Repair the linked worktree after the gitdir has been moved.
141 + */
142 +void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path);
143 +
144 /*
145 * Repair administrative files corresponding to the worktree at the given path.
146 * The worktree's .git file pointing at the repository must be intact for the