{"thread":{"id":"62428","subject":"[PATCH v3 0/8] Allow relative worktree linking to be configured by the user","startedAt":"2024-10-31T05:05:27Z","lastAt":"2024-10-31T19:52:50Z","messageCount":11,"participants":["Caleb White","Taylor Blau"],"isPatch":true,"patchVersion":3,"patchTotal":8},"messages":[{"id":"506348","messageId":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":null,"subject":"[PATCH v3 0/8] Allow relative worktree linking to be configured by the user","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:05:23Z","receivedAt":"2024-10-31T05:05:27Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"This series introduces the `--[no-]relative-paths` CLI option for\n`git worktree {add, move, repair}` commands, as well as the\n`worktree.useRelativePaths` configuration setting. When enabled,\nthese options allow worktrees to be linked using relative paths,\nenhancing portability across environments where absolute paths\nmay differ (e.g., containerized setups, shared network drives).\nGit still creates absolute paths by default, but these options allow\nusers to opt-in to relative paths if desired.\n\nUsing the `--relative-paths` option with `worktree {move, repair}`\nwill convert absolute paths to relative ones, while `--no-relative-paths`\ndoes the reverse. For cases where users want consistency in path handling,\nthe config option `worktree.useRelativePaths` provides a persistent setting.\n\nA new extension, `relativeWorktrees`, is added to indicate that at least\none worktree in the repository has been linked with relative paths. This\nextension is automatically set when a worktree is created or repaired\nusing the `--relative-paths` option, or when the\n`worktree.useRelativePaths` config is set to `true`.\n\nThe `relativeWorktrees` extension ensures older Git versions do not\nattempt to automatically prune worktrees with relative paths, as they\nwould not not recognize the paths as being valid.\n\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\nThe base for this patch series is obtained by applying the following\npatch onto 6a11438f43:\n- cw/config-extensions topic (doc: consolidate extensions in git-config\n  documentation, 2024-10-22, <20241021-cleanup-extension-docs-v1-1-ab02cece3132@pm.me>)\n\nLink to original patch series:\nhttps://lore.kernel.org/git/20241007-wt_relative_paths-v3-0-622cf18c45eb@pm.me\n\n---\nChanges in v3:\n- Split patches into smaller edits.\n- Moved tests into the patches with the relevant code changes.\n- Removed global `use_relative_paths` and instead pass parameter to functions.\n- Changed `infer_backlink` return type from `int` to `ssize_t`.\n- Updated `worktree.useRelativePaths` and `--relative-paths` descriptions.\n- Reordered patches\n- Link to v2: https://lore.kernel.org/r/20241028-wt_relative_options-v2-0-33a5021bd7bb@pm.me\nChanges in v2:\n- Fixed a bug where repositories with valid extensions would be downgraded\n  to v0 during reinitialization, causing future operations to fail.\n- Split patch [1/2] into three separate patches.\n- Updated cover letter and commit messages.\n- Updated documentation wording.\n- Link to v1: https://lore.kernel.org/r/20241025-wt_relative_options-v1-0-c3005df76bf9@pm.me\n\n---\nCaleb White (8):\n      setup: correctly reinitialize repository version\n      worktree: add `relativeWorktrees` extension\n      worktree: refactor infer_backlink return\n      worktree: add `write_worktree_linking_files()` function\n      worktree: add relative cli/config options to `add` command\n      worktree: add relative cli/config options to `move` command\n      worktree: add relative cli/config options to `repair` command\n      worktree: refactor `repair_worktree_after_gitdir_move()`\n\n Documentation/config/extensions.txt |   6 ++\n Documentation/config/worktree.txt   |  10 +++\n Documentation/git-worktree.txt      |   7 ++\n builtin/worktree.c                  |  29 ++++---\n repository.c                        |   1 +\n repository.h                        |   1 +\n setup.c                             |  39 +++++++--\n setup.h                             |   1 +\n t/t0001-init.sh                     |  22 ++++-\n t/t2400-worktree-add.sh             |  54 ++++++++++++\n t/t2401-worktree-prune.sh           |   3 +-\n t/t2402-worktree-list.sh            |  22 +++++\n t/t2403-worktree-move.sh            |  22 +++++\n t/t2406-worktree-repair.sh          |  26 ++++++\n t/t2408-worktree-relative.sh        |  39 ---------\n worktree.c                          | 163 ++++++++++++++++++++----------------\n worktree.h                          |  22 ++++-\n 17 files changed, 328 insertions(+), 139 deletions(-)\n---\nbase-commit: 6a11438f43469f3815f2f0fc997bd45792ff04c0\nchange-id: 20241025-wt_relative_options-afa41987bc32\nprerequisite-change-id: 20241020-cleanup-extension-docs-f365868711bf:v1\nprerequisite-patch-id: 60a443b24e92938b9b6f4a016a7bab87e13bf3ea\n\nBest regards,\n-- \nCaleb White <cdwhite3@pm.me>\n\n\n"},{"id":"506349","messageId":"20241031-wt_relative_options-v3-1-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"[PATCH v3 1/8] setup: correctly reinitialize repository version","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:05:27Z","receivedAt":"2024-10-31T05:05:35Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"When reinitializing a repository, Git does not account for extensions\nother than `objectformat` and `refstorage` when determining the\nrepository version. This can lead to a repository being downgraded to\nversion 0 if extensions are set, causing Git future operations to fail.\n\nThis patch teaches Git to check if other extensions are defined in the\nconfig to ensure that the repository version is set correctly.\n\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\n setup.c | 32 +++++++++++++++++++++++---------\n 1 file changed, 23 insertions(+), 9 deletions(-)\n\ndiff --git a/setup.c b/setup.c\nindex 7b648de0279116b381eea46800ad130606926103..1e5c2eacb19eb6b230d7c9954f66fc7ae0b05631 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2204,8 +2204,8 @@ void initialize_repository_version(int hash_algo,\n \t\t\t\t   enum ref_storage_format ref_storage_format,\n \t\t\t\t   int reinit)\n {\n-\tchar repo_version_string[10];\n-\tint repo_version = GIT_REPO_VERSION;\n+\tstruct strbuf repo_version = STRBUF_INIT;\n+\tint target_version = GIT_REPO_VERSION;\n \n \t/*\n \t * Note that we initialize the repository version to 1 when the ref\n@@ -2216,12 +2216,7 @@ void initialize_repository_version(int hash_algo,\n \t */\n \tif (hash_algo != GIT_HASH_SHA1 ||\n \t    ref_storage_format != REF_STORAGE_FORMAT_FILES)\n-\t\trepo_version = GIT_REPO_VERSION_READ;\n-\n-\t/* This forces creation of new config file */\n-\txsnprintf(repo_version_string, sizeof(repo_version_string),\n-\t\t  \"%d\", repo_version);\n-\tgit_config_set(\"core.repositoryformatversion\", repo_version_string);\n+\t\ttarget_version = GIT_REPO_VERSION_READ;\n \n \tif (hash_algo != GIT_HASH_SHA1 && hash_algo != GIT_HASH_UNKNOWN)\n \t\tgit_config_set(\"extensions.objectformat\",\n@@ -2234,6 +2229,25 @@ void initialize_repository_version(int hash_algo,\n \t\t\t       ref_storage_format_to_name(ref_storage_format));\n \telse if (reinit)\n \t\tgit_config_set_gently(\"extensions.refstorage\", NULL);\n+\n+\tif (reinit) {\n+\t\tstruct strbuf config = STRBUF_INIT;\n+\t\tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n+\n+\t\tstrbuf_git_common_path(&config, the_repository, \"config\");\n+\t\tread_repository_format(&repo_fmt, config.buf);\n+\n+\t\tif (repo_fmt.v1_only_extensions.nr)\n+\t\t\ttarget_version = GIT_REPO_VERSION_READ;\n+\n+\t\tstrbuf_release(&config);\n+\t\tclear_repository_format(&repo_fmt);\n+\t}\n+\n+\tstrbuf_addf(&repo_version, \"%d\", target_version);\n+\tgit_config_set(\"core.repositoryformatversion\", repo_version.buf);\n+\n+\tstrbuf_release(&repo_version);\n }\n \n static int is_reinit(void)\n@@ -2333,7 +2347,7 @@ static int create_default_files(const char *template_path,\n \t\tadjust_shared_perm(repo_get_git_dir(the_repository));\n \t}\n \n-\tinitialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, 0);\n+\tinitialize_repository_version(fmt->hash_algo, fmt->ref_storage_format, reinit);\n \n \t/* Check filemode trustability */\n \tpath = git_path_buf(&buf, \"config\");\n\n-- \n2.47.0\n\n\n"},{"id":"506350","messageId":"20241031-wt_relative_options-v3-3-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"[PATCH v3 3/8] worktree: refactor infer_backlink return","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:05:42Z","receivedAt":"2024-10-31T05:05:48Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"The previous round[1] was merged a bit early before reviewer feedback\ncould be applied. This correctly indents a code block and updates the\n`infer_backlink` function to return `-1` on failure and strbuf.len on\nsuccess.\n\n[1]: https://lore.kernel.org/git/20241007-wt_relative_paths-v3-0-622cf18c45eb@pm.me\n\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\n worktree.c | 17 +++++++++--------\n 1 file changed, 9 insertions(+), 8 deletions(-)\n\ndiff --git a/worktree.c b/worktree.c\nindex 77ff484d3ec48c547ee4e3d958dfa28a52c1eaa7..9346d51c438cbd029e9e57591edd8c9f30cc7638 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -111,9 +111,9 @@ struct worktree *get_linked_worktree(const char *id,\n \tstrbuf_strip_suffix(&worktree_path, \"/.git\");\n \n \tif (!is_absolute_path(worktree_path.buf)) {\n-\t    strbuf_strip_suffix(&path, \"gitdir\");\n-\t    strbuf_addbuf(&path, &worktree_path);\n-\t    strbuf_realpath_forgiving(&worktree_path, path.buf, 0);\n+\t\tstrbuf_strip_suffix(&path, \"gitdir\");\n+\t\tstrbuf_addbuf(&path, &worktree_path);\n+\t\tstrbuf_realpath_forgiving(&worktree_path, path.buf, 0);\n \t}\n \n \tCALLOC_ARRAY(worktree, 1);\n@@ -725,12 +725,15 @@ static int is_main_worktree_path(const char *path)\n  * won't know which <repo>/worktrees/<id>/gitdir to repair. However, we may\n  * be able to infer the gitdir by manually reading /path/to/worktree/.git,\n  * extracting the <id>, and checking if <repo>/worktrees/<id> exists.\n+ *\n+ * Returns -1 on failure and strbuf.len on success.\n  */\n-static int infer_backlink(const char *gitfile, struct strbuf *inferred)\n+static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred)\n {\n \tstruct strbuf actual = STRBUF_INIT;\n \tconst char *id;\n \n+\tstrbuf_reset(inferred);\n \tif (strbuf_read_file(&actual, gitfile, 0) < 0)\n \t\tgoto error;\n \tif (!starts_with(actual.buf, \"gitdir:\"))\n@@ -741,18 +744,16 @@ static int infer_backlink(const char *gitfile, struct strbuf *inferred)\n \tid++; /* advance past '/' to point at <id> */\n \tif (!*id)\n \t\tgoto error;\n-\tstrbuf_reset(inferred);\n \tstrbuf_git_common_path(inferred, the_repository, \"worktrees/%s\", id);\n \tif (!is_directory(inferred->buf))\n \t\tgoto error;\n \n \tstrbuf_release(&actual);\n-\treturn 1;\n-\n+\treturn inferred->len;\n error:\n \tstrbuf_release(&actual);\n \tstrbuf_reset(inferred); /* clear invalid path */\n-\treturn 0;\n+\treturn -1;\n }\n \n /*\n\n-- \n2.47.0\n\n\n"},{"id":"506351","messageId":"20241031-wt_relative_options-v3-2-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"[PATCH v3 2/8] worktree: add `relativeWorktrees` extension","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:05:35Z","receivedAt":"2024-10-31T05:05:49Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"A new extension, `relativeWorktrees`, is added to indicate that at least\none worktree in the repository has been linked with relative paths.\nThis ensures older Git versions do not attempt to automatically prune\nworktrees with relative paths, as they would not not recognize the\npaths as being valid.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\n Documentation/config/extensions.txt | 6 ++++++\n repository.c                        | 1 +\n repository.h                        | 1 +\n setup.c                             | 7 +++++++\n setup.h                             | 1 +\n 5 files changed, 16 insertions(+)\n\ndiff --git a/Documentation/config/extensions.txt b/Documentation/config/extensions.txt\nindex 5dc569d1c9c77c15e32441493289f9c9dd5e7f0b..5cb4721a0e0ae1ed64f90492c0dc18b96473cb33 100644\n--- a/Documentation/config/extensions.txt\n+++ b/Documentation/config/extensions.txt\n@@ -63,6 +63,12 @@ Note that this setting should only be set by linkgit:git-init[1] or\n linkgit:git-clone[1]. Trying to change it after initialization will not\n work and will produce hard-to-diagnose issues.\n \n+relativeWorktrees::\n+\tIf enabled, indicates at least one worktree has been linked with\n+\trelative paths. Automatically set if a worktree has been created or\n+\trepaired with either the `--relative-paths` option or with the\n+\t`worktree.useRelativePaths` config set to `true`.\n+\n worktreeConfig::\n \tIf enabled, then worktrees will load config settings from the\n \t`$GIT_DIR/config.worktree` file in addition to the\ndiff --git a/repository.c b/repository.c\nindex f988b8ae68a6a29792e7f2c980a02bd0e388a3b9..1a6a62bbd03a5dc4fdade3eb45ea2696968abc23 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -283,6 +283,7 @@ int repo_init(struct repository *repo,\n \trepo_set_compat_hash_algo(repo, format.compat_hash_algo);\n \trepo_set_ref_storage_format(repo, format.ref_storage_format);\n \trepo->repository_format_worktree_config = format.worktree_config;\n+\trepo->repository_format_relative_worktrees = format.relative_worktrees;\n \n \t/* take ownership of format.partial_clone */\n \trepo->repository_format_partial_clone = format.partial_clone;\ndiff --git a/repository.h b/repository.h\nindex 24a66a496a6ff516ce06d47b7329b3d36eb701ca..c4c92b2ab9c9e3b425dc2974636e33d1f4089c69 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -150,6 +150,7 @@ struct repository {\n \n \t/* Configurations */\n \tint repository_format_worktree_config;\n+\tint repository_format_relative_worktrees;\n \n \t/* Indicate if a repository has a different 'commondir' from 'gitdir' */\n \tunsigned different_commondir:1;\ndiff --git a/setup.c b/setup.c\nindex 1e5c2eacb19eb6b230d7c9954f66fc7ae0b05631..39ff48d9dc5d67b16159c6cca66ff2663bbba6cf 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -683,6 +683,9 @@ static enum extension_result handle_extension(const char *var,\n \t\t\t\t     \"extensions.refstorage\", value);\n \t\tdata->ref_storage_format = format;\n \t\treturn EXTENSION_OK;\n+\t} else if (!strcmp(ext, \"relativeworktrees\")) {\n+\t\tdata->relative_worktrees = git_config_bool(var, value);\n+\t\treturn EXTENSION_OK;\n \t}\n \treturn EXTENSION_UNKNOWN;\n }\n@@ -1854,6 +1857,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t\t\t\t\t\t    repo_fmt.ref_storage_format);\n \t\t\tthe_repository->repository_format_worktree_config =\n \t\t\t\trepo_fmt.worktree_config;\n+\t\t\tthe_repository->repository_format_relative_worktrees =\n+\t\t\t\trepo_fmt.relative_worktrees;\n \t\t\t/* take ownership of repo_fmt.partial_clone */\n \t\t\tthe_repository->repository_format_partial_clone =\n \t\t\t\trepo_fmt.partial_clone;\n@@ -1950,6 +1955,8 @@ void check_repository_format(struct repository_format *fmt)\n \t\t\t\t    fmt->ref_storage_format);\n \tthe_repository->repository_format_worktree_config =\n \t\tfmt->worktree_config;\n+\tthe_repository->repository_format_relative_worktrees =\n+\t\tfmt->relative_worktrees;\n \tthe_repository->repository_format_partial_clone =\n \t\txstrdup_or_null(fmt->partial_clone);\n \tclear_repository_format(&repo_fmt);\ndiff --git a/setup.h b/setup.h\nindex e496ab3e4de580c2d9f95a7ea0eaf90e0d41b070..18dc3b73686ce28fac2fe04282ce95f8bf3e6b74 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -129,6 +129,7 @@ struct repository_format {\n \tint precious_objects;\n \tchar *partial_clone; /* value of extensions.partialclone */\n \tint worktree_config;\n+\tint relative_worktrees;\n \tint is_bare;\n \tint hash_algo;\n \tint compat_hash_algo;\n\n-- \n2.47.0\n\n\n"},{"id":"506352","messageId":"20241031-wt_relative_options-v3-4-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"[PATCH v3 4/8] worktree: add `write_worktree_linking_files()` function","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:05:50Z","receivedAt":"2024-10-31T05:05:55Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"A new helper function, `write_worktree_linking_files()`, centralizes\nthe logic for computing and writing either relative or absolute\npaths, based on the provided configuration. This function accepts\n`strbuf` pointers to both the worktree’s `.git` link and the\nrepository’s `gitdir`, and then writes the appropriate path to each.\nThe `relativeWorktrees` extension is automatically set when a worktree\nis linked with relative paths.\n\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\n worktree.c | 36 ++++++++++++++++++++++++++++++++++++\n worktree.h | 14 ++++++++++++++\n 2 files changed, 50 insertions(+)\n\ndiff --git a/worktree.c b/worktree.c\nindex 9346d51c438cbd029e9e57591edd8c9f30cc7638..ab6f1e036fa019285a1a5152decb9aed5202909f 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -1032,3 +1032,39 @@ int init_worktree_config(struct repository *r)\n \tfree(main_worktree_file);\n \treturn res;\n }\n+\n+void write_worktree_linking_files(struct strbuf dotgit,\n+\t\t\t\t  struct strbuf gitdir,\n+\t\t\t\t  int use_relative_paths)\n+{\n+\tstruct strbuf path = STRBUF_INIT;\n+\tstruct strbuf repo = STRBUF_INIT;\n+\tstruct strbuf tmp = STRBUF_INIT;\n+\n+\tstrbuf_addbuf(&path, &dotgit);\n+\tstrbuf_strip_suffix(&path, \"/.git\");\n+\tstrbuf_realpath(&path, path.buf, 1);\n+\tstrbuf_addbuf(&repo, &gitdir);\n+\tstrbuf_strip_suffix(&repo, \"/gitdir\");\n+\tstrbuf_realpath(&repo, repo.buf, 1);\n+\n+\tif (use_relative_paths && !the_repository->repository_format_relative_worktrees) {\n+\t\tif (upgrade_repository_format(1) < 0)\n+\t\t\tdie(_(\"unable to upgrade repository format to support relative worktrees\"));\n+\t\tif (git_config_set_gently(\"extensions.relativeWorktrees\", \"true\"))\n+\t\t\tdie(_(\"unable to set extensions.relativeWorktrees setting\"));\n+\t\tthe_repository->repository_format_relative_worktrees = 1;\n+\t}\n+\n+\tif (use_relative_paths) {\n+\t\twrite_file(gitdir.buf, \"%s/.git\", relative_path(path.buf, repo.buf, &tmp));\n+\t\twrite_file(dotgit.buf, \"gitdir: %s\", relative_path(repo.buf, path.buf, &tmp));\n+\t} else {\n+\t\twrite_file(gitdir.buf, \"%s/.git\", path.buf);\n+\t\twrite_file(dotgit.buf, \"gitdir: %s\", repo.buf);\n+\t}\n+\n+\tstrbuf_release(&path);\n+\tstrbuf_release(&repo);\n+\tstrbuf_release(&tmp);\n+}\ndiff --git a/worktree.h b/worktree.h\nindex e96118621638667d87c5d7e0452ed10bd1ddf606..4786aa545ca1fa12bb16a55ddf5a59c06503b2c5 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -215,4 +215,18 @@ void strbuf_worktree_ref(const struct worktree *wt,\n  */\n int init_worktree_config(struct repository *r);\n \n+/**\n+ * Write the .git file and gitdir file that links the worktree to the repository.\n+ *\n+ * The `dotgit` parameter is the path to the worktree's .git file, and `gitdir`\n+ * is the path to the repository's `gitdir` file.\n+ *\n+ * Example\n+ *  dotgit: \"/path/to/foo/.git\"\n+ *  gitdir: \"/path/to/repo/worktrees/foo/gitdir\"\n+ */\n+void write_worktree_linking_files(struct strbuf dotgit,\n+\t\t\t\t  struct strbuf gitdir,\n+\t\t\t\t  int use_relative_paths);\n+\n #endif\n\n-- \n2.47.0\n\n\n"},{"id":"506353","messageId":"20241031-wt_relative_options-v3-5-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"[PATCH v3 5/8] worktree: add relative cli/config options to `add` command","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:05:58Z","receivedAt":"2024-10-31T05:06:07Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"This introduces the `--[no-]relative-paths` CLI option and\n`worktree.useRelativePaths` configuration setting to the `worktree add`\ncommand. When enabled these options allow worktrees to be linked using\nrelative paths, enhancing portability across environments where absolute\npaths may differ (e.g., containerized setups, shared network drives).\nGit still creates absolute paths by default, but these options allow\nusers to opt-in to relative paths if desired.\n\nThe t2408 test file is removed and more comprehensive tests are\nwritten for the various worktree operations in their own files.\n\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\n Documentation/config/worktree.txt | 10 ++++++++\n Documentation/git-worktree.txt    |  4 +++\n builtin/worktree.c                | 19 +++++++-------\n t/t2400-worktree-add.sh           | 54 +++++++++++++++++++++++++++++++++++++++\n t/t2401-worktree-prune.sh         |  3 ++-\n t/t2402-worktree-list.sh          | 22 ++++++++++++++++\n t/t2408-worktree-relative.sh      | 39 ----------------------------\n 7 files changed, 102 insertions(+), 49 deletions(-)\n\ndiff --git a/Documentation/config/worktree.txt b/Documentation/config/worktree.txt\nindex 048e349482df6c892055720eb53cdcd6c327b6ed..5e35c7d018aecdedca0642b11e45df6d19024d42 100644\n--- a/Documentation/config/worktree.txt\n+++ b/Documentation/config/worktree.txt\n@@ -7,3 +7,13 @@ worktree.guessRemote::\n \tsuch a branch exists, it is checked out and set as \"upstream\"\n \tfor the new branch.  If no such match can be found, it falls\n \tback to creating a new branch from the current HEAD.\n+\n+worktree.useRelativePaths::\n+\tLink worktrees using relative paths (when \"true\") or absolute\n+\tpaths (when \"false\"). This is particularly useful for setups\n+\twhere the repository and worktrees may be moved between\n+\tdifferent locations or environments. Defaults to \"false\".\n++\n+Note that setting `worktree.useRelativePaths` to \"true\" implies enabling the\n+`extension.relativeWorktrees` config (see linkgit:git-config[1]),\n+thus making it incompatible with older versions of Git.\ndiff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\nindex 70437c815f13852bd2eb862176b8b933e6de0acf..88d2261012166a929b7f167d79720e4d965fd71b 100644\n--- a/Documentation/git-worktree.txt\n+++ b/Documentation/git-worktree.txt\n@@ -216,6 +216,10 @@ To remove a locked worktree, specify `--force` twice.\n This can also be set up as the default behaviour by using the\n `worktree.guessRemote` config option.\n \n+--[no-]relative-paths::\n+\tOverrides the `worktree.useRelativePaths` config option, see\n+\tlinkgit:git-config[1].\n+\n --[no-]track::\n \tWhen creating a new branch, if `<commit-ish>` is a branch,\n \tmark it as \"upstream\" from the new branch.  This is the\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex dae63dedf4cac2621f51f95a39aa456b33acd894..e3b4a71ee0bc13d5e817cf7dcc398e9e2bd975de 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -120,12 +120,14 @@ struct add_opts {\n \tint quiet;\n \tint checkout;\n \tint orphan;\n+\tint relative_paths;\n \tconst char *keep_locked;\n };\n \n static int show_only;\n static int verbose;\n static int guess_remote;\n+static int use_relative_paths;\n static timestamp_t expire;\n \n static int git_worktree_config(const char *var, const char *value,\n@@ -134,6 +136,9 @@ static int git_worktree_config(const char *var, const char *value,\n \tif (!strcmp(var, \"worktree.guessremote\")) {\n \t\tguess_remote = git_config_bool(var, value);\n \t\treturn 0;\n+\t} else if (!strcmp(var, \"worktree.userelativepaths\")) {\n+\t\tuse_relative_paths = git_config_bool(var, value);\n+\t\treturn 0;\n \t}\n \n \treturn git_default_config(var, value, ctx, cb);\n@@ -414,8 +419,7 @@ static int add_worktree(const char *path, const char *refname,\n \t\t\tconst struct add_opts *opts)\n {\n \tstruct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;\n-\tstruct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT;\n-\tstruct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT;\n+\tstruct strbuf sb = STRBUF_INIT;\n \tconst char *name;\n \tstruct strvec child_env = STRVEC_INIT;\n \tunsigned int counter = 0;\n@@ -491,10 +495,7 @@ static int add_worktree(const char *path, const char *refname,\n \n \tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/gitdir\", sb_repo.buf);\n-\tstrbuf_realpath(&sb_path_realpath, path, 1);\n-\tstrbuf_realpath(&sb_repo_realpath, sb_repo.buf, 1);\n-\twrite_file(sb.buf, \"%s/.git\", relative_path(sb_path_realpath.buf, sb_repo_realpath.buf, &sb_tmp));\n-\twrite_file(sb_git.buf, \"gitdir: %s\", relative_path(sb_repo_realpath.buf, sb_path_realpath.buf, &sb_tmp));\n+\twrite_worktree_linking_files(sb_git, sb, opts->relative_paths);\n \tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"%s/commondir\", sb_repo.buf);\n \twrite_file(sb.buf, \"../..\");\n@@ -578,12 +579,9 @@ static int add_worktree(const char *path, const char *refname,\n \n \tstrvec_clear(&child_env);\n \tstrbuf_release(&sb);\n-\tstrbuf_release(&sb_tmp);\n \tstrbuf_release(&symref);\n \tstrbuf_release(&sb_repo);\n-\tstrbuf_release(&sb_repo_realpath);\n \tstrbuf_release(&sb_git);\n-\tstrbuf_release(&sb_path_realpath);\n \tstrbuf_release(&sb_name);\n \tfree_worktree(wt);\n \treturn ret;\n@@ -796,12 +794,15 @@ static int add(int ac, const char **av, const char *prefix)\n \t\t\t     PARSE_OPT_NOARG | PARSE_OPT_OPTARG),\n \t\tOPT_BOOL(0, \"guess-remote\", &guess_remote,\n \t\t\t N_(\"try to match the new branch name with a remote-tracking branch\")),\n+\t\tOPT_BOOL(0, \"relative-paths\", &opts.relative_paths,\n+\t\t\t N_(\"use relative paths for worktrees\")),\n \t\tOPT_END()\n \t};\n \tint ret;\n \n \tmemset(&opts, 0, sizeof(opts));\n \topts.checkout = 1;\n+\topts.relative_paths = use_relative_paths;\n \tac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0);\n \tif (!!opts.detach + !!new_branch + !!new_branch_force > 1)\n \t\tdie(_(\"options '%s', '%s', and '%s' cannot be used together\"), \"-b\", \"-B\", \"--detach\");\ndiff --git a/t/t2400-worktree-add.sh b/t/t2400-worktree-add.sh\nindex cfc4aeb1798c6d023909cec771e5b74e983af5ea..d36d8a4db0e924877787697579544f10f92dc0cf 100755\n--- a/t/t2400-worktree-add.sh\n+++ b/t/t2400-worktree-add.sh\n@@ -1207,4 +1207,58 @@ test_expect_success '\"add\" with initialized submodule, with submodule.recurse se\n \tgit -C project-clone -c submodule.recurse worktree add ../project-5\n '\n \n+test_expect_success 'can create worktrees with relative paths' '\n+\ttest_when_finished \"git worktree remove relative\" &&\n+\tgit config worktree.useRelativePaths false &&\n+\tgit worktree add --relative-paths ./relative &&\n+\tcat relative/.git >actual &&\n+\techo \"gitdir: ../.git/worktrees/relative\" >expect &&\n+\ttest_cmp expect actual &&\n+\tcat .git/worktrees/relative/gitdir >actual &&\n+\techo \"../../../relative/.git\" >expect &&\n+\ttest_cmp expect actual\n+\n+'\n+\n+test_expect_success 'can create worktrees with absolute paths' '\n+\tgit config worktree.useRelativePaths true &&\n+\tgit worktree add ./relative &&\n+\tcat relative/.git >actual &&\n+\techo \"gitdir: ../.git/worktrees/relative\" >expect &&\n+\ttest_cmp expect actual &&\n+\tgit worktree add --no-relative-paths ./absolute &&\n+\tcat absolute/.git >actual &&\n+\techo \"gitdir: $(pwd)/.git/worktrees/absolute\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'move repo without breaking relative internal links' '\n+\ttest_when_finished rm -rf repo moved &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config worktree.useRelativePaths true &&\n+\t\ttest_commit initial &&\n+\t\tgit worktree add wt1 &&\n+\t\tcd .. &&\n+\t\tmv repo moved &&\n+\t\tcd moved/wt1 &&\n+\t\tgit status >out 2>err &&\n+\t\ttest_must_be_empty err\n+\t)\n+'\n+\n+test_expect_success 'relative worktree sets extension config' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\tgit -C repo commit --allow-empty -m base &&\n+\tgit -C repo worktree add --relative-paths ./foo &&\n+\tgit -C repo config get core.repositoryformatversion >actual &&\n+\techo 1 >expected &&\n+\ttest_cmp expected actual &&\n+\tgit -C repo config get extensions.relativeworktrees >actual &&\n+\techo true >expected &&\n+\ttest_cmp expected actual\n+'\n+\n test_done\ndiff --git a/t/t2401-worktree-prune.sh b/t/t2401-worktree-prune.sh\nindex 976d048e3efc74be9cd909ce76d552b3944d2e10..5eb52b9abbf29514dc082c260ebb7a5e8e63aae0 100755\n--- a/t/t2401-worktree-prune.sh\n+++ b/t/t2401-worktree-prune.sh\n@@ -120,11 +120,12 @@ test_expect_success 'prune duplicate (main/linked)' '\n \t! test -d .git/worktrees/wt\n '\n \n-test_expect_success 'not prune proper worktrees when run inside linked worktree' '\n+test_expect_success 'not prune proper worktrees inside linked worktree with relative paths' '\n \ttest_when_finished rm -rf repo wt_ext &&\n \tgit init repo &&\n \t(\n \t    cd repo &&\n+\t    git config worktree.useRelativePaths true &&\n \t    echo content >file &&\n \t    git add file &&\n \t    git commit -m msg &&\ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex 33ea9cb21ba07c9563530b54da06753eaa993fe2..780daa6cd6351f8fa9434619cc212aade8f01420 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -261,6 +261,7 @@ test_expect_success 'broken main worktree still at the top' '\n '\n \n test_expect_success 'linked worktrees are sorted' '\n+\ttest_when_finished \"rm -rf sorted\" &&\n \tmkdir sorted &&\n \tgit init sorted/main &&\n \t(\n@@ -280,6 +281,27 @@ test_expect_success 'linked worktrees are sorted' '\n \ttest_cmp expected sorted/main/actual\n '\n \n+test_expect_success 'linked worktrees with relative paths are shown with absolute paths' '\n+\ttest_when_finished \"rm -rf sorted\" &&\n+\tmkdir sorted &&\n+\tgit init sorted/main &&\n+\t(\n+\t\tcd sorted/main &&\n+\t\ttest_tick &&\n+\t\ttest_commit new &&\n+\t\tgit worktree add --relative-paths ../first &&\n+\t\tgit worktree add ../second &&\n+\t\tgit worktree list --porcelain >out &&\n+\t\tgrep ^worktree out >actual\n+\t) &&\n+\tcat >expected <<-EOF &&\n+\tworktree $(pwd)/sorted/main\n+\tworktree $(pwd)/sorted/first\n+\tworktree $(pwd)/sorted/second\n+\tEOF\n+\ttest_cmp expected sorted/main/actual\n+'\n+\n test_expect_success 'worktree path when called in .git directory' '\n \tgit worktree list >list1 &&\n \tgit -C .git worktree list >list2 &&\ndiff --git a/t/t2408-worktree-relative.sh b/t/t2408-worktree-relative.sh\ndeleted file mode 100755\nindex a3136db7e28cb20926ff44211e246ce625a6e51a..0000000000000000000000000000000000000000\n--- a/t/t2408-worktree-relative.sh\n+++ /dev/null\n@@ -1,39 +0,0 @@\n-#!/bin/sh\n-\n-test_description='test worktrees linked with relative paths'\n-\n-TEST_PASSES_SANITIZE_LEAK=true\n-. ./test-lib.sh\n-\n-test_expect_success 'links worktrees with relative paths' '\n-\ttest_when_finished rm -rf repo &&\n-\tgit init repo &&\n-\t(\n-\t\tcd repo &&\n-\t\ttest_commit initial &&\n-\t\tgit worktree add wt1 &&\n-\t\techo \"../../../wt1/.git\" >expected_gitdir &&\n-\t\tcat .git/worktrees/wt1/gitdir >actual_gitdir &&\n-\t\techo \"gitdir: ../.git/worktrees/wt1\" >expected_git &&\n-\t\tcat wt1/.git >actual_git &&\n-\t\ttest_cmp expected_gitdir actual_gitdir &&\n-\t\ttest_cmp expected_git actual_git\n-\t)\n-'\n-\n-test_expect_success 'move repo without breaking relative internal links' '\n-\ttest_when_finished rm -rf repo moved &&\n-\tgit init repo &&\n-\t(\n-\t\tcd repo &&\n-\t\ttest_commit initial &&\n-\t\tgit worktree add wt1 &&\n-\t\tcd .. &&\n-\t\tmv repo moved &&\n-\t\tcd moved/wt1 &&\n-\t\tgit status >out 2>err &&\n-\t\ttest_must_be_empty err\n-\t)\n-'\n-\n-test_done\n\n-- \n2.47.0\n\n\n"},{"id":"506354","messageId":"20241031-wt_relative_options-v3-6-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"[PATCH v3 6/8] worktree: add relative cli/config options to `move` command","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:06:06Z","receivedAt":"2024-10-31T05:06:12Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"This teaches the `worktree move` command to respect the\n`--[no-]relative-paths` CLI option and `worktree.useRelativePaths`\nconfig setting. If an existing worktree is moved with `--relative-paths`\nthe new path will be relative (and visa-versa).\n\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\n builtin/worktree.c       |  4 +++-\n t/t2403-worktree-move.sh | 22 ++++++++++++++++++++++\n worktree.c               | 23 ++++++++++-------------\n worktree.h               |  3 ++-\n 4 files changed, 37 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex e3b4a71ee0bc13d5e817cf7dcc398e9e2bd975de..302151506981718658db1cd338cd9064688f5c14 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -1190,6 +1190,8 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \t\tOPT__FORCE(&force,\n \t\t\t N_(\"force move even if worktree is dirty or locked\"),\n \t\t\t PARSE_OPT_NOCOMPLETE),\n+\t\tOPT_BOOL(0, \"relative-paths\", &use_relative_paths,\n+\t\t\t N_(\"use relative paths for worktrees\")),\n \t\tOPT_END()\n \t};\n \tstruct worktree **worktrees, *wt;\n@@ -1242,7 +1244,7 @@ static int move_worktree(int ac, const char **av, const char *prefix)\n \tif (rename(wt->path, dst.buf) == -1)\n \t\tdie_errno(_(\"failed to move '%s' to '%s'\"), wt->path, dst.buf);\n \n-\tupdate_worktree_location(wt, dst.buf);\n+\tupdate_worktree_location(wt, dst.buf, use_relative_paths);\n \n \tstrbuf_release(&dst);\n \tfree_worktrees(worktrees);\ndiff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh\nindex 901342ea09b51a8e832f1109fbb737df84283aa2..6ce9ed3f1e6b3f73d2a290e770233eec30221fe5 100755\n--- a/t/t2403-worktree-move.sh\n+++ b/t/t2403-worktree-move.sh\n@@ -247,4 +247,26 @@ test_expect_success 'not remove a repo with initialized submodule' '\n \t)\n '\n \n+test_expect_success 'move worktree with absolute path to relative path' '\n+\tgit config worktree.useRelativePaths false &&\n+\tgit worktree add ./absolute &&\n+\tgit worktree move --relative-paths absolute relative &&\n+\tcat relative/.git >actual &&\n+\techo \"gitdir: ../.git/worktrees/absolute\" >expect &&\n+\ttest_cmp expect actual &&\n+\tgit config worktree.useRelativePaths true &&\n+\tgit worktree move relative relative2 &&\n+\tcat relative2/.git >actual &&\n+\techo \"gitdir: ../.git/worktrees/absolute\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'move worktree with relative path to absolute path' '\n+\tgit config worktree.useRelativePaths true &&\n+\tgit worktree move --no-relative-paths relative2 absolute &&\n+\tcat absolute/.git >actual &&\n+\techo \"gitdir: $(pwd)/.git/worktrees/absolute\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\ndiff --git a/worktree.c b/worktree.c\nindex ab6f1e036fa019285a1a5152decb9aed5202909f..6b640cd9549ecb060236f7eddf1390caa181f1a0 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -376,32 +376,29 @@ int validate_worktree(const struct worktree *wt, struct strbuf *errmsg,\n \treturn ret;\n }\n \n-void update_worktree_location(struct worktree *wt, const char *path_)\n+void update_worktree_location(struct worktree *wt,\n+\t\t\t      const char *path_,\n+\t\t\t      int use_relative_paths)\n {\n \tstruct strbuf path = STRBUF_INIT;\n-\tstruct strbuf repo = STRBUF_INIT;\n-\tstruct strbuf file = STRBUF_INIT;\n-\tstruct strbuf tmp = STRBUF_INIT;\n+\tstruct strbuf dotgit = STRBUF_INIT;\n+\tstruct strbuf gitdir = STRBUF_INIT;\n \n \tif (is_main_worktree(wt))\n \t\tBUG(\"can't relocate main worktree\");\n \n-\tstrbuf_realpath(&repo, git_common_path(\"worktrees/%s\", wt->id), 1);\n+\tstrbuf_realpath(&gitdir, git_common_path(\"worktrees/%s/gitdir\", wt->id), 1);\n \tstrbuf_realpath(&path, path_, 1);\n+\tstrbuf_addf(&dotgit, \"%s/.git\", path.buf);\n \tif (fspathcmp(wt->path, path.buf)) {\n-\t\tstrbuf_addf(&file, \"%s/gitdir\", repo.buf);\n-\t\twrite_file(file.buf, \"%s/.git\", relative_path(path.buf, repo.buf, &tmp));\n-\t\tstrbuf_reset(&file);\n-\t\tstrbuf_addf(&file, \"%s/.git\", path.buf);\n-\t\twrite_file(file.buf, \"gitdir: %s\", relative_path(repo.buf, path.buf, &tmp));\n+\t\twrite_worktree_linking_files(dotgit, gitdir, use_relative_paths);\n \n \t\tfree(wt->path);\n \t\twt->path = strbuf_detach(&path, NULL);\n \t}\n \tstrbuf_release(&path);\n-\tstrbuf_release(&repo);\n-\tstrbuf_release(&file);\n-\tstrbuf_release(&tmp);\n+\tstrbuf_release(&dotgit);\n+\tstrbuf_release(&gitdir);\n }\n \n int is_worktree_being_rebased(const struct worktree *wt,\ndiff --git a/worktree.h b/worktree.h\nindex 4786aa545ca1fa12bb16a55ddf5a59c06503b2c5..6dd5dfc9b71870320c96aae35a7a983b5b9ba9a7 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -118,7 +118,8 @@ int validate_worktree(const struct worktree *wt,\n  * Update worktrees/xxx/gitdir with the new path.\n  */\n void update_worktree_location(struct worktree *wt,\n-\t\t\t      const char *path_);\n+\t\t\t      const char *path_,\n+\t\t\t      int use_relative_paths);\n \n typedef void (* worktree_repair_fn)(int iserr, const char *path,\n \t\t\t\t    const char *msg, void *cb_data);\n\n-- \n2.47.0\n\n\n"},{"id":"506355","messageId":"20241031-wt_relative_options-v3-7-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"[PATCH v3 7/8] worktree: add relative cli/config options to `repair` command","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:06:14Z","receivedAt":"2024-10-31T05:06:21Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"This teaches the `worktree repair` command to respect the\n`--[no-]relative-paths` CLI option and `worktree.useRelativePaths`\nconfig setting. If an existing worktree with an absolute path is repaired\nwith `--relative-paths`, the links will be replaced with relative paths,\neven if the original path was correct. This allows a user to covert\nexisting worktrees between absolute/relative as desired.\n\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\n Documentation/git-worktree.txt |  3 +++\n builtin/worktree.c             |  6 +++--\n t/t2406-worktree-repair.sh     | 26 +++++++++++++++++++\n worktree.c                     | 58 +++++++++++++++++++++---------------------\n worktree.h                     |  5 ++--\n 5 files changed, 65 insertions(+), 33 deletions(-)\n\ndiff --git a/Documentation/git-worktree.txt b/Documentation/git-worktree.txt\nindex 88d2261012166a929b7f167d79720e4d965fd71b..1d86323def16b15fbc85365aaea18ce6a2ef433a 100644\n--- a/Documentation/git-worktree.txt\n+++ b/Documentation/git-worktree.txt\n@@ -219,6 +219,9 @@ This can also be set up as the default behaviour by using the\n --[no-]relative-paths::\n \tOverrides the `worktree.useRelativePaths` config option, see\n \tlinkgit:git-config[1].\n++\n+With `repair`, the linking files will be updated if there's an absolute/relative\n+mismatch, even if the links are correct.\n \n --[no-]track::\n \tWhen creating a new branch, if `<commit-ish>` is a branch,\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 302151506981718658db1cd338cd9064688f5c14..fde9ff4dc9a734c655e95ccd62774282950cbba6 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -1385,6 +1385,8 @@ static int repair(int ac, const char **av, const char *prefix)\n \tconst char **p;\n \tconst char *self[] = { \".\", NULL };\n \tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"relative-paths\", &use_relative_paths,\n+\t\t\t N_(\"use relative paths for worktrees\")),\n \t\tOPT_END()\n \t};\n \tint rc = 0;\n@@ -1392,8 +1394,8 @@ static int repair(int ac, const char **av, const char *prefix)\n \tac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0);\n \tp = ac > 0 ? av : self;\n \tfor (; *p; p++)\n-\t\trepair_worktree_at_path(*p, report_repair, &rc);\n-\trepair_worktrees(report_repair, &rc);\n+\t\trepair_worktree_at_path(*p, report_repair, &rc, use_relative_paths);\n+\trepair_worktrees(report_repair, &rc, use_relative_paths);\n \treturn rc;\n }\n \ndiff --git a/t/t2406-worktree-repair.sh b/t/t2406-worktree-repair.sh\nindex 7686e60f6ad186519b275f11a5e14064c905b207..84451e903b2ef3c645c0311faf055c846588baf6 100755\n--- a/t/t2406-worktree-repair.sh\n+++ b/t/t2406-worktree-repair.sh\n@@ -216,4 +216,30 @@ test_expect_success 'repair copied main and linked worktrees' '\n \ttest_cmp dup/linked.expect dup/linked/.git\n '\n \n+test_expect_success 'repair absolute worktree to use relative paths' '\n+\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+\ttest_create_repo main &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --detach ../side &&\n+\techo \"../../../../sidemoved/.git\" >expect-gitdir &&\n+\techo \"gitdir: ../main/.git/worktrees/side\" >expect-gitfile &&\n+\tmv side sidemoved &&\n+\tgit -C main worktree repair --relative-paths ../sidemoved &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile sidemoved/.git\n+'\n+\n+test_expect_success 'repair relative worktree to use absolute paths' '\n+\ttest_when_finished \"rm -rf main side sidemoved\" &&\n+\ttest_create_repo main &&\n+\ttest_commit -C main init &&\n+\tgit -C main worktree add --relative-paths --detach ../side &&\n+\techo \"$(pwd)/sidemoved/.git\" >expect-gitdir &&\n+\techo \"gitdir: $(pwd)/main/.git/worktrees/side\" >expect-gitfile &&\n+\tmv side sidemoved &&\n+\tgit -C main worktree repair ../sidemoved &&\n+\ttest_cmp expect-gitdir main/.git/worktrees/side/gitdir &&\n+\ttest_cmp expect-gitfile sidemoved/.git\n+'\n+\n test_done\ndiff --git a/worktree.c b/worktree.c\nindex 6b640cd9549ecb060236f7eddf1390caa181f1a0..2cb994ac462debf966ac51b5a4f33c30cfebd4ef 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -574,12 +574,14 @@ int other_head_refs(each_ref_fn fn, void *cb_data)\n  * pointing at <repo>/worktrees/<id>.\n  */\n static void repair_gitfile(struct worktree *wt,\n-\t\t\t   worktree_repair_fn fn, void *cb_data)\n+\t\t\t   worktree_repair_fn fn,\n+\t\t\t   void *cb_data,\n+\t\t\t   int use_relative_paths)\n {\n \tstruct strbuf dotgit = STRBUF_INIT;\n+\tstruct strbuf gitdir = STRBUF_INIT;\n \tstruct strbuf repo = STRBUF_INIT;\n \tstruct strbuf backlink = STRBUF_INIT;\n-\tstruct strbuf tmp = STRBUF_INIT;\n \tchar *dotgit_contents = NULL;\n \tconst char *repair = NULL;\n \tint err;\n@@ -595,6 +597,7 @@ static void repair_gitfile(struct worktree *wt,\n \n \tstrbuf_realpath(&repo, git_common_path(\"worktrees/%s\", wt->id), 1);\n \tstrbuf_addf(&dotgit, \"%s/.git\", wt->path);\n+\tstrbuf_addf(&gitdir, \"%s/gitdir\", repo.buf);\n \tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n \n \tif (dotgit_contents) {\n@@ -612,18 +615,20 @@ static void repair_gitfile(struct worktree *wt,\n \t\trepair = _(\".git file broken\");\n \telse if (fspathcmp(backlink.buf, repo.buf))\n \t\trepair = _(\".git file incorrect\");\n+\telse if (use_relative_paths == is_absolute_path(dotgit_contents))\n+\t\trepair = _(\".git file absolute/relative path mismatch\");\n \n \tif (repair) {\n \t\tfn(0, wt->path, repair, cb_data);\n-\t\twrite_file(dotgit.buf, \"gitdir: %s\", relative_path(repo.buf, wt->path, &tmp));\n+\t\twrite_worktree_linking_files(dotgit, gitdir, use_relative_paths);\n \t}\n \n done:\n \tfree(dotgit_contents);\n \tstrbuf_release(&repo);\n \tstrbuf_release(&dotgit);\n+\tstrbuf_release(&gitdir);\n \tstrbuf_release(&backlink);\n-\tstrbuf_release(&tmp);\n }\n \n static void repair_noop(int iserr UNUSED,\n@@ -634,7 +639,7 @@ static void repair_noop(int iserr UNUSED,\n \t/* nothing */\n }\n \n-void repair_worktrees(worktree_repair_fn fn, void *cb_data)\n+void repair_worktrees(worktree_repair_fn fn, void *cb_data, int use_relative_paths)\n {\n \tstruct worktree **worktrees = get_worktrees_internal(1);\n \tstruct worktree **wt = worktrees + 1; /* +1 skips main worktree */\n@@ -642,7 +647,7 @@ void repair_worktrees(worktree_repair_fn fn, void *cb_data)\n \tif (!fn)\n \t\tfn = repair_noop;\n \tfor (; *wt; wt++)\n-\t\trepair_gitfile(*wt, fn, cb_data);\n+\t\trepair_gitfile(*wt, fn, cb_data, use_relative_paths);\n \tfree_worktrees(worktrees);\n }\n \n@@ -758,16 +763,15 @@ static ssize_t infer_backlink(const char *gitfile, struct strbuf *inferred)\n  * the worktree's path.\n  */\n void repair_worktree_at_path(const char *path,\n-\t\t\t     worktree_repair_fn fn, void *cb_data)\n+\t\t\t     worktree_repair_fn fn,\n+\t\t\t     void *cb_data,\n+\t\t\t     int use_relative_paths)\n {\n \tstruct strbuf dotgit = STRBUF_INIT;\n-\tstruct strbuf realdotgit = STRBUF_INIT;\n \tstruct strbuf backlink = STRBUF_INIT;\n \tstruct strbuf inferred_backlink = STRBUF_INIT;\n \tstruct strbuf gitdir = STRBUF_INIT;\n \tstruct strbuf olddotgit = STRBUF_INIT;\n-\tstruct strbuf realolddotgit = STRBUF_INIT;\n-\tstruct strbuf tmp = STRBUF_INIT;\n \tchar *dotgit_contents = NULL;\n \tconst char *repair = NULL;\n \tint err;\n@@ -779,25 +783,25 @@ void repair_worktree_at_path(const char *path,\n \t\tgoto done;\n \n \tstrbuf_addf(&dotgit, \"%s/.git\", path);\n-\tif (!strbuf_realpath(&realdotgit, dotgit.buf, 0)) {\n+\tif (!strbuf_realpath(&dotgit, dotgit.buf, 0)) {\n \t\tfn(1, path, _(\"not a valid path\"), cb_data);\n \t\tgoto done;\n \t}\n \n-\tinfer_backlink(realdotgit.buf, &inferred_backlink);\n+\tinfer_backlink(dotgit.buf, &inferred_backlink);\n \tstrbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);\n-\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err));\n+\tdotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));\n \tif (dotgit_contents) {\n \t\tif (is_absolute_path(dotgit_contents)) {\n \t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n \t\t} else {\n-\t\t\tstrbuf_addbuf(&backlink, &realdotgit);\n+\t\t\tstrbuf_addbuf(&backlink, &dotgit);\n \t\t\tstrbuf_strip_suffix(&backlink, \".git\");\n \t\t\tstrbuf_addstr(&backlink, dotgit_contents);\n \t\t\tstrbuf_realpath_forgiving(&backlink, backlink.buf, 0);\n \t\t}\n \t} else if (err == READ_GITFILE_ERR_NOT_A_FILE) {\n-\t\tfn(1, realdotgit.buf, _(\"unable to locate repository; .git is not a file\"), cb_data);\n+\t\tfn(1, dotgit.buf, _(\"unable to locate repository; .git is not a file\"), cb_data);\n \t\tgoto done;\n \t} else if (err == READ_GITFILE_ERR_NOT_A_REPO) {\n \t\tif (inferred_backlink.len) {\n@@ -810,11 +814,11 @@ void repair_worktree_at_path(const char *path,\n \t\t\t */\n \t\t\tstrbuf_swap(&backlink, &inferred_backlink);\n \t\t} else {\n-\t\t\tfn(1, realdotgit.buf, _(\"unable to locate repository; .git file does not reference a repository\"), cb_data);\n+\t\t\tfn(1, dotgit.buf, _(\"unable to locate repository; .git file does not reference a repository\"), cb_data);\n \t\t\tgoto done;\n \t\t}\n \t} else {\n-\t\tfn(1, realdotgit.buf, _(\"unable to locate repository; .git file broken\"), cb_data);\n+\t\tfn(1, dotgit.buf, _(\"unable to locate repository; .git file broken\"), cb_data);\n \t\tgoto done;\n \t}\n \n@@ -836,39 +840,35 @@ void repair_worktree_at_path(const char *path,\n \t * in the \"copy\" repository. In this case, point the \"copy\" worktree's\n \t * .git file at the \"copy\" repository.\n \t */\n-\tif (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf)) {\n+\tif (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf))\n \t\tstrbuf_swap(&backlink, &inferred_backlink);\n-\t}\n \n \tstrbuf_addf(&gitdir, \"%s/gitdir\", backlink.buf);\n \tif (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0)\n \t\trepair = _(\"gitdir unreadable\");\n+\telse if (use_relative_paths == is_absolute_path(olddotgit.buf))\n+\t\trepair = _(\"gitdir absolute/relative path mismatch\");\n \telse {\n \t\tstrbuf_rtrim(&olddotgit);\n-\t\tif (is_absolute_path(olddotgit.buf)) {\n-\t\t\tstrbuf_addbuf(&realolddotgit, &olddotgit);\n-\t\t} else {\n-\t\t\tstrbuf_addf(&realolddotgit, \"%s/%s\", backlink.buf, olddotgit.buf);\n-\t\t\tstrbuf_realpath_forgiving(&realolddotgit, realolddotgit.buf, 0);\n+\t\tif (!is_absolute_path(olddotgit.buf)) {\n+\t\t\tstrbuf_insertf(&olddotgit, 0, \"%s/\", backlink.buf);\n+\t\t\tstrbuf_realpath_forgiving(&olddotgit, olddotgit.buf, 0);\n \t\t}\n-\t\tif (fspathcmp(realolddotgit.buf, realdotgit.buf))\n+\t\tif (fspathcmp(olddotgit.buf, dotgit.buf))\n \t\t\trepair = _(\"gitdir incorrect\");\n \t}\n \n \tif (repair) {\n \t\tfn(0, gitdir.buf, repair, cb_data);\n-\t\twrite_file(gitdir.buf, \"%s\", relative_path(realdotgit.buf, backlink.buf, &tmp));\n+\t\twrite_worktree_linking_files(dotgit, gitdir, use_relative_paths);\n \t}\n done:\n \tfree(dotgit_contents);\n \tstrbuf_release(&olddotgit);\n-\tstrbuf_release(&realolddotgit);\n \tstrbuf_release(&backlink);\n \tstrbuf_release(&inferred_backlink);\n \tstrbuf_release(&gitdir);\n-\tstrbuf_release(&realdotgit);\n \tstrbuf_release(&dotgit);\n-\tstrbuf_release(&tmp);\n }\n \n int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)\ndiff --git a/worktree.h b/worktree.h\nindex 6dd5dfc9b71870320c96aae35a7a983b5b9ba9a7..f158a2ee429cc7af971bf4a6665c058a9e15614a 100644\n--- a/worktree.h\n+++ b/worktree.h\n@@ -130,7 +130,7 @@ typedef void (* worktree_repair_fn)(int iserr, const char *path,\n  * function, if non-NULL, is called with the path of the worktree and a\n  * description of the repair or error, along with the callback user-data.\n  */\n-void repair_worktrees(worktree_repair_fn, void *cb_data);\n+void repair_worktrees(worktree_repair_fn, void *cb_data, int use_relative_paths);\n \n /*\n  * Repair the linked worktrees after the gitdir has been moved.\n@@ -152,7 +152,8 @@ void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path\n  * worktree and a description of the repair or error, along with the callback\n  * user-data.\n  */\n-void repair_worktree_at_path(const char *, worktree_repair_fn, void *cb_data);\n+void repair_worktree_at_path(const char *, worktree_repair_fn,\n+\t\t\t     void *cb_data, int use_relative_paths);\n \n /*\n  * Free up the memory for a worktree.\n\n-- \n2.47.0\n\n\n"},{"id":"506356","messageId":"20241031-wt_relative_options-v3-8-3e44ccdf64e6@pm.me","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"[PATCH v3 8/8] worktree: refactor `repair_worktree_after_gitdir_move()`","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T05:06:21Z","receivedAt":"2024-10-31T05:06:33Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"This refactors `repair_worktree_after_gitdir_move()` to use the new\n`write_worktree_linking_files` function. It also preserves the\nrelativity of the linking files; e.g., if an existing worktree used\nabsolute paths then the repaired paths will be absolute (and visa-versa).\n\nThis also adds a test case for reinitializing a repository that has\nrelative worktrees.\n\nSigned-off-by: Caleb White <cdwhite3@pm.me>\n---\n t/t0001-init.sh | 22 ++++++++++++++++++----\n worktree.c      | 29 ++++++++---------------------\n 2 files changed, 26 insertions(+), 25 deletions(-)\n\ndiff --git a/t/t0001-init.sh b/t/t0001-init.sh\nindex 0178aa62a41f1606f2382a4a10ab593ccf11e0e8..264951592a711f6879555b785905e799db6c5dd4 100755\n--- a/t/t0001-init.sh\n+++ b/t/t0001-init.sh\n@@ -434,6 +434,12 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '\n sep_git_dir_worktree ()  {\n \ttest_when_finished \"rm -rf mainwt linkwt seprepo\" &&\n \tgit init mainwt &&\n+\tif test \"relative\" = $2\n+\tthen\n+\t\tgit -C mainwt config worktree.useRelativePaths true\n+\telse\n+\t\tgit -C mainwt config worktree.useRelativePaths false\n+\tfi\n \ttest_commit -C mainwt gumby &&\n \tgit -C mainwt worktree add --detach ../linkwt &&\n \tgit -C \"$1\" init --separate-git-dir ../seprepo &&\n@@ -442,12 +448,20 @@ sep_git_dir_worktree ()  {\n \ttest_cmp expect actual\n }\n \n-test_expect_success 're-init to move gitdir with linked worktrees' '\n-\tsep_git_dir_worktree mainwt\n+test_expect_success 're-init to move gitdir with linked worktrees (absolute)' '\n+\tsep_git_dir_worktree mainwt absolute\n+'\n+\n+test_expect_success 're-init to move gitdir within linked worktree (absolute)' '\n+\tsep_git_dir_worktree linkwt absolute\n+'\n+\n+test_expect_success 're-init to move gitdir with linked worktrees (relative)' '\n+\tsep_git_dir_worktree mainwt relative\n '\n \n-test_expect_success 're-init to move gitdir within linked worktree' '\n-\tsep_git_dir_worktree linkwt\n+test_expect_success 're-init to move gitdir within linked worktree (relative)' '\n+\tsep_git_dir_worktree linkwt relative\n '\n \n test_expect_success MINGW '.git hidden' '\ndiff --git a/worktree.c b/worktree.c\nindex 2cb994ac462debf966ac51b5a4f33c30cfebd4ef..2c2e9fd8fdc22d4fe3705227d72b8dbd293038b2 100644\n--- a/worktree.c\n+++ b/worktree.c\n@@ -653,45 +653,32 @@ void repair_worktrees(worktree_repair_fn fn, void *cb_data, int use_relative_pat\n \n void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path)\n {\n-\tstruct strbuf path = STRBUF_INIT;\n-\tstruct strbuf repo = STRBUF_INIT;\n \tstruct strbuf gitdir = STRBUF_INIT;\n \tstruct strbuf dotgit = STRBUF_INIT;\n-\tstruct strbuf olddotgit = STRBUF_INIT;\n-\tstruct strbuf tmp = STRBUF_INIT;\n+\tint is_relative_path;\n \n \tif (is_main_worktree(wt))\n \t\tgoto done;\n \n-\tstrbuf_realpath(&repo, git_common_path(\"worktrees/%s\", wt->id), 1);\n-\tstrbuf_addf(&gitdir, \"%s/gitdir\", repo.buf);\n+\tstrbuf_realpath(&gitdir, git_common_path(\"worktrees/%s/gitdir\", wt->id), 1);\n \n-\tif (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0)\n+\tif (strbuf_read_file(&dotgit, gitdir.buf, 0) < 0)\n \t\tgoto done;\n \n-\tstrbuf_rtrim(&olddotgit);\n-\tif (is_absolute_path(olddotgit.buf)) {\n-\t\tstrbuf_addbuf(&dotgit, &olddotgit);\n-\t} else {\n-\t\tstrbuf_addf(&dotgit, \"%s/worktrees/%s/%s\", old_path, wt->id, olddotgit.buf);\n+\tstrbuf_rtrim(&dotgit);\n+\tis_relative_path = ! is_absolute_path(dotgit.buf);\n+\tif (is_relative_path) {\n+\t\tstrbuf_insertf(&dotgit, 0, \"%s/worktrees/%s/\", old_path, wt->id);\n \t\tstrbuf_realpath_forgiving(&dotgit, dotgit.buf, 0);\n \t}\n \n \tif (!file_exists(dotgit.buf))\n \t\tgoto done;\n \n-\tstrbuf_addbuf(&path, &dotgit);\n-\tstrbuf_strip_suffix(&path, \"/.git\");\n-\n-\twrite_file(dotgit.buf, \"gitdir: %s\", relative_path(repo.buf, path.buf, &tmp));\n-\twrite_file(gitdir.buf, \"%s\", relative_path(dotgit.buf, repo.buf, &tmp));\n+\twrite_worktree_linking_files(dotgit, gitdir, is_relative_path);\n done:\n-\tstrbuf_release(&path);\n-\tstrbuf_release(&repo);\n \tstrbuf_release(&gitdir);\n \tstrbuf_release(&dotgit);\n-\tstrbuf_release(&olddotgit);\n-\tstrbuf_release(&tmp);\n }\n \n void repair_worktrees_after_gitdir_move(const char *old_path)\n\n-- \n2.47.0\n\n\n"},{"id":"506401","messageId":"ZyPYiffYRDK/4LdG@nand.local","threadId":"62428","inReplyTo":"20241031-wt_relative_options-v3-0-3e44ccdf64e6@pm.me","subject":"Re: [PATCH v3 0/8] Allow relative worktree linking to be configured by the user","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2024-10-31T19:20:41Z","receivedAt":"2024-10-31T19:20:44Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Thu, Oct 31, 2024 at 05:05:23AM +0000, Caleb White wrote:\n> Caleb White (8):\n>       setup: correctly reinitialize repository version\n>       worktree: add `relativeWorktrees` extension\n>       worktree: refactor infer_backlink return\n>       worktree: add `write_worktree_linking_files()` function\n>       worktree: add relative cli/config options to `add` command\n>       worktree: add relative cli/config options to `move` command\n>       worktree: add relative cli/config options to `repair` command\n>       worktree: refactor `repair_worktree_after_gitdir_move()`\n\nThis version of the topic, as well as the previous round, appear to\nbreak CI:\n\n    https://github.com/ttaylorr/git/actions/runs/11602969587\n\nAn easy reproduction locally is to run t/t5504-fetch-receive-strict.sh\nwhen built with 'make SANITIZE=leak' and run with\n'GIT_TEST_DEFAULT_REF_FORMAT=reftable'.\n\nI'm going to kick this topic out of jch for the time being as it breaks\nCI, but please do send another round once you have fixed the issue. In\nthe future, please verify that your series does not break CI before\nsending it to the list. Thanks.\n\nThanks,\nTaylor\n"},{"id":"506402","messageId":"D5A95I1T5FZ6.60QJMN704514@pm.me","threadId":"62428","inReplyTo":"ZyPYiffYRDK/4LdG@nand.local","subject":"Re: [PATCH v3 0/8] Allow relative worktree linking to be configured by the user","fromName":"Caleb White","fromEmail":"cdwhite3@pm.me","sentAt":"2024-10-31T19:52:46Z","receivedAt":"2024-10-31T19:52:50Z","isPatch":true,"sender":{"key":"cdwhite3@pm.me","avatar":"https://avatars.githubusercontent.com/u/4176520?v=4"},"body":"On Thu Oct 31, 2024 at 2:20 PM CDT, Taylor Blau wrote:\n> This version of the topic, as well as the previous round, appear to\n> break CI:\n>\n>     https://github.com/ttaylorr/git/actions/runs/11602969587\n>\n> An easy reproduction locally is to run t/t5504-fetch-receive-strict.sh\n> when built with 'make SANITIZE=leak' and run with\n> 'GIT_TEST_DEFAULT_REF_FORMAT=reftable'.\n>\n> I'm going to kick this topic out of jch for the time being as it breaks\n> CI, but please do send another round once you have fixed the issue. In\n> the future, please verify that your series does not break CI before\n> sending it to the list. Thanks.\n\nMy apologies, I executed\n\n    make -j12\n    cd t\n    prove -j12 t[0-9]*.sh\n\n(as recommended by t/readme) and everything passed. I did not know I\nneeded to run the tests with other environment variables set.\n\nBest,\n\n"}]}