worktree: add relative cli/config options to `repair` command

This teaches the `worktree repair` command to respect the `--[no-]relative-paths` CLI option and `worktree.useRelativePaths` config setting. If an existing worktree with an absolute path is repaired with `--relative-paths`, the links will be replaced with relative paths, even if the original path was correct. This allows a user to covert existing worktrees between absolute/relative as desired. To simplify things, both linking files are written when one of the files needs to be repaired. In some cases, this fixes the other file before it is checked, in other cases this results in a correct file being written with the same contents. 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:23 UTC e6df1ee2c13405ef7077256fef49424f69d61125
5 files changed +76 -33
Documentation/git-worktree.txt
+3
@@ -220,6 +220,9 @@ This can also be set up as the default behaviour by using the
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
227 --[no-]track::
228 When creating a new branch, if `<commit-ish>` is a branch,
builtin/worktree.c
+4 -2
@@ -1385,6 +1385,8 @@ static int repair(int ac, const char **av, const char *prefix)
1385 const char **p;
1386 const char *self[] = { ".", NULL };
1387 struct option options[] = {
1388 + OPT_BOOL(0, "relative-paths", &use_relative_paths,
1389 + N_("use relative paths for worktrees")),
1390 OPT_END()
1391 };
1392 int rc = 0;
@@ -1392,8 +1394,8 @@ static int repair(int ac, const char **av, const char *prefix)
1394 ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0);
1395 p = ac > 0 ? av : self;
1396 for (; *p; p++)
1395 - repair_worktree_at_path(*p, report_repair, &rc);
1396 - repair_worktrees(report_repair, &rc);
1397 + repair_worktree_at_path(*p, report_repair, &rc, use_relative_paths);
1398 + repair_worktrees(report_repair, &rc, use_relative_paths);
1399 return rc;
1400 }
1401
t/t2406-worktree-repair.sh
+39
@@ -216,4 +216,43 @@ test_expect_success 'repair copied main and linked worktrees' '
216 test_cmp dup/linked.expect dup/linked/.git
217 '
218
219 +test_expect_success 'repair worktree with relative path with missing gitfile' '
220 + test_when_finished "rm -rf main wt" &&
221 + test_create_repo main &&
222 + git -C main config worktree.useRelativePaths true &&
223 + test_commit -C main init &&
224 + git -C main worktree add --detach ../wt &&
225 + rm wt/.git &&
226 + test_path_is_missing wt/.git &&
227 + git -C main worktree repair &&
228 + echo "gitdir: ../main/.git/worktrees/wt" >expect &&
229 + test_cmp expect wt/.git
230 +'
231 +
232 +test_expect_success 'repair absolute worktree to use relative paths' '
233 + test_when_finished "rm -rf main side sidemoved" &&
234 + test_create_repo main &&
235 + test_commit -C main init &&
236 + git -C main worktree add --detach ../side &&
237 + echo "../../../../sidemoved/.git" >expect-gitdir &&
238 + echo "gitdir: ../main/.git/worktrees/side" >expect-gitfile &&
239 + mv side sidemoved &&
240 + git -C main worktree repair --relative-paths ../sidemoved &&
241 + test_cmp expect-gitdir main/.git/worktrees/side/gitdir &&
242 + test_cmp expect-gitfile sidemoved/.git
243 +'
244 +
245 +test_expect_success 'repair relative worktree to use absolute paths' '
246 + test_when_finished "rm -rf main side sidemoved" &&
247 + test_create_repo main &&
248 + test_commit -C main init &&
249 + git -C main worktree add --relative-paths --detach ../side &&
250 + echo "$(pwd)/sidemoved/.git" >expect-gitdir &&
251 + echo "gitdir: $(pwd)/main/.git/worktrees/side" >expect-gitfile &&
252 + mv side sidemoved &&
253 + git -C main worktree repair ../sidemoved &&
254 + test_cmp expect-gitdir main/.git/worktrees/side/gitdir &&
255 + test_cmp expect-gitfile sidemoved/.git
256 +'
257 +
258 test_done
worktree.c
+27 -29
@@ -573,12 +573,13 @@ int other_head_refs(each_ref_fn fn, void *cb_data)
573 * pointing at <repo>/worktrees/<id>.
574 */
575 static void repair_gitfile(struct worktree *wt,
576 - worktree_repair_fn fn, void *cb_data)
576 + worktree_repair_fn fn, void *cb_data,
577 + int use_relative_paths)
578 {
579 struct strbuf dotgit = STRBUF_INIT;
580 + struct strbuf gitdir = STRBUF_INIT;
581 struct strbuf repo = STRBUF_INIT;
582 struct strbuf backlink = STRBUF_INIT;
581 - struct strbuf tmp = STRBUF_INIT;
583 char *dotgit_contents = NULL;
584 const char *repair = NULL;
585 int err;
@@ -594,6 +595,7 @@ static void repair_gitfile(struct worktree *wt,
595
596 strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
597 strbuf_addf(&dotgit, "%s/.git", wt->path);
598 + strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
599 dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
600
601 if (dotgit_contents) {
@@ -611,18 +613,20 @@ static void repair_gitfile(struct worktree *wt,
613 repair = _(".git file broken");
614 else if (fspathcmp(backlink.buf, repo.buf))
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
619 if (repair) {
620 fn(0, wt->path, repair, cb_data);
617 - write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, wt->path, &tmp));
621 + write_worktree_linking_files(dotgit, gitdir, use_relative_paths);
622 }
623
624 done:
625 free(dotgit_contents);
626 strbuf_release(&repo);
627 strbuf_release(&dotgit);
628 + strbuf_release(&gitdir);
629 strbuf_release(&backlink);
625 - strbuf_release(&tmp);
630 }
631
632 static void repair_noop(int iserr UNUSED,
@@ -633,7 +637,7 @@ static void repair_noop(int iserr UNUSED,
637 /* nothing */
638 }
639
636 -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 {
642 struct worktree **worktrees = get_worktrees_internal(1);
643 struct worktree **wt = worktrees + 1; /* +1 skips main worktree */
@@ -641,7 +645,7 @@ void repair_worktrees(worktree_repair_fn fn, void *cb_data)
645 if (!fn)
646 fn = repair_noop;
647 for (; *wt; wt++)
644 - repair_gitfile(*wt, fn, cb_data);
648 + repair_gitfile(*wt, fn, cb_data, use_relative_paths);
649 free_worktrees(worktrees);
650 }
651
@@ -757,16 +761,14 @@ error:
761 * the worktree's path.
762 */
763 void repair_worktree_at_path(const char *path,
760 - worktree_repair_fn fn, void *cb_data)
764 + worktree_repair_fn fn, void *cb_data,
765 + int use_relative_paths)
766 {
767 struct strbuf dotgit = STRBUF_INIT;
763 - struct strbuf realdotgit = STRBUF_INIT;
768 struct strbuf backlink = STRBUF_INIT;
769 struct strbuf inferred_backlink = STRBUF_INIT;
770 struct strbuf gitdir = STRBUF_INIT;
771 struct strbuf olddotgit = STRBUF_INIT;
768 - struct strbuf realolddotgit = STRBUF_INIT;
769 - struct strbuf tmp = STRBUF_INIT;
772 char *dotgit_contents = NULL;
773 const char *repair = NULL;
774 int err;
@@ -778,25 +780,25 @@ void repair_worktree_at_path(const char *path,
780 goto done;
781
782 strbuf_addf(&dotgit, "%s/.git", path);
781 - if (!strbuf_realpath(&realdotgit, dotgit.buf, 0)) {
783 + if (!strbuf_realpath(&dotgit, dotgit.buf, 0)) {
784 fn(1, path, _("not a valid path"), cb_data);
785 goto done;
786 }
787
786 - infer_backlink(realdotgit.buf, &inferred_backlink);
788 + infer_backlink(dotgit.buf, &inferred_backlink);
789 strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);
788 - dotgit_contents = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err));
790 + dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
791 if (dotgit_contents) {
792 if (is_absolute_path(dotgit_contents)) {
793 strbuf_addstr(&backlink, dotgit_contents);
794 } else {
793 - strbuf_addbuf(&backlink, &realdotgit);
795 + strbuf_addbuf(&backlink, &dotgit);
796 strbuf_strip_suffix(&backlink, ".git");
797 strbuf_addstr(&backlink, dotgit_contents);
798 strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
799 }
800 } else if (err == READ_GITFILE_ERR_NOT_A_FILE) {
799 - fn(1, realdotgit.buf, _("unable to locate repository; .git is not a file"), cb_data);
801 + fn(1, dotgit.buf, _("unable to locate repository; .git is not a file"), cb_data);
802 goto done;
803 } else if (err == READ_GITFILE_ERR_NOT_A_REPO) {
804 if (inferred_backlink.len) {
@@ -809,11 +811,11 @@ void repair_worktree_at_path(const char *path,
811 */
812 strbuf_swap(&backlink, &inferred_backlink);
813 } else {
812 - fn(1, realdotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data);
814 + fn(1, dotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data);
815 goto done;
816 }
817 } else {
816 - fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data);
818 + fn(1, dotgit.buf, _("unable to locate repository; .git file broken"), cb_data);
819 goto done;
820 }
821
@@ -835,39 +837,35 @@ void repair_worktree_at_path(const char *path,
837 * in the "copy" repository. In this case, point the "copy" worktree's
838 * .git file at the "copy" repository.
839 */
838 - if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) {
840 + if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf))
841 strbuf_swap(&backlink, &inferred_backlink);
840 - }
842
843 strbuf_addf(&gitdir, "%s/gitdir", backlink.buf);
844 if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0)
845 repair = _("gitdir unreadable");
846 + else if (use_relative_paths == is_absolute_path(olddotgit.buf))
847 + repair = _("gitdir absolute/relative path mismatch");
848 else {
849 strbuf_rtrim(&olddotgit);
847 - if (is_absolute_path(olddotgit.buf)) {
848 - strbuf_addbuf(&realolddotgit, &olddotgit);
849 - } else {
850 - strbuf_addf(&realolddotgit, "%s/%s", backlink.buf, olddotgit.buf);
851 - strbuf_realpath_forgiving(&realolddotgit, realolddotgit.buf, 0);
850 + if (!is_absolute_path(olddotgit.buf)) {
851 + strbuf_insertf(&olddotgit, 0, "%s/", backlink.buf);
852 + strbuf_realpath_forgiving(&olddotgit, olddotgit.buf, 0);
853 }
853 - if (fspathcmp(realolddotgit.buf, realdotgit.buf))
854 + if (fspathcmp(olddotgit.buf, dotgit.buf))
855 repair = _("gitdir incorrect");
856 }
857
858 if (repair) {
859 fn(0, gitdir.buf, repair, cb_data);
859 - write_file(gitdir.buf, "%s", relative_path(realdotgit.buf, backlink.buf, &tmp));
860 + write_worktree_linking_files(dotgit, gitdir, use_relative_paths);
861 }
862 done:
863 free(dotgit_contents);
864 strbuf_release(&olddotgit);
864 - strbuf_release(&realolddotgit);
865 strbuf_release(&backlink);
866 strbuf_release(&inferred_backlink);
867 strbuf_release(&gitdir);
868 - strbuf_release(&realdotgit);
868 strbuf_release(&dotgit);
870 - strbuf_release(&tmp);
869 }
870
871 int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
worktree.h
+3 -2
@@ -129,7 +129,7 @@ typedef void (* worktree_repair_fn)(int iserr, const char *path,
129 * function, if non-NULL, is called with the path of the worktree and a
130 * description of the repair or error, along with the callback user-data.
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
134 /*
135 * Repair the linked worktrees after the gitdir has been moved.
@@ -151,7 +151,8 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path
151 * worktree and a description of the repair or error, along with the callback
152 * user-data.
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);
156
157 /*
158 * Free up the memory for a worktree.