diff mbox series

[v2,2/4] worktree: link worktrees with relative paths

Message ID 20241006060017.171788-3-cdwhite3@pm.me (mailing list archive)
State New
Headers show
Series Link worktrees with relative paths | expand

Commit Message

Caleb White Oct. 6, 2024, 6:01 a.m. UTC
This modifies Git’s handling of worktree linking to use relative
paths instead of absolute paths. Previously, when creating a worktree,
Git would store the absolute paths to both the main repository and the
linked worktrees. These hardcoded absolute paths cause breakages such
as when the repository is moved to a different directory or during
containerized development where the absolute differs between systems.

By switching to relative paths, we help ensure that worktree links are
more resilient when the repository is moved. While links external to the
repository may still break, Git still automatically handles many common
scenarios, reducing the need for manual repair. This is particularly
useful in containerized or portable development environments, where the
absolute path to the repository can differ between systems. Developers
no longer need to reinitialize or repair worktrees after relocating the
repository, improving workflow efficiency and reducing breakages.

For self-contained repositories (such as using a bare repository with
worktrees), where both the repository and its worktrees are located
within the same directory structure, using relative paths guarantees all
links remain functional regardless of where the directory is located.

Signed-off-by: Caleb White <cdwhite3@pm.me>
---
 builtin/worktree.c           |  17 ++--
 t/t2408-worktree-relative.sh |  39 +++++++++
 worktree.c                   | 152 +++++++++++++++++++++++++----------
 3 files changed, 159 insertions(+), 49 deletions(-)
 create mode 100755 t/t2408-worktree-relative.sh

Comments

Eric Sunshine Oct. 6, 2024, 11:05 a.m. UTC | #1
On Sun, Oct 6, 2024 at 2:01 AM Caleb White <cdwhite3@pm.me> wrote:
> This modifies Git’s handling of worktree linking to use relative
> paths instead of absolute paths. Previously, when creating a worktree,
> Git would store the absolute paths to both the main repository and the
> linked worktrees. These hardcoded absolute paths cause breakages such
> as when the repository is moved to a different directory or during
> containerized development where the absolute differs between systems.
>
> By switching to relative paths, we help ensure that worktree links are
> more resilient when the repository is moved. While links external to the
> repository may still break, Git still automatically handles many common
> scenarios, reducing the need for manual repair. This is particularly
> useful in containerized or portable development environments, where the
> absolute path to the repository can differ between systems. Developers
> no longer need to reinitialize or repair worktrees after relocating the
> repository, improving workflow efficiency and reducing breakages.
>
> For self-contained repositories (such as using a bare repository with
> worktrees), where both the repository and its worktrees are located
> within the same directory structure, using relative paths guarantees all
> links remain functional regardless of where the directory is located.
>
> Signed-off-by: Caleb White <cdwhite3@pm.me>

When you reroll, please extend the commit message to give a more
detailed overview of how this patch actually changes the behavior both
at a high level and at a low level. This is especially important since
this patch is sufficiently long and involved that it's not easy to
glean these details at-a-glance from the code changes themselves.

For instance, the cover letter talked about retaining correct behavior
for both absolute and relative paths, but this commit message mentions
only relative paths, so it's not obvious just from reading the commit
message whether absolute paths are still supported or, if they are,
what exactly that support looks like or how the user controls the
choice between the two path types. This sort of information should be
present in the commit message.

Similarly, if there is a chance that this change may break existing
tooling and workflows, then the commit message should talk about those
risks, as well, and what if any remediations are available. Also, if
there are such risks, then Documentation/git-worktree.txt may need to
be updated to warn users.

Regarding what you wrote above, there seems to be a good deal of
redundancy between the first two paragraphs; combining the paragraphs
and folding out the duplication might make the message more
streamlined. I do like the discussion about containerized environments
being used as (at least one) justification for employing relative
paths, and think that may be a good lead-in for the commit message.
Please see [1] for some helpful hints for composing a good commit
message.

I'm not going to review the code itself right now since I haven't been
able to apply the patches or play around with the functionality, but I
wanted to get the above written up since I think this series is going
to need to be rerolled anyhow.

[1]: https://lore.kernel.org/git/xmqqmsm6sc0q.fsf@gitster.g/
shejialuo Oct. 6, 2024, 3:37 p.m. UTC | #2
On Sun, Oct 06, 2024 at 06:01:17AM +0000, Caleb White wrote:
> This modifies Git’s handling of worktree linking to use relative
> paths instead of absolute paths. Previously, when creating a worktree,
> Git would store the absolute paths to both the main repository and the
> linked worktrees. These hardcoded absolute paths cause breakages such
> as when the repository is moved to a different directory or during
> containerized development where the absolute differs between systems.
> 
> By switching to relative paths, we help ensure that worktree links are
> more resilient when the repository is moved. While links external to the
> repository may still break, Git still automatically handles many common
> scenarios, reducing the need for manual repair. This is particularly
> useful in containerized or portable development environments, where the
> absolute path to the repository can differ between systems. Developers
> no longer need to reinitialize or repair worktrees after relocating the
> repository, improving workflow efficiency and reducing breakages.
> 
> For self-contained repositories (such as using a bare repository with
> worktrees), where both the repository and its worktrees are located
> within the same directory structure, using relative paths guarantees all
> links remain functional regardless of where the directory is located.
> 

Eric has already commented on this commit message. I think this commit
has done a lot of things which make the review painful.

> Signed-off-by: Caleb White <cdwhite3@pm.me>
> ---
>  builtin/worktree.c           |  17 ++--
>  t/t2408-worktree-relative.sh |  39 +++++++++
>  worktree.c                   | 152 +++++++++++++++++++++++++----------
>  3 files changed, 159 insertions(+), 49 deletions(-)
>  create mode 100755 t/t2408-worktree-relative.sh
> 
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index fc31d07..99cee56 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -414,7 +414,8 @@ static int add_worktree(const char *path, const char *refname,
>  			const struct add_opts *opts)
>  {
>  	struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
> -	struct strbuf sb = STRBUF_INIT, realpath = STRBUF_INIT;
> +	struct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT;
> +	struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT;
>  	const char *name;
>  	struct strvec child_env = STRVEC_INIT;
>  	unsigned int counter = 0;
> @@ -490,11 +491,11 @@ static int add_worktree(const char *path, const char *refname,
>  
>  	strbuf_reset(&sb);
>  	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
> -	strbuf_realpath(&realpath, sb_git.buf, 1);
> -	write_file(sb.buf, "%s", realpath.buf);
> -	strbuf_realpath(&realpath, repo_get_common_dir(the_repository), 1);
> -	write_file(sb_git.buf, "gitdir: %s/worktrees/%s",
> -		   realpath.buf, name);
> +	strbuf_realpath(&sb_path_realpath, path, 1);
> +	strbuf_realpath(&sb_repo_realpath, sb_repo.buf, 1);
> +	write_file(sb.buf, "%s/.git", relative_path(sb_path_realpath.buf, sb_repo_realpath.buf, &sb_tmp));
> +	strbuf_reset(&sb_tmp);

Do we need reset the "sb_tmp"? I guess we do not need, "relative_path"
does not care about "sb_tmp". It will always reset the value of the
"sb_tmp". So, we may delete this line.

[snip]

> diff --git a/worktree.c b/worktree.c
> index c6d2ede..fc14e9a 100644
> --- a/worktree.c
> +++ b/worktree.c
> @@ -373,18 +379,30 @@ int validate_worktree(const struct worktree *wt, struct strbuf *errmsg,
>  void update_worktree_location(struct worktree *wt, const char *path_)
>  {
>  	struct strbuf path = STRBUF_INIT;
> +	struct strbuf repo = STRBUF_INIT;
> +	struct strbuf tmp = STRBUF_INIT;
> +	char *file = NULL;
>  
>  	if (is_main_worktree(wt))
>  		BUG("can't relocate main worktree");
>  
> +	strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
>  	strbuf_realpath(&path, path_, 1);
>  	if (fspathcmp(wt->path, path.buf)) {
> -		write_file(git_common_path("worktrees/%s/gitdir", wt->id),
> -			   "%s/.git", path.buf);
> +		file = xstrfmt("%s/gitdir", repo.buf);
> +		write_file(file, "%s/.git", relative_path(path.buf, repo.buf, &tmp));
> +		free(file);
> +		strbuf_reset(&tmp);
> +		file = xstrfmt("%s/.git", path.buf);

Still, we do not need to call "strbuf_reset" again for "tmp". But there
is another question here. Should we define the "file" just in this "if"
block and free "file" also in the block?

And I don't think it's a good idea to use "xstrfmt". Here, we need to
allocate two times and free two times. Why not just define a "struct
strbuf" and the use "strbuf_*" method here?

> +		write_file(file, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp));
> +
>  		free(wt->path);
>  		wt->path = strbuf_detach(&path, NULL);
>  	}
> +	free(file);
>  	strbuf_release(&path);
> +	strbuf_release(&repo);
> +	strbuf_release(&tmp);
>  }
>  
> @@ -564,38 +582,52 @@ static void repair_gitfile(struct worktree *wt,
>  {
>  

[snip]

>  	strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
>  	strbuf_addf(&dotgit, "%s/.git", wt->path);
> -	backlink = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
> +	git_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));

Why here we need to use "xstrdup_or_null". The life cycle of the
"git_contents" variable is in the "repair_gitfile" function.

[snip]

I omit the review for the later code, there are too many codes to
review. And due to my limited knowledge about worktree, the overhead
will be too big for me.

Thanks,
Jialuo
Caleb White Oct. 6, 2024, 10:37 p.m. UTC | #3
On Sunday, October 6th, 2024 at 06:05, Eric Sunshine <sunshine@sunshineco.com> wrote:

> When you reroll, please extend the commit message to give a more
> detailed overview of how this patch actually changes the behavior both
> at a high level and at a low level. This is especially important since
> this patch is sufficiently long and involved that it's not easy to
> glean these details at-a-glance from the code changes themselves.

I will do that, I was not sure how much low level detail I should dive into.

> Regarding what you wrote above, there seems to be a good deal of
> redundancy between the first two paragraphs; combining the paragraphs
> and folding out the duplication might make the message more
> streamlined. I do like the discussion about containerized environments
> being used as (at least one) justification for employing relative
> paths, and think that may be a good lead-in for the commit message.
> Please see [1] for some helpful hints for composing a good commit
> message.

Thanks, I will clean up the redundancy and add more detail to the commit.
Caleb White Oct. 6, 2024, 11:57 p.m. UTC | #4
On Sunday, October 6th, 2024 at 10:37, shejialuo <shejialuo@gmail.com> wrote
> Eric has already commented on this commit message. I think this commit
> has done a lot of things which make the review painful.

My apologies, I will do better on this commit message.

> Do we need reset the "sb_tmp"? I guess we do not need, "relative_path"
> does not care about "sb_tmp". It will always reset the value of the
> "sb_tmp". So, we may delete this line.

You are right, this is reset by relative_path(). I originally encountered
a bug and I thought not resetting this strbuf between relative_path() calls
was the cause but it must have been something else that I fixed.

> Still, we do not need to call "strbuf_reset" again for "tmp". But there
> is another question here. Should we define the "file" just in this "if"
> block and free "file" also in the block?

The style this code uses seems to place most / all of the declarations at
the top of the function and frees at the bottom so I think this fits in.

> And I don't think it's a good idea to use "xstrfmt". Here, we need to
> allocate two times and free two times. Why not just define a "struct
> strbuf" and the use "strbuf_*" method here?

I can use strbufs, I just wasn't sure if I really needed a strbuf for
each of the paths and was just trying to reuse a var.

> > strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
> > strbuf_addf(&dotgit, "%s/.git", wt->path);
> > - backlink = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
> > + git_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
> 

> 

> Why here we need to use "xstrdup_or_null". The life cycle of the
> "git_contents" variable is in the "repair_gitfile" function.

This what the existing code used and I saw no reason to change it...
shejialuo Oct. 7, 2024, 3:45 a.m. UTC | #5
On Sun, Oct 06, 2024 at 11:57:22PM +0000, Caleb White wrote:

[snip]

> > Still, we do not need to call "strbuf_reset" again for "tmp". But there
> > is another question here. Should we define the "file" just in this "if"
> > block and free "file" also in the block?
> 
> The style this code uses seems to place most / all of the declarations at
> the top of the function and frees at the bottom so I think this fits in.
> 

Yes, as you have said, the code style places most / all of the
declarations at the top and free at the bottom. But the trouble here is
we will free the "file" in the "if" block.

    char *file = NULL;

    if (...) {
        file = xstrfmt(...);
        free(file);
        file = xstrfmt(...);
    }

    free(file);

If we want to follow the original code style, should we create two
variables at the top and free them at the bottom?

> > And I don't think it's a good idea to use "xstrfmt". Here, we need to
> > allocate two times and free two times. Why not just define a "struct
> > strbuf" and the use "strbuf_*" method here?
> 
> I can use strbufs, I just wasn't sure if I really needed a strbuf for
> each of the paths and was just trying to reuse a var.
> 

You don't need to create a new "strbuf" for each of the paths. You could
just use "strbuf_reset" for only one "struct strbuf".

    struct strbuf file = STRBUF_INIT;

    if (...) {
        strbuf_addf(...);
        strbuf_reset(&file);
        strbuf_addf(...);

    }

    strbuf_release(&file);

> > > strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
> > > strbuf_addf(&dotgit, "%s/.git", wt->path);
> > > - backlink = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
> > > + git_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
> > 
> 
> > 
> 
> > Why here we need to use "xstrdup_or_null". The life cycle of the
> > "git_contents" variable is in the "repair_gitfile" function.
> 
> This what the existing code used and I saw no reason to change it...

Actually, I somehow understand why in the original code, it will use
"xstrdup_or_null" to initialize the "backlink". Because in
"read_gitfile_gently", we will use a static "struct strbuf" as the
buffer.

I guess the intention of the original code is that if we call again
"read_gitfile_gently" in this function or we have another thread which
calls this function, the content of the buffer will be changed. So we
explicitly use "xstrdup_or_null" to create a copy here to avoid this.

But I wonder whether we really need to consider above problem?
Eric Sunshine Oct. 7, 2024, 4:02 a.m. UTC | #6
On Sun, Oct 6, 2024 at 11:45 PM shejialuo <shejialuo@gmail.com> wrote:
> Actually, I somehow understand why in the original code, it will use
> "xstrdup_or_null" to initialize the "backlink". Because in
> "read_gitfile_gently", we will use a static "struct strbuf" as the
> buffer.
>
> I guess the intention of the original code is that if we call again
> "read_gitfile_gently" in this function or we have another thread which
> calls this function, the content of the buffer will be changed. So we
> explicitly use "xstrdup_or_null" to create a copy here to avoid this.

That's correct. The code is being defensive in case the static strbuf
inside read_gitfile_gently() gets overwritten.

> But I wonder whether we really need to consider above problem?

It's easier to reason about the code in this function if we don't have
to worry about the underlying static strbuf. Also, this is not a hot
path so the extra allocation and string copy is not a concern. As
such, it makes sense for the code to remain robust (by creating the
copy) rather than becoming potentially more fragile by eliminating the
string copy.
Caleb White Oct. 7, 2024, 4:59 p.m. UTC | #7
On Sunday, October 6th, 2024 at 22:45, shejialuo <shejialuo@gmail.com> wrote:
> You don't need to create a new "strbuf" for each of the paths. You could
> just use "strbuf_reset" for only one "struct strbuf".
> 

> struct strbuf file = STRBUF_INIT;
> 

> if (...) {
> strbuf_addf(...);
> strbuf_reset(&file);
> strbuf_addf(...);
> 

> }
> 

> strbuf_release(&file);

Ah, this is a better design---I've updated this to use a single strbuf, thanks!
diff mbox series

Patch

diff --git a/builtin/worktree.c b/builtin/worktree.c
index fc31d07..99cee56 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -414,7 +414,8 @@  static int add_worktree(const char *path, const char *refname,
 			const struct add_opts *opts)
 {
 	struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
-	struct strbuf sb = STRBUF_INIT, realpath = STRBUF_INIT;
+	struct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT;
+	struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT;
 	const char *name;
 	struct strvec child_env = STRVEC_INIT;
 	unsigned int counter = 0;
@@ -490,11 +491,11 @@  static int add_worktree(const char *path, const char *refname,
 
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
-	strbuf_realpath(&realpath, sb_git.buf, 1);
-	write_file(sb.buf, "%s", realpath.buf);
-	strbuf_realpath(&realpath, repo_get_common_dir(the_repository), 1);
-	write_file(sb_git.buf, "gitdir: %s/worktrees/%s",
-		   realpath.buf, name);
+	strbuf_realpath(&sb_path_realpath, path, 1);
+	strbuf_realpath(&sb_repo_realpath, sb_repo.buf, 1);
+	write_file(sb.buf, "%s/.git", relative_path(sb_path_realpath.buf, sb_repo_realpath.buf, &sb_tmp));
+	strbuf_reset(&sb_tmp);
+	write_file(sb_git.buf, "gitdir: %s", relative_path(sb_repo_realpath.buf, sb_path_realpath.buf, &sb_tmp));
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
 	write_file(sb.buf, "../..");
@@ -578,11 +579,13 @@  static int add_worktree(const char *path, const char *refname,
 
 	strvec_clear(&child_env);
 	strbuf_release(&sb);
+	strbuf_release(&sb_tmp);
 	strbuf_release(&symref);
 	strbuf_release(&sb_repo);
+	strbuf_release(&sb_repo_realpath);
 	strbuf_release(&sb_git);
+	strbuf_release(&sb_path_realpath);
 	strbuf_release(&sb_name);
-	strbuf_release(&realpath);
 	free_worktree(wt);
 	return ret;
 }
diff --git a/t/t2408-worktree-relative.sh b/t/t2408-worktree-relative.sh
new file mode 100755
index 0000000..a3136db
--- /dev/null
+++ b/t/t2408-worktree-relative.sh
@@ -0,0 +1,39 @@ 
+#!/bin/sh
+
+test_description='test worktrees linked with relative paths'
+
+TEST_PASSES_SANITIZE_LEAK=true
+. ./test-lib.sh
+
+test_expect_success 'links worktrees with relative paths' '
+	test_when_finished rm -rf repo &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		git worktree add wt1 &&
+		echo "../../../wt1/.git" >expected_gitdir &&
+		cat .git/worktrees/wt1/gitdir >actual_gitdir &&
+		echo "gitdir: ../.git/worktrees/wt1" >expected_git &&
+		cat wt1/.git >actual_git &&
+		test_cmp expected_gitdir actual_gitdir &&
+		test_cmp expected_git actual_git
+	)
+'
+
+test_expect_success 'move repo without breaking relative internal links' '
+	test_when_finished rm -rf repo moved &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit initial &&
+		git worktree add wt1 &&
+		cd .. &&
+		mv repo moved &&
+		cd moved/wt1 &&
+		git status >out 2>err &&
+		test_must_be_empty err
+	)
+'
+
+test_done
diff --git a/worktree.c b/worktree.c
index c6d2ede..fc14e9a 100644
--- a/worktree.c
+++ b/worktree.c
@@ -110,6 +110,12 @@  struct worktree *get_linked_worktree(const char *id,
 	strbuf_rtrim(&worktree_path);
 	strbuf_strip_suffix(&worktree_path, "/.git");
 
+	if (!is_absolute_path(worktree_path.buf)) {
+	    strbuf_strip_suffix(&path, "gitdir");
+	    strbuf_addbuf(&path, &worktree_path);
+	    strbuf_realpath_forgiving(&worktree_path, path.buf, 0);
+	}
+
 	CALLOC_ARRAY(worktree, 1);
 	worktree->repo = the_repository;
 	worktree->path = strbuf_detach(&worktree_path, NULL);
@@ -373,18 +379,30 @@  int validate_worktree(const struct worktree *wt, struct strbuf *errmsg,
 void update_worktree_location(struct worktree *wt, const char *path_)
 {
 	struct strbuf path = STRBUF_INIT;
+	struct strbuf repo = STRBUF_INIT;
+	struct strbuf tmp = STRBUF_INIT;
+	char *file = NULL;
 
 	if (is_main_worktree(wt))
 		BUG("can't relocate main worktree");
 
+	strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
 	strbuf_realpath(&path, path_, 1);
 	if (fspathcmp(wt->path, path.buf)) {
-		write_file(git_common_path("worktrees/%s/gitdir", wt->id),
-			   "%s/.git", path.buf);
+		file = xstrfmt("%s/gitdir", repo.buf);
+		write_file(file, "%s/.git", relative_path(path.buf, repo.buf, &tmp));
+		free(file);
+		strbuf_reset(&tmp);
+		file = xstrfmt("%s/.git", path.buf);
+		write_file(file, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp));
+
 		free(wt->path);
 		wt->path = strbuf_detach(&path, NULL);
 	}
+	free(file);
 	strbuf_release(&path);
+	strbuf_release(&repo);
+	strbuf_release(&tmp);
 }
 
 int is_worktree_being_rebased(const struct worktree *wt,
@@ -564,38 +582,52 @@  static void repair_gitfile(struct worktree *wt,
 {
 	struct strbuf dotgit = STRBUF_INIT;
 	struct strbuf repo = STRBUF_INIT;
-	char *backlink;
+	struct strbuf backlink = STRBUF_INIT;
+	struct strbuf tmp = STRBUF_INIT;
+	char *git_contents = NULL;
 	const char *repair = NULL;
 	int err;
 
 	/* missing worktree can't be repaired */
 	if (!file_exists(wt->path))
-		return;
+		goto done;
 
 	if (!is_directory(wt->path)) {
 		fn(1, wt->path, _("not a directory"), cb_data);
-		return;
+		goto done;
 	}
 
 	strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
 	strbuf_addf(&dotgit, "%s/.git", wt->path);
-	backlink = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
+	git_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
+
+	if (git_contents) {
+		if (is_absolute_path(git_contents)) {
+			strbuf_addstr(&backlink, git_contents);
+		} else {
+			strbuf_addf(&backlink, "%s/%s", wt->path, git_contents);
+			strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
+		}
+	}
 
 	if (err == READ_GITFILE_ERR_NOT_A_FILE)
 		fn(1, wt->path, _(".git is not a file"), cb_data);
 	else if (err)
 		repair = _(".git file broken");
-	else if (fspathcmp(backlink, repo.buf))
+	else if (fspathcmp(backlink.buf, repo.buf))
 		repair = _(".git file incorrect");
 
 	if (repair) {
 		fn(0, wt->path, repair, cb_data);
-		write_file(dotgit.buf, "gitdir: %s", repo.buf);
+		write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, wt->path, &tmp));
 	}
 
-	free(backlink);
+done:
+	free(git_contents);
 	strbuf_release(&repo);
 	strbuf_release(&dotgit);
+	strbuf_release(&backlink);
+	strbuf_release(&tmp);
 }
 
 static void repair_noop(int iserr UNUSED,
@@ -681,6 +713,8 @@  void repair_worktree_at_path(const char *path,
 	struct strbuf backlink = STRBUF_INIT;
 	struct strbuf gitdir = STRBUF_INIT;
 	struct strbuf olddotgit = STRBUF_INIT;
+	struct strbuf realolddotgit = STRBUF_INIT;
+	struct strbuf tmp = STRBUF_INIT;
 	char *git_contents = NULL;
 	const char *repair = NULL;
 	int err;
@@ -710,97 +744,131 @@  void repair_worktree_at_path(const char *path,
 		fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data);
 		goto done;
 	} else if (git_contents) {
-		strbuf_addstr(&backlink, git_contents);
+		if (is_absolute_path(git_contents)) {
+			strbuf_addstr(&backlink, git_contents);
+		} else {
+			strbuf_addbuf(&backlink, &realdotgit);
+			strbuf_strip_suffix(&backlink, ".git");
+			strbuf_addstr(&backlink, git_contents);
+		}
 	}
 
+	strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
 	strbuf_addf(&gitdir, "%s/gitdir", backlink.buf);
 	if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0)
 		repair = _("gitdir unreadable");
 	else {
 		strbuf_rtrim(&olddotgit);
-		if (fspathcmp(olddotgit.buf, realdotgit.buf))
+		if (is_absolute_path(olddotgit.buf)) {
+			strbuf_addbuf(&realolddotgit, &olddotgit);
+		} else {
+			strbuf_addf(&realolddotgit, "%s/%s", backlink.buf, olddotgit.buf);
+			strbuf_realpath_forgiving(&realolddotgit, realolddotgit.buf, 0);
+		}
+		if (fspathcmp(realolddotgit.buf, realdotgit.buf))
 			repair = _("gitdir incorrect");
 	}
 
 	if (repair) {
 		fn(0, gitdir.buf, repair, cb_data);
-		write_file(gitdir.buf, "%s", realdotgit.buf);
+		write_file(gitdir.buf, "%s", relative_path(realdotgit.buf, backlink.buf, &tmp));
 	}
 done:
 	free(git_contents);
 	strbuf_release(&olddotgit);
+	strbuf_release(&realolddotgit);
 	strbuf_release(&backlink);
 	strbuf_release(&gitdir);
 	strbuf_release(&realdotgit);
 	strbuf_release(&dotgit);
+	strbuf_release(&tmp);
 }
 
 int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
 {
 	struct stat st;
-	char *path;
+	struct strbuf dotgit = STRBUF_INIT;
+	struct strbuf gitdir = STRBUF_INIT;
+	struct strbuf repo = STRBUF_INIT;
+	char *path = NULL;
+	char *file = NULL;
+	int rc = 0;
 	int fd;
 	size_t len;
 	ssize_t read_result;
 
 	*wtpath = NULL;
-	if (!is_directory(git_path("worktrees/%s", id))) {
+	strbuf_realpath(&repo, git_common_path("worktrees/%s", id), 1);
+	strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
+	if (!is_directory(repo.buf)) {
 		strbuf_addstr(reason, _("not a valid directory"));
-		return 1;
+		rc = 1;
+		goto done;
 	}
-	if (file_exists(git_path("worktrees/%s/locked", id)))
-		return 0;
-	if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
+	file = xstrfmt("%s/locked", repo.buf);
+	if (file_exists(file)) {
+		goto done;
+	}
+	if (stat(gitdir.buf, &st)) {
 		strbuf_addstr(reason, _("gitdir file does not exist"));
-		return 1;
+		rc = 1;
+		goto done;
 	}
-	fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
+	fd = open(gitdir.buf, O_RDONLY);
 	if (fd < 0) {
 		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
 			    strerror(errno));
-		return 1;
+		rc = 1;
+		goto done;
 	}
 	len = xsize_t(st.st_size);
 	path = xmallocz(len);
 
 	read_result = read_in_full(fd, path, len);
+	close(fd);
 	if (read_result < 0) {
 		strbuf_addf(reason, _("unable to read gitdir file (%s)"),
 			    strerror(errno));
-		close(fd);
-		free(path);
-		return 1;
-	}
-	close(fd);
-
-	if (read_result != len) {
+		rc = 1;
+		goto done;
+	} else if (read_result != len) {
 		strbuf_addf(reason,
 			    _("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
 			    (uintmax_t)len, (uintmax_t)read_result);
-		free(path);
-		return 1;
+		rc = 1;
+		goto done;
 	}
 	while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
 		len--;
 	if (!len) {
 		strbuf_addstr(reason, _("invalid gitdir file"));
-		free(path);
-		return 1;
+		rc = 1;
+		goto done;
 	}
 	path[len] = '\0';
-	if (!file_exists(path)) {
-		if (stat(git_path("worktrees/%s/index", id), &st) ||
-		    st.st_mtime <= expire) {
+	if (is_absolute_path(path)) {
+		strbuf_addstr(&dotgit, path);
+	} else {
+		strbuf_addf(&dotgit, "%s/%s", repo.buf, path);
+		strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0);
+	}
+	if (!file_exists(dotgit.buf)) {
+		free(file);
+		file = xstrfmt("%s/index", repo.buf);
+		if (stat(file, &st) || st.st_mtime <= expire) {
 			strbuf_addstr(reason, _("gitdir file points to non-existent location"));
-			free(path);
-			return 1;
-		} else {
-			*wtpath = path;
-			return 0;
+			rc = 1;
+			goto done;
 		}
 	}
-	*wtpath = path;
-	return 0;
+	*wtpath = strbuf_detach(&dotgit, NULL);
+done:
+	free(path);
+	free(file);
+	strbuf_release(&dotgit);
+	strbuf_release(&gitdir);
+	strbuf_release(&repo);
+	return rc;
 }
 
 static int move_config_setting(const char *key, const char *value,