diff mbox series

[v3,5/8] worktree: add relative cli/config options to `add` command

Message ID 20241031-wt_relative_options-v3-5-3e44ccdf64e6@pm.me (mailing list archive)
State Superseded
Headers show
Series Allow relative worktree linking to be configured by the user | expand

Commit Message

Caleb White Oct. 31, 2024, 5:05 a.m. UTC
This introduces the `--[no-]relative-paths` CLI option and
`worktree.useRelativePaths` configuration setting to the `worktree add`
command. When enabled these options allow worktrees to be linked using
relative paths, enhancing portability across environments where absolute
paths may differ (e.g., containerized setups, shared network drives).
Git still creates absolute paths by default, but these options allow
users to opt-in to relative paths if desired.

The t2408 test file is removed and more comprehensive tests are
written for the various worktree operations in their own files.

Signed-off-by: Caleb White <cdwhite3@pm.me>
---
 Documentation/config/worktree.txt | 10 ++++++++
 Documentation/git-worktree.txt    |  4 +++
 builtin/worktree.c                | 19 +++++++-------
 t/t2400-worktree-add.sh           | 54 +++++++++++++++++++++++++++++++++++++++
 t/t2401-worktree-prune.sh         |  3 ++-
 t/t2402-worktree-list.sh          | 22 ++++++++++++++++
 t/t2408-worktree-relative.sh      | 39 ----------------------------
 7 files changed, 102 insertions(+), 49 deletions(-)
diff mbox series

Patch

diff --git a/Documentation/config/worktree.txt b/Documentation/config/worktree.txt
index 048e349482df6c892055720eb53cdcd6c327b6ed..5e35c7d018aecdedca0642b11e45df6d19024d42 100644
--- a/Documentation/config/worktree.txt
+++ b/Documentation/config/worktree.txt
@@ -7,3 +7,13 @@  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::
+	Link worktrees using relative paths (when "true") or absolute
+	paths (when "false"). This is particularly useful for setups
+	where the repository and worktrees may be moved between
+	different locations or environments. Defaults to "false".
++
+Note that setting `worktree.useRelativePaths` to "true" implies enabling the
+`extension.relativeWorktrees` config (see linkgit:git-config[1]),
+thus making it incompatible with older versions of Git.
diff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt
index 70437c815f13852bd2eb862176b8b933e6de0acf..88d2261012166a929b7f167d79720e4d965fd71b 100644
--- a/Documentation/git-worktree.txt
+++ b/Documentation/git-worktree.txt
@@ -216,6 +216,10 @@  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::
+	Overrides the `worktree.useRelativePaths` config option, see
+	linkgit:git-config[1].
+
 --[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..e3b4a71ee0bc13d5e817cf7dcc398e9e2bd975de 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -120,12 +120,14 @@  struct add_opts {
 	int quiet;
 	int checkout;
 	int orphan;
+	int relative_paths;
 	const char *keep_locked;
 };
 
 static int show_only;
 static int verbose;
 static int guess_remote;
+static int use_relative_paths;
 static timestamp_t expire;
 
 static int git_worktree_config(const char *var, const char *value,
@@ -134,6 +136,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 +419,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 +495,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, opts->relative_paths);
 	strbuf_reset(&sb);
 	strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
 	write_file(sb.buf, "../..");
@@ -578,12 +579,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,12 +794,15 @@  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", &opts.relative_paths,
+			 N_("use relative paths for worktrees")),
 		OPT_END()
 	};
 	int ret;
 
 	memset(&opts, 0, sizeof(opts));
 	opts.checkout = 1;
+	opts.relative_paths = use_relative_paths;
 	ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);
 	if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
 		die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach");
diff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh
index cfc4aeb1798c6d023909cec771e5b74e983af5ea..d36d8a4db0e924877787697579544f10f92dc0cf 100755
--- a/t/t2400-worktree-add.sh
+++ b/t/t2400-worktree-add.sh
@@ -1207,4 +1207,58 @@  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_expect_success 'relative worktree sets extension config' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	git -C repo commit --allow-empty -m base &&
+	git -C repo worktree add --relative-paths ./foo &&
+	git -C repo config get core.repositoryformatversion >actual &&
+	echo 1 >expected &&
+	test_cmp expected actual &&
+	git -C repo config get extensions.relativeworktrees >actual &&
+	echo true >expected &&
+	test_cmp expected actual
+'
+
 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/t2408-worktree-relative.sh b/t/t2408-worktree-relative.sh
deleted file mode 100755
index a3136db7e28cb20926ff44211e246ce625a6e51a..0000000000000000000000000000000000000000
--- a/t/t2408-worktree-relative.sh
+++ /dev/null
@@ -1,39 +0,0 @@ 
-#!/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