diff --git a/Documentation/config/worktree.txt b/Documentation/config/worktree.txt
index 048e349482df6c892055720eb53cdcd6c327b6ed..d0589c21d034f07f70ca172947520d7d78720cca 100644
--- a/Documentation/config/worktree.txt
+++ b/Documentation/config/worktree.txt
@@ -7,3 +7,8 @@ worktree.guessRemote::
 	such a branch exists, it is checked out and set as "upstream"
 	for the new branch.  If no such match can be found, it falls
 	back to creating a new branch from the current HEAD.
+worktree.useRelativePaths::
+	If set to `true`, the worktree paths will be stored relative to the
+	repository’s top-level directory, rather than using absolute paths.
+	This is particularly useful for setups where the repository may be moved
+	between different locations or environments.
diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 70437c815f13852bd2eb862176b8b933e6de0acf..4a6ad481ee1d44b43683bd67881a2771e2b090c7 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -216,6 +216,15 @@ To remove a locked worktree, specify `--force` twice.
 This can also be set up as the default behaviour by using the
 `worktree.guessRemote` config option.
 
+--[no-]relative-paths::
+	The worktree paths will be stored relative to the repository’s
+	top-level directory, rather than using absolute paths.
+	This is particularly useful for setups where the repository may be moved
+	between different locations or environments.
++
+This can also be set up as the default behaviour by using the
+`worktree.useRelativePaths` config option.
+
 --[no-]track::
 	When creating a new branch, if `<commit-ish>` is a branch,
 	mark it as "upstream" from the new branch.  This is the
diff --git a/builtin/worktree.c b/builtin/worktree.c
index dae63dedf4cac2621f51f95a39aa456b33acd894..bb06830d6fe82aa97833c6e87f034115dfaa23bd 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -134,6 +134,9 @@ static int git_worktree_config(const char *var, const char *value,
 	if (!strcmp(var, "worktree.guessremote")) {
 		guess_remote = git_config_bool(var, value);
 		return 0;
+	} else if (!strcmp(var, "worktree.userelativepaths")) {
+		use_relative_paths = git_config_bool(var, value);
+		return 0;
 	}
 
 	return git_default_config(var, value, ctx, cb);
@@ -414,8 +417,7 @@ 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, sb_tmp = STRBUF_INIT;
-	struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT;
+	struct strbuf sb = STRBUF_INIT;
 	const char *name;
 	struct strvec child_env = STRVEC_INIT;
 	unsigned int counter = 0;
@@ -491,10 +493,7 @@ static int add_worktree(const char *path, const char *refname,
 
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
-	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));
-	write_file(sb_git.buf, "gitdir: %s", relative_path(sb_repo_realpath.buf, sb_path_realpath.buf, &sb_tmp));
+	write_worktree_linking_files(sb_git, sb);
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
 	write_file(sb.buf, "../..");
@@ -578,12 +577,9 @@ 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);
 	free_worktree(wt);
 	return ret;
@@ -796,6 +792,8 @@ static int add(int ac, const char **av, const char *prefix)
 			     PARSE_OPT_NOARG | PARSE_OPT_OPTARG),
 		OPT_BOOL(0, "guess-remote", &guess_remote,
 			 N_("try to match the new branch name with a remote-tracking branch")),
+		OPT_BOOL(0, "relative-paths", &use_relative_paths,
+			 N_("use relative paths for worktrees")),
 		OPT_END()
 	};
 	int ret;
@@ -1189,6 +1187,8 @@ static int move_worktree(int ac, const char **av, const char *prefix)
 		OPT__FORCE(&force,
 			 N_("force move even if worktree is dirty or locked"),
 			 PARSE_OPT_NOCOMPLETE),
+		OPT_BOOL(0, "relative-paths", &use_relative_paths,
+			 N_("use relative paths for worktrees")),
 		OPT_END()
 	};
 	struct worktree **worktrees, *wt;
@@ -1382,6 +1382,8 @@ static int repair(int ac, const char **av, const char *prefix)
 	const char **p;
 	const char *self[] = { ".", NULL };
 	struct option options[] = {
+		OPT_BOOL(0, "relative-paths", &use_relative_paths,
+			 N_("use relative paths for worktrees")),
 		OPT_END()
 	};
 	int rc = 0;
diff --git a/t/t0001-init.sh b/t/t0001-init.sh
index 0178aa62a41f1606f2382a4a10ab593ccf11e0e8..e21b9aa5e7f4599af8b20165b50896c9a49ba7ea 100755
--- a/t/t0001-init.sh
+++ b/t/t0001-init.sh
@@ -435,6 +435,7 @@ sep_git_dir_worktree ()  {
 	test_when_finished "rm -rf mainwt linkwt seprepo" &&
 	git init mainwt &&
 	test_commit -C mainwt gumby &&
+	git -C mainwt config worktree.useRelativePaths "$([ "$2" = "relative" ] && echo true || echo false)" &&
 	git -C mainwt worktree add --detach ../linkwt &&
 	git -C "$1" init --separate-git-dir ../seprepo &&
 	git -C mainwt rev-parse --git-common-dir >expect &&
@@ -442,12 +443,20 @@ sep_git_dir_worktree ()  {
 	test_cmp expect actual
 }
 
-test_expect_success 're-init to move gitdir with linked worktrees' '
-	sep_git_dir_worktree mainwt
+test_expect_success 're-init to move gitdir with linked worktrees (absolute)' '
+	sep_git_dir_worktree mainwt absolute
 '
 
-test_expect_success 're-init to move gitdir within linked worktree' '
-	sep_git_dir_worktree linkwt
+test_expect_success 're-init to move gitdir within linked worktree (absolute)' '
+	sep_git_dir_worktree linkwt absolute
+'
+
+test_expect_success 're-init to move gitdir with linked worktrees (relative)' '
+	sep_git_dir_worktree mainwt relative
+'
+
+test_expect_success 're-init to move gitdir within linked worktree (relative)' '
+	sep_git_dir_worktree linkwt relative
 '
 
 test_expect_success MINGW '.git hidden' '
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index cfc4aeb1798c6d023909cec771e5b74e983af5ea..630c13230b3cc762ce8d943e22be8891aa2b1871 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -1207,4 +1207,45 @@ test_expect_success '"add" with initialized submodule, with submodule.recurse se
 	git -C project-clone -c submodule.recurse worktree add ../project-5
 '
 
+test_expect_success 'can create worktrees with relative paths' '
+	test_when_finished "git worktree remove relative" &&
+	git config worktree.useRelativePaths false &&
+	git worktree add --relative-paths ./relative &&
+	cat relative/.git >actual &&
+	echo "gitdir: ../.git/worktrees/relative" >expect &&
+	test_cmp expect actual &&
+	cat .git/worktrees/relative/gitdir >actual &&
+	echo "../../../relative/.git" >expect &&
+	test_cmp expect actual
+
+'
+
+test_expect_success 'can create worktrees with absolute paths' '
+	git config worktree.useRelativePaths true &&
+	git worktree add ./relative &&
+	cat relative/.git >actual &&
+	echo "gitdir: ../.git/worktrees/relative" >expect &&
+	test_cmp expect actual &&
+	git worktree add --no-relative-paths ./absolute &&
+	cat absolute/.git >actual &&
+	echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'move repo without breaking relative internal links' '
+	test_when_finished rm -rf repo moved &&
+	git init repo &&
+	(
+		cd repo &&
+		git config worktree.useRelativePaths true &&
+		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/t/t2401-worktree-prune.sh b/t/t2401-worktree-prune.sh
index 976d048e3efc74be9cd909ce76d552b3944d2e10..5eb52b9abbf29514dc082c260ebb7a5e8e63aae0 100755
--- a/t/t2401-worktree-prune.sh
+++ b/t/t2401-worktree-prune.sh
@@ -120,11 +120,12 @@ test_expect_success 'prune duplicate (main/linked)' '
 	! test -d .git/worktrees/wt
 '
 
-test_expect_success 'not prune proper worktrees when run inside linked worktree' '
+test_expect_success 'not prune proper worktrees inside linked worktree with relative paths' '
 	test_when_finished rm -rf repo wt_ext &&
 	git init repo &&
 	(
 	    cd repo &&
+	    git config worktree.useRelativePaths true &&
 	    echo content >file &&
 	    git add file &&
 	    git commit -m msg &&
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index 33ea9cb21ba07c9563530b54da06753eaa993fe2..780daa6cd6351f8fa9434619cc212aade8f01420 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -261,6 +261,7 @@ test_expect_success 'broken main worktree still at the top' '
 '
 
 test_expect_success 'linked worktrees are sorted' '
+	test_when_finished "rm -rf sorted" &&
 	mkdir sorted &&
 	git init sorted/main &&
 	(
@@ -280,6 +281,27 @@ test_expect_success 'linked worktrees are sorted' '
 	test_cmp expected sorted/main/actual
 '
 
+test_expect_success 'linked worktrees with relative paths are shown with absolute paths' '
+	test_when_finished "rm -rf sorted" &&
+	mkdir sorted &&
+	git init sorted/main &&
+	(
+		cd sorted/main &&
+		test_tick &&
+		test_commit new &&
+		git worktree add --relative-paths ../first &&
+		git worktree add ../second &&
+		git worktree list --porcelain >out &&
+		grep ^worktree out >actual
+	) &&
+	cat >expected <<-EOF &&
+	worktree $(pwd)/sorted/main
+	worktree $(pwd)/sorted/first
+	worktree $(pwd)/sorted/second
+	EOF
+	test_cmp expected sorted/main/actual
+'
+
 test_expect_success 'worktree path when called in .git directory' '
 	git worktree list >list1 &&
 	git -C .git worktree list >list2 &&
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index 901342ea09b51a8e832f1109fbb737df84283aa2..6ce9ed3f1e6b3f73d2a290e770233eec30221fe5 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh
@@ -247,4 +247,26 @@ test_expect_success 'not remove a repo with initialized submodule' '
 	)
 '
 
+test_expect_success 'move worktree with absolute path to relative path' '
+	git config worktree.useRelativePaths false &&
+	git worktree add ./absolute &&
+	git worktree move --relative-paths absolute relative &&
+	cat relative/.git >actual &&
+	echo "gitdir: ../.git/worktrees/absolute" >expect &&
+	test_cmp expect actual &&
+	git config worktree.useRelativePaths true &&
+	git worktree move relative relative2 &&
+	cat relative2/.git >actual &&
+	echo "gitdir: ../.git/worktrees/absolute" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'move worktree with relative path to absolute path' '
+	git config worktree.useRelativePaths true &&
+	git worktree move --no-relative-paths relative2 absolute &&
+	cat absolute/.git >actual &&
+	echo "gitdir: $(pwd)/.git/worktrees/absolute" >expect &&
+	test_cmp expect actual
+'
+
 test_done
diff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh
index 7686e60f6ad186519b275f11a5e14064c905b207..84451e903b2ef3c645c0311faf055c846588baf6 100755
--- a/t/t2406-worktree-repair.sh
+++ b/t/t2406-worktree-repair.sh
@@ -216,4 +216,30 @@ test_expect_success 'repair copied main and linked worktrees' '
 	test_cmp dup/linked.expect dup/linked/.git
 '
 
+test_expect_success 'repair absolute worktree to use relative paths' '
+	test_when_finished "rm -rf main side sidemoved" &&
+	test_create_repo main &&
+	test_commit -C main init &&
+	git -C main worktree add --detach ../side &&
+	echo "../../../../sidemoved/.git" >expect-gitdir &&
+	echo "gitdir: ../main/.git/worktrees/side" >expect-gitfile &&
+	mv side sidemoved &&
+	git -C main worktree repair --relative-paths ../sidemoved &&
+	test_cmp expect-gitdir main/.git/worktrees/side/gitdir &&
+	test_cmp expect-gitfile sidemoved/.git
+'
+
+test_expect_success 'repair relative worktree to use absolute paths' '
+	test_when_finished "rm -rf main side sidemoved" &&
+	test_create_repo main &&
+	test_commit -C main init &&
+	git -C main worktree add --relative-paths --detach ../side &&
+	echo "$(pwd)/sidemoved/.git" >expect-gitdir &&
+	echo "gitdir: $(pwd)/main/.git/worktrees/side" >expect-gitfile &&
+	mv side sidemoved &&
+	git -C main worktree repair ../sidemoved &&
+	test_cmp expect-gitdir main/.git/worktrees/side/gitdir &&
+	test_cmp expect-gitfile sidemoved/.git
+'
+
 test_done
diff --git a/worktree.c b/worktree.c
index 77ff484d3ec48c547ee4e3d958dfa28a52c1eaa7..f4cee73d7a1edecafdff30b6d5e2d9dd1365b93e 100644
--- a/worktree.c
+++ b/worktree.c
@@ -14,6 +14,8 @@
 #include "wt-status.h"
 #include "config.h"
 
+int use_relative_paths = 0;
+
 void free_worktree(struct worktree *worktree)
 {
 	if (!worktree)
@@ -111,9 +113,9 @@ struct worktree *get_linked_worktree(const char *id,
 	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);
+		strbuf_strip_suffix(&path, "gitdir");
+		strbuf_addbuf(&path, &worktree_path);
+		strbuf_realpath_forgiving(&worktree_path, path.buf, 0);
 	}
 
 	CALLOC_ARRAY(worktree, 1);
@@ -379,29 +381,24 @@ 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 file = STRBUF_INIT;
-	struct strbuf tmp = STRBUF_INIT;
+	struct strbuf dotgit = STRBUF_INIT;
+	struct strbuf gitdir = STRBUF_INIT;
 
 	if (is_main_worktree(wt))
 		BUG("can't relocate main worktree");
 
-	strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
+	strbuf_realpath(&gitdir, git_common_path("worktrees/%s/gitdir", wt->id), 1);
 	strbuf_realpath(&path, path_, 1);
+	strbuf_addf(&dotgit, "%s/.git", path.buf);
 	if (fspathcmp(wt->path, path.buf)) {
-		strbuf_addf(&file, "%s/gitdir", repo.buf);
-		write_file(file.buf, "%s/.git", relative_path(path.buf, repo.buf, &tmp));
-		strbuf_reset(&file);
-		strbuf_addf(&file, "%s/.git", path.buf);
-		write_file(file.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp));
+		write_worktree_linking_files(dotgit, gitdir);
 
 		free(wt->path);
 		wt->path = strbuf_detach(&path, NULL);
 	}
 	strbuf_release(&path);
-	strbuf_release(&repo);
-	strbuf_release(&file);
-	strbuf_release(&tmp);
+	strbuf_release(&dotgit);
+	strbuf_release(&gitdir);
 }
 
 int is_worktree_being_rebased(const struct worktree *wt,
@@ -580,9 +577,9 @@ static void repair_gitfile(struct worktree *wt,
 			   worktree_repair_fn fn, void *cb_data)
 {
 	struct strbuf dotgit = STRBUF_INIT;
+	struct strbuf gitdir = STRBUF_INIT;
 	struct strbuf repo = STRBUF_INIT;
 	struct strbuf backlink = STRBUF_INIT;
-	struct strbuf tmp = STRBUF_INIT;
 	char *dotgit_contents = NULL;
 	const char *repair = NULL;
 	int err;
@@ -598,6 +595,7 @@ static void repair_gitfile(struct worktree *wt,
 
 	strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
 	strbuf_addf(&dotgit, "%s/.git", wt->path);
+	strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
 	dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
 
 	if (dotgit_contents) {
@@ -615,18 +613,20 @@ static void repair_gitfile(struct worktree *wt,
 		repair = _(".git file broken");
 	else if (fspathcmp(backlink.buf, repo.buf))
 		repair = _(".git file incorrect");
+	else if (use_relative_paths == is_absolute_path(dotgit_contents))
+		repair = _(".git file absolute/relative path mismatch");
 
 	if (repair) {
 		fn(0, wt->path, repair, cb_data);
-		write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, wt->path, &tmp));
+		write_worktree_linking_files(dotgit, gitdir);
 	}
 
 done:
 	free(dotgit_contents);
 	strbuf_release(&repo);
 	strbuf_release(&dotgit);
+	strbuf_release(&gitdir);
 	strbuf_release(&backlink);
-	strbuf_release(&tmp);
 }
 
 static void repair_noop(int iserr UNUSED,
@@ -651,45 +651,30 @@ void repair_worktrees(worktree_repair_fn fn, void *cb_data)
 
 void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path)
 {
-	struct strbuf path = STRBUF_INIT;
-	struct strbuf repo = STRBUF_INIT;
 	struct strbuf gitdir = STRBUF_INIT;
 	struct strbuf dotgit = STRBUF_INIT;
-	struct strbuf olddotgit = STRBUF_INIT;
-	struct strbuf tmp = STRBUF_INIT;
 
 	if (is_main_worktree(wt))
 		goto done;
 
-	strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
-	strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
+	strbuf_realpath(&gitdir, git_common_path("worktrees/%s/gitdir", wt->id), 1);
 
-	if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0)
+	if (strbuf_read_file(&dotgit, gitdir.buf, 0) < 0)
 		goto done;
 
-	strbuf_rtrim(&olddotgit);
-	if (is_absolute_path(olddotgit.buf)) {
-		strbuf_addbuf(&dotgit, &olddotgit);
-	} else {
-		strbuf_addf(&dotgit, "%s/worktrees/%s/%s", old_path, wt->id, olddotgit.buf);
+	strbuf_rtrim(&dotgit);
+	if (!is_absolute_path(dotgit.buf)) {
+		strbuf_insertf(&dotgit, 0, "%s/worktrees/%s/", old_path, wt->id);
 		strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0);
 	}
 
 	if (!file_exists(dotgit.buf))
 		goto done;
 
-	strbuf_addbuf(&path, &dotgit);
-	strbuf_strip_suffix(&path, "/.git");
-
-	write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp));
-	write_file(gitdir.buf, "%s", relative_path(dotgit.buf, repo.buf, &tmp));
+	write_worktree_linking_files(dotgit, gitdir);
 done:
-	strbuf_release(&path);
-	strbuf_release(&repo);
 	strbuf_release(&gitdir);
 	strbuf_release(&dotgit);
-	strbuf_release(&olddotgit);
-	strbuf_release(&tmp);
 }
 
 void repair_worktrees_after_gitdir_move(const char *old_path)
@@ -725,12 +710,15 @@ static int is_main_worktree_path(const char *path)
  * won't know which <repo>/worktrees/<id>/gitdir to repair. However, we may
  * be able to infer the gitdir by manually reading /path/to/worktree/.git,
  * extracting the <id>, and checking if <repo>/worktrees/<id> exists.
+ *
+ * Returns -1 on failure and strbuf.len on success.
  */
 static int infer_backlink(const char *gitfile, struct strbuf *inferred)
 {
 	struct strbuf actual = STRBUF_INIT;
 	const char *id;
 
+	strbuf_reset(inferred);
 	if (strbuf_read_file(&actual, gitfile, 0) < 0)
 		goto error;
 	if (!starts_with(actual.buf, "gitdir:"))
@@ -741,18 +729,16 @@ static int infer_backlink(const char *gitfile, struct strbuf *inferred)
 	id++; /* advance past '/' to point at <id> */
 	if (!*id)
 		goto error;
-	strbuf_reset(inferred);
 	strbuf_git_common_path(inferred, the_repository, "worktrees/%s", id);
 	if (!is_directory(inferred->buf))
 		goto error;
 
 	strbuf_release(&actual);
-	return 1;
-
+	return inferred->len;
 error:
 	strbuf_release(&actual);
 	strbuf_reset(inferred); /* clear invalid path */
-	return 0;
+	return -1;
 }
 
 /*
@@ -763,13 +749,10 @@ void repair_worktree_at_path(const char *path,
 			     worktree_repair_fn fn, void *cb_data)
 {
 	struct strbuf dotgit = STRBUF_INIT;
-	struct strbuf realdotgit = STRBUF_INIT;
 	struct strbuf backlink = STRBUF_INIT;
 	struct strbuf inferred_backlink = STRBUF_INIT;
 	struct strbuf gitdir = STRBUF_INIT;
 	struct strbuf olddotgit = STRBUF_INIT;
-	struct strbuf realolddotgit = STRBUF_INIT;
-	struct strbuf tmp = STRBUF_INIT;
 	char *dotgit_contents = NULL;
 	const char *repair = NULL;
 	int err;
@@ -781,25 +764,25 @@ void repair_worktree_at_path(const char *path,
 		goto done;
 
 	strbuf_addf(&dotgit, "%s/.git", path);
-	if (!strbuf_realpath(&realdotgit, dotgit.buf, 0)) {
+	if (!strbuf_realpath(&dotgit, dotgit.buf, 0)) {
 		fn(1, path, _("not a valid path"), cb_data);
 		goto done;
 	}
 
-	infer_backlink(realdotgit.buf, &inferred_backlink);
+	infer_backlink(dotgit.buf, &inferred_backlink);
 	strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);
-	dotgit_contents = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err));
+	dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
 	if (dotgit_contents) {
 		if (is_absolute_path(dotgit_contents)) {
 			strbuf_addstr(&backlink, dotgit_contents);
 		} else {
-			strbuf_addbuf(&backlink, &realdotgit);
+			strbuf_addbuf(&backlink, &dotgit);
 			strbuf_strip_suffix(&backlink, ".git");
 			strbuf_addstr(&backlink, dotgit_contents);
 			strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
 		}
 	} else if (err == READ_GITFILE_ERR_NOT_A_FILE) {
-		fn(1, realdotgit.buf, _("unable to locate repository; .git is not a file"), cb_data);
+		fn(1, dotgit.buf, _("unable to locate repository; .git is not a file"), cb_data);
 		goto done;
 	} else if (err == READ_GITFILE_ERR_NOT_A_REPO) {
 		if (inferred_backlink.len) {
@@ -812,11 +795,11 @@ void repair_worktree_at_path(const char *path,
 			 */
 			strbuf_swap(&backlink, &inferred_backlink);
 		} else {
-			fn(1, realdotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data);
+			fn(1, dotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data);
 			goto done;
 		}
 	} else {
-		fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data);
+		fn(1, dotgit.buf, _("unable to locate repository; .git file broken"), cb_data);
 		goto done;
 	}
 
@@ -838,39 +821,35 @@ void repair_worktree_at_path(const char *path,
 	 * in the "copy" repository. In this case, point the "copy" worktree's
 	 * .git file at the "copy" repository.
 	 */
-	if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) {
+	if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf))
 		strbuf_swap(&backlink, &inferred_backlink);
-	}
 
 	strbuf_addf(&gitdir, "%s/gitdir", backlink.buf);
 	if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0)
 		repair = _("gitdir unreadable");
+	else if (use_relative_paths == is_absolute_path(olddotgit.buf))
+		repair = _("gitdir absolute/relative path mismatch");
 	else {
 		strbuf_rtrim(&olddotgit);
-		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 (!is_absolute_path(olddotgit.buf)) {
+			strbuf_insertf(&olddotgit, 0, "%s/", backlink.buf);
+			strbuf_realpath_forgiving(&olddotgit, olddotgit.buf, 0);
 		}
-		if (fspathcmp(realolddotgit.buf, realdotgit.buf))
+		if (fspathcmp(olddotgit.buf, dotgit.buf))
 			repair = _("gitdir incorrect");
 	}
 
 	if (repair) {
 		fn(0, gitdir.buf, repair, cb_data);
-		write_file(gitdir.buf, "%s", relative_path(realdotgit.buf, backlink.buf, &tmp));
+		write_worktree_linking_files(dotgit, gitdir);
 	}
 done:
 	free(dotgit_contents);
 	strbuf_release(&olddotgit);
-	strbuf_release(&realolddotgit);
 	strbuf_release(&backlink);
 	strbuf_release(&inferred_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)
@@ -1031,3 +1010,29 @@ int init_worktree_config(struct repository *r)
 	free(main_worktree_file);
 	return res;
 }
+
+void write_worktree_linking_files(struct strbuf dotgit, struct strbuf gitdir)
+{
+	struct strbuf path = STRBUF_INIT;
+	struct strbuf repo = STRBUF_INIT;
+	struct strbuf tmp = STRBUF_INIT;
+
+	strbuf_addbuf(&path, &dotgit);
+	strbuf_strip_suffix(&path, "/.git");
+	strbuf_realpath(&path, path.buf, 1);
+	strbuf_addbuf(&repo, &gitdir);
+	strbuf_strip_suffix(&repo, "/gitdir");
+	strbuf_realpath(&repo, repo.buf, 1);
+
+	if (use_relative_paths) {
+		write_file(gitdir.buf, "%s/.git", relative_path(path.buf, repo.buf, &tmp));
+		write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp));
+	} else {
+		write_file(gitdir.buf, "%s/.git", path.buf);
+		write_file(dotgit.buf, "gitdir: %s", repo.buf);
+	}
+
+	strbuf_release(&path);
+	strbuf_release(&repo);
+	strbuf_release(&tmp);
+}
diff --git a/worktree.h b/worktree.h
index e96118621638667d87c5d7e0452ed10bd1ddf606..5929089891c97318a8f5329f7938264c717050d5 100644
--- a/worktree.h
+++ b/worktree.h
@@ -5,6 +5,8 @@
 
 struct strbuf;
 
+extern int use_relative_paths;
+
 struct worktree {
 	/* The repository this worktree belongs to. */
 	struct repository *repo;
@@ -215,4 +217,16 @@ void strbuf_worktree_ref(const struct worktree *wt,
  */
 int init_worktree_config(struct repository *r);
 
+/**
+ * Write the .git file and gitdir file that links the worktree to the repository.
+ *
+ * The `dotgit` parameter is the path to the worktree's .git file, and `gitdir`
+ * is the path to the repository's `gitdir` file.
+ *
+ * Example
+ *  dotgit: "/path/to/foo/.git"
+ *  gitdir: "/path/to/repo/worktrees/foo/gitdir"
+ */
+void write_worktree_linking_files(struct strbuf dotgit, struct strbuf gitdir);
+
 #endif
