{"thread":{"id":"63967","subject":"[PATCH 1/9] submodule--helper: use submodule_name_to_gitdir in add_submodule","startedAt":"2025-08-16T21:37:34Z","lastAt":"2026-01-13T06:12:26Z","messageCount":219,"participants":["Adrian Ratiu","Ben Knoble","Junio C Hamano","Josh Steadmon","Jeff King","Phillip Wood","Patrick Steinhardt","SZEDER Gábor","Kristoffer Haugsbakk","Aaron Schrab"],"isPatch":true,"patchVersion":1,"patchTotal":9},"messages":[{"id":"524296","messageId":"20250816213642.3517822-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 1/9] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:34Z","receivedAt":"2025-08-16T21:37:34Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded name-based path leading to test failures, so\nconvert it to the common helper function introduced by commit ce125d431a\n(\"submodule: extract path to submodule gitdir func\") and used in other\nlocations accross the source tree.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 11 ++++++-----\n 1 file changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 07a1935cbe..7243429c6f 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3213,10 +3213,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3225,8 +3226,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3244,7 +3245,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524297","messageId":"20250816213642.3517822-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":null,"subject":"[PATCH 0/9] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:33Z","receivedAt":"2025-08-16T21:37:36Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello,\n\nThis is a continuation of work done back in 2018 [1], so a big thank you to\neveryone who participated in the initial thread, especially Brandon on whose\ncode this is partially based upon. Hope you are still around and doing well. :)\n\nIt's mostly a rewrite from scratch addressig open feedback. I decided to\niterate upen Brandon's url-encoding design instead of pursuing alternatives\nlike a custom encoding, name hashing or round-trip encoding/decoding using\nan in-memory git mapping (we'd still have to encode/hash the paths to avoid\ncolflicts so IIUC this last one is more complicated for little gain).\n\nI tried to organize and explain the commits in a logical way which is also\neasy to review, keeping the encoding parts, new tests, code moving around\nand path update churn as clearly separated as possible.\n\nThis is based on master and I've merged and succesfully run all tests in\nboth the next and seen branches.\n\nP.S. I plan to give a short talk at the mini-summit in 2 weesks based on this\nseries and some other patches I wish to propose on the ML, so if any of you\nare attending and wish to connect in person, see you there!\n\nLink: https://lore.kernel.org/git/20180807230637.247200-1-bmwill@google.com/ [1]\n\nAdrian Ratiu (9):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  submodule: create new gitdirs under submodules path\n  submodule: add gitdir path config override\n  t: submodules: add basic mixed gitdir path tests\n  strbuf: bring back is_rfc3986_unreserved\n  submodule: encode gitdir paths to avoid conflicts\n  submodule: remove validate_submodule_git_dir()\n  t: move nested gitdir tests to proper location\n  t: add gitdir encoding tests\n\n Documentation/fetch-options.adoc           |   2 +-\n Documentation/git-fetch.adoc               |   2 +-\n Documentation/git-submodule.adoc           |   2 +-\n Documentation/gitsubmodules.adoc           |   8 +-\n builtin/credential-store.c                 |   6 -\n builtin/submodule--helper.c                |  49 +++--\n setup.c                                    |   2 +-\n strbuf.c                                   |   6 +\n strbuf.h                                   |   2 +\n submodule.c                                | 158 +++++++---------\n submodule.h                                |   5 -\n t/lib-submodule-update.sh                  |  50 ++---\n t/lib-verify-submodule-gitdir-path.sh      |  15 ++\n t/meson.build                              |   1 +\n t/t0035-safe-bare-repository.sh            |   4 +-\n t/t1600-index.sh                           |   4 +-\n t/t2405-worktree-submodule.sh              |   8 +-\n t/t2501-cwd-empty.sh                       |   2 +-\n t/t3600-rm.sh                              |   8 +-\n t/t5526-fetch-submodules.sh                |   2 +-\n t/t5619-clone-local-ambiguous-transport.sh |   4 +-\n t/t6120-describe.sh                        |   4 +-\n t/t7001-mv.sh                              |   4 +-\n t/t7400-submodule-basic.sh                 |  33 +++-\n t/t7406-submodule-update.sh                |  14 +-\n t/t7407-submodule-foreach.sh               |   6 +-\n t/t7408-submodule-reference.sh             |  22 +--\n t/t7412-submodule-absorbgitdirs.sh         |  22 +--\n t/t7423-submodule-symlinks.sh              |   8 +-\n t/t7425-submodule-mixed-gitdir-paths.sh    | 207 +++++++++++++++++++++\n t/t7450-bad-git-dotfiles.sh                |  73 +-------\n t/t7527-builtin-fsmonitor.sh               |   4 +-\n 32 files changed, 451 insertions(+), 286 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-mixed-gitdir-paths.sh\n\n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524299","messageId":"20250816213642.3517822-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 2/9] submodule: create new gitdirs under submodules path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:35Z","receivedAt":"2025-08-16T21:37:39Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This is in preparation for encoding the submodule names to avoid conflicts\nlike submodules named foo and foo/bar together with case-insensitive file-\nsystem handling and other corner cases like reserved filenames on Windows.\n\nBackward compatibility is kept with plain-name modules already existing at\npaths like .git/modules/<name>, however a clear separation between legacy\n(plain) and new (encoded) namespaces is desirable, to avoid situations like\nan existing plain-name module containing the encoding escape character/\n\nThus we split the new-style (encoded) gitdir name paths to .git/submodules,\nwhile legacy-style paths remain under .git/modules.\n\nThis is just a default directory change with the accompanying test updates,\nin preparation for the actual encoding additions in future commits.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/fetch-options.adoc           |  2 +-\n Documentation/git-fetch.adoc               |  2 +-\n Documentation/git-submodule.adoc           |  2 +-\n Documentation/gitsubmodules.adoc           |  8 ++--\n setup.c                                    |  2 +-\n submodule.c                                | 28 +++++++++---\n t/lib-submodule-update.sh                  | 50 +++++++++++-----------\n t/t0035-safe-bare-repository.sh            |  4 +-\n t/t1600-index.sh                           |  4 +-\n t/t2405-worktree-submodule.sh              |  8 ++--\n t/t2501-cwd-empty.sh                       |  2 +-\n t/t3600-rm.sh                              |  8 ++--\n t/t5526-fetch-submodules.sh                |  2 +-\n t/t5619-clone-local-ambiguous-transport.sh |  4 +-\n t/t6120-describe.sh                        |  4 +-\n t/t7001-mv.sh                              |  4 +-\n t/t7400-submodule-basic.sh                 | 18 ++++----\n t/t7406-submodule-update.sh                | 10 ++---\n t/t7407-submodule-foreach.sh               |  6 +--\n t/t7408-submodule-reference.sh             | 22 +++++-----\n t/t7412-submodule-absorbgitdirs.sh         | 22 +++++-----\n t/t7423-submodule-symlinks.sh              |  8 ++--\n t/t7450-bad-git-dotfiles.sh                | 32 +++++++-------\n t/t7527-builtin-fsmonitor.sh               |  4 +-\n 24 files changed, 136 insertions(+), 120 deletions(-)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex b01372e4b3..f8d3f65009 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -210,7 +210,7 @@ ifndef::git-pull[]\n \tsubmodule that has commits that are referenced by a newly fetched\n \tsuperproject commit but are missing in the local submodule clone. A\n \tchanged submodule can be fetched as long as it is present locally e.g.\n-\tin `$GIT_DIR/modules/` (see linkgit:gitsubmodules[7]); if the upstream\n+\tin `$GIT_DIR/submodules/` (see linkgit:gitsubmodules[7]); if the upstream\n \tadds a new submodule, that submodule cannot be fetched until it is\n \tcloned e.g. by `git submodule update`.\n +\ndiff --git a/Documentation/git-fetch.adoc b/Documentation/git-fetch.adoc\nindex 16f5d9d69a..2923a29bef 100644\n--- a/Documentation/git-fetch.adoc\n+++ b/Documentation/git-fetch.adoc\n@@ -304,7 +304,7 @@ include::config/fetch.adoc[]\n BUGS\n ----\n Using --recurse-submodules can only fetch new commits in submodules that are\n-present locally e.g. in `$GIT_DIR/modules/`. If the upstream adds a new\n+present locally e.g. in `$GIT_DIR/submodules/`. If the upstream adds a new\n submodule, that submodule cannot be fetched until it is cloned e.g. by `git\n submodule update`. This is expected to be fixed in a future Git version.\n \ndiff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc\nindex 503c84a200..9389862208 100644\n--- a/Documentation/git-submodule.adoc\n+++ b/Documentation/git-submodule.adoc\n@@ -266,7 +266,7 @@ registered submodules, and sync any nested submodules within.\n absorbgitdirs::\n \tIf a git directory of a submodule is inside the submodule,\n \tmove the git directory of the submodule into its superproject's\n-\t`$GIT_DIR/modules` path and then connect the git directory and\n+\t`$GIT_DIR/submodules` path and then connect the git directory and\n \tits working directory by setting the `core.worktree` and adding\n \ta .git file pointing to the git directory embedded in the\n \tsuperprojects git directory.\ndiff --git a/Documentation/gitsubmodules.adoc b/Documentation/gitsubmodules.adoc\nindex f7b5a25a0c..061e24f316 100644\n--- a/Documentation/gitsubmodules.adoc\n+++ b/Documentation/gitsubmodules.adoc\n@@ -21,12 +21,12 @@ The submodule has its own history; the repository it is embedded\n in is called a superproject.\n \n On the filesystem, a submodule usually (but not always - see FORMS below)\n-consists of (i) a Git directory located under the `$GIT_DIR/modules/`\n+consists of (i) a Git directory located under the `$GIT_DIR/submodules/`\n directory of its superproject, (ii) a working directory inside the\n superproject's working directory, and a `.git` file at the root of\n the submodule's working directory pointing to (i).\n \n-Assuming the submodule has a Git directory at `$GIT_DIR/modules/foo/`\n+Assuming the submodule has a Git directory at `$GIT_DIR/submodules/foo/`\n and a working directory at `path/to/bar/`, the superproject tracks the\n submodule via a `gitlink` entry in the tree at `path/to/bar` and an entry\n in its `.gitmodules` file (see linkgit:gitmodules[5]) of the form\n@@ -137,7 +137,7 @@ using older versions of Git.\n It is possible to construct these old form repositories manually.\n +\n When deinitialized or deleted (see below), the submodule's Git\n-directory is automatically moved to `$GIT_DIR/modules/<name>/`\n+directory is automatically moved to `$GIT_DIR/submodules/<name>/`\n of the superproject.\n \n  * Deinitialized submodule: A `gitlink`, and a `.gitmodules` entry,\n@@ -162,7 +162,7 @@ possible to checkout past commits without requiring fetching\n from another repository.\n +\n To completely remove a submodule, manually delete\n-`$GIT_DIR/modules/<name>/`.\n+`$GIT_DIR/submodules/<name>/`.\n \n ACTIVE SUBMODULES\n -----------------\ndiff --git a/setup.c b/setup.c\nindex 98ddbf377f..d054dafa6a 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1416,7 +1416,7 @@ static int is_implicit_bare_repo(const char *path)\n \t * we are inside $GIT_DIR of a worktree of a non-embedded\n \t * submodule, whose superproject is not a bare repository.\n \t */\n-\tif (strstr(path, \"/.git/modules/\"))\n+\tif (strstr(path, \"/.git/modules/\") || strstr(path, \"/.git/submodules/\"))\n \t\treturn 1;\n \n \treturn 0;\ndiff --git a/submodule.c b/submodule.c\nindex fff3c75570..dbf2244e60 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1278,22 +1278,29 @@ void check_for_new_submodule_commits(struct object_id *oid)\n \n /*\n  * Returns 1 if there is at least one submodule gitdir in\n- * $GIT_DIR/modules and 0 otherwise. This follows\n+ * $GIT_DIR/(sub)modules and 0 otherwise. This follows\n  * submodule_name_to_gitdir(), which looks for submodules in\n- * $GIT_DIR/modules, not $GIT_COMMON_DIR.\n+ * $GIT_DIR/(sub)modules, not $GIT_COMMON_DIR.\n  *\n- * A submodule can be moved to $GIT_DIR/modules manually by running \"git\n- * submodule absorbgitdirs\", or it may be initialized there by \"git\n- * submodule update\".\n+ * A submodule can be moved to $GIT_DIR/(sub)modules manually by running\n+ * \"git submodule absorbgitdirs\", or it may be initialized there by\n+ * \"git submodule update\".\n  */\n static int repo_has_absorbed_submodules(struct repository *r)\n {\n \tint ret;\n \tstruct strbuf buf = STRBUF_INIT;\n \n+\t/* check legacy path */\n \trepo_git_path_append(r, &buf, \"modules/\");\n \tret = file_exists(buf.buf) && !is_empty_dir(buf.buf);\n+\tstrbuf_reset(&buf);\n+\n+\t/* new (encoded name) path */\n+\trepo_git_path_append(r, &buf, \"submodules/\");\n+\tret |= file_exists(buf.buf) && !is_empty_dir(buf.buf);\n \tstrbuf_release(&buf);\n+\n \treturn ret;\n }\n \n@@ -2273,7 +2280,7 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \t *\n \t * Example: having a submodule named `hippo` and another one named\n \t * `hippo/hooks` would result in the git directories\n-\t * `.git/modules/hippo/` and `.git/modules/hippo/hooks/`, respectively,\n+\t * `.git/submodules/hippo/` and `.git/submodules/hippo/hooks/`, respectively,\n \t * but the latter directory is already designated to contain the hooks\n \t * of the former.\n \t */\n@@ -2604,6 +2611,15 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t * administrators can explicitly set. Nothing has been decided,\n \t * so for now, just append the name at the end of the path.\n \t */\n+\n+\t/* Legacy behavior: allow existing paths under modules/<name>. */\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n+\tif (!access(buf->buf, F_OK))\n+\t\treturn;\n+\n+\t/* New style (encoded) paths go under submodules/<encoded>. */\n+\tstrbuf_reset(buf);\n+\trepo_git_path_append(r, buf, \"submodules/\");\n+\tstrbuf_addstr(buf, submodule_name);\n }\ndiff --git a/t/lib-submodule-update.sh b/t/lib-submodule-update.sh\nindex 36f767cb74..b6b2be1df5 100644\n--- a/t/lib-submodule-update.sh\n+++ b/t/lib-submodule-update.sh\n@@ -161,7 +161,7 @@ replace_gitfile_with_git_dir () {\n }\n \n # Test that the .git directory in the submodule is unchanged (except for the\n-# core.worktree setting, which appears only in $GIT_DIR/modules/$1/config).\n+# core.worktree setting, which appears only in $GIT_DIR/submodules/$1/config).\n # Call this function before test_submodule_content as the latter might\n # write the index file leading to false positive index differences.\n #\n@@ -170,23 +170,23 @@ replace_gitfile_with_git_dir () {\n test_git_directory_is_unchanged () {\n \t# does core.worktree point at the right place?\n \techo \"../../../$1\" >expect &&\n-\tgit -C \".git/modules/$1\" config core.worktree >actual &&\n+\tgit -C \".git/submodules/$1\" config core.worktree >actual &&\n \ttest_cmp expect actual &&\n \t# remove it temporarily before comparing, as\n \t# \"$1/.git/config\" lacks it...\n-\tgit -C \".git/modules/$1\" config --unset core.worktree &&\n-\tdiff -r \".git/modules/$1\" \"$1/.git\" &&\n+\tgit -C \".git/submodules/$1\" config --unset core.worktree &&\n+\tdiff -r \".git/submodules/$1\" \"$1/.git\" &&\n \t# ... and then restore.\n-\tgit -C \".git/modules/$1\" config core.worktree \"../../../$1\"\n+\tgit -C \".git/submodules/$1\" config core.worktree \"../../../$1\"\n }\n \n test_git_directory_exists () {\n-\ttest -e \".git/modules/$1\" &&\n+\ttest -e \".git/submodules/$1\" &&\n \tif test -f sub1/.git\n \tthen\n \t\t# does core.worktree point at the right place?\n \t\techo \"../../../$1\" >expect &&\n-\t\tgit -C \".git/modules/$1\" config core.worktree >actual &&\n+\t\tgit -C \".git/submodules/$1\" config core.worktree >actual &&\n \t\ttest_cmp expect actual\n \tfi\n }\n@@ -225,22 +225,22 @@ reset_work_tree_to () {\n reset_work_tree_to_interested () {\n \treset_work_tree_to $1 &&\n \t# make the submodule git dirs available\n-\tif ! test -d submodule_update/.git/modules/sub1\n+\tif ! test -d submodule_update/.git/submodules/sub1\n \tthen\n-\t\tmkdir -p submodule_update/.git/modules &&\n-\t\tcp -r submodule_update_repo/.git/modules/sub1 submodule_update/.git/modules/sub1\n-\t\tGIT_WORK_TREE=. git -C submodule_update/.git/modules/sub1 config --unset core.worktree\n+\t\tmkdir -p submodule_update/.git/submodules &&\n+\t\tcp -r submodule_update_repo/.git/submodules/sub1 submodule_update/.git/submodules/sub1\n+\t\tGIT_WORK_TREE=. git -C submodule_update/.git/submodules/sub1 config --unset core.worktree\n \tfi &&\n-\tif ! test -d submodule_update/.git/modules/sub1/modules/sub2\n+\tif ! test -d submodule_update/.git/submodules/sub1/submodules/sub2\n \tthen\n-\t\tmkdir -p submodule_update/.git/modules/sub1/modules &&\n-\t\tcp -r submodule_update_repo/.git/modules/sub1/modules/sub2 submodule_update/.git/modules/sub1/modules/sub2\n+\t\tmkdir -p submodule_update/.git/submodules/sub1/submodules &&\n+\t\tcp -r submodule_update_repo/.git/submodules/sub1/submodules/sub2 submodule_update/.git/submodules/sub1/submodules/sub2\n \t\t# core.worktree is unset for sub2 as it is not checked out\n \tfi &&\n \t# indicate we are interested in the submodule:\n \tgit -C submodule_update config submodule.sub1.url \"bogus\" &&\n \t# sub1 might not be checked out, so use the git dir\n-\tgit -C submodule_update/.git/modules/sub1 config submodule.sub2.url \"bogus\"\n+\tgit -C submodule_update/.git/submodules/sub1 config submodule.sub2.url \"bogus\"\n }\n \n # Test that the superproject contains the content according to commit \"$1\"\n@@ -742,7 +742,7 @@ test_submodule_recursing_with_args_common () {\n \t\t\t$command remove_sub1 &&\n \t\t\ttest_superproject_content origin/remove_sub1 &&\n \t\t\t! test -e sub1 &&\n-\t\t\ttest_must_fail git config -f .git/modules/sub1/config core.worktree\n+\t\t\ttest_must_fail git config -f .git/submodules/sub1/config core.worktree\n \t\t)\n \t'\n \t# ... absorbing a .git directory along the way.\n@@ -753,7 +753,7 @@ test_submodule_recursing_with_args_common () {\n \t\t\tcd submodule_update &&\n \t\t\tgit branch -t remove_sub1 origin/remove_sub1 &&\n \t\t\treplace_gitfile_with_git_dir sub1 &&\n-\t\t\trm -rf .git/modules &&\n+\t\t\trm -rf .git/submodules &&\n \t\t\t$command remove_sub1 &&\n \t\t\ttest_superproject_content origin/remove_sub1 &&\n \t\t\t! test -e sub1 &&\n@@ -803,8 +803,8 @@ test_submodule_recursing_with_args_common () {\n \t\t\t$command no_submodule &&\n \t\t\ttest_superproject_content origin/no_submodule &&\n \t\t\ttest_path_is_missing sub1 &&\n-\t\t\ttest_must_fail git config -f .git/modules/sub1/config core.worktree &&\n-\t\t\ttest_must_fail git config -f .git/modules/sub1/modules/sub2/config core.worktree\n+\t\t\ttest_must_fail git config -f .git/submodules/sub1/config core.worktree &&\n+\t\t\ttest_must_fail git config -f .git/submodules/sub1/submodules/sub2/config core.worktree\n \t\t)\n \t'\n \n@@ -937,7 +937,7 @@ test_submodule_switch_recursing_with_args () {\n \t\t\tcd submodule_update &&\n \t\t\tgit branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&\n \t\t\treplace_gitfile_with_git_dir sub1 &&\n-\t\t\trm -rf .git/modules &&\n+\t\t\trm -rf .git/submodules &&\n \t\t\t$command replace_sub1_with_directory &&\n \t\t\ttest_superproject_content origin/replace_sub1_with_directory &&\n \t\t\ttest_git_directory_exists sub1\n@@ -946,15 +946,15 @@ test_submodule_switch_recursing_with_args () {\n \n \t# ... and ignored files are ignored\n \ttest_expect_success \"$command: replace submodule with a file works ignores ignored files in submodule\" '\n-\t\ttest_when_finished \"rm submodule_update/.git/modules/sub1/info/exclude\" &&\n+\t\ttest_when_finished \"rm submodule_update/.git/submodules/sub1/info/exclude\" &&\n \t\tprolog &&\n \t\treset_work_tree_to_interested add_sub1 &&\n \t\t(\n \t\t\tcd submodule_update &&\n-\t\t\trm -rf .git/modules/sub1/info &&\n+\t\t\trm -rf .git/submodules/sub1/info &&\n \t\t\tgit branch -t replace_sub1_with_file origin/replace_sub1_with_file &&\n-\t\t\tmkdir .git/modules/sub1/info &&\n-\t\t\techo ignored >.git/modules/sub1/info/exclude &&\n+\t\t\tmkdir .git/submodules/sub1/info &&\n+\t\t\techo ignored >.git/submodules/sub1/info/exclude &&\n \t\t\t: >sub1/ignored &&\n \t\t\t$command replace_sub1_with_file &&\n \t\t\ttest_superproject_content origin/replace_sub1_with_file &&\n@@ -1034,7 +1034,7 @@ test_submodule_forced_switch_recursing_with_args () {\n \t\t\tcd submodule_update &&\n \t\t\tgit branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&\n \t\t\treplace_gitfile_with_git_dir sub1 &&\n-\t\t\trm -rf .git/modules/sub1 &&\n+\t\t\trm -rf .git/submodules/sub1 &&\n \t\t\t$command replace_sub1_with_directory &&\n \t\t\ttest_superproject_content origin/replace_sub1_with_directory &&\n \t\t\ttest_git_directory_exists sub1\ndiff --git a/t/t0035-safe-bare-repository.sh b/t/t0035-safe-bare-repository.sh\nindex ae7ef092ab..a480ddf8d6 100755\n--- a/t/t0035-safe-bare-repository.sh\n+++ b/t/t0035-safe-bare-repository.sh\n@@ -41,7 +41,7 @@ test_expect_success 'setup an embedded bare repo, secondary worktree and submodu\n \t\t\tsubmodule add --name subn -- ./bare-repo subd\n \t) &&\n \ttest_path_is_dir outer-repo/.git/worktrees/outer-secondary &&\n-\ttest_path_is_dir outer-repo/.git/modules/subn\n+\ttest_path_is_dir outer-repo/.git/submodules/subn\n '\n \n test_expect_success 'safe.bareRepository unset' '\n@@ -100,7 +100,7 @@ test_expect_success 'no trace in $GIT_DIR of secondary worktree' '\n '\n \n test_expect_success 'no trace in $GIT_DIR of a submodule' '\n-\texpect_accepted_implicit -C outer-repo/.git/modules/subn\n+\texpect_accepted_implicit -C outer-repo/.git/submodules/subn\n '\n \n test_done\ndiff --git a/t/t1600-index.sh b/t/t1600-index.sh\nindex 03239e9faa..0e5e8efb20 100755\n--- a/t/t1600-index.sh\n+++ b/t/t1600-index.sh\n@@ -87,10 +87,10 @@ test_expect_success 'index.skipHash config option' '\n \tgit -c protocol.file.allow=always submodule add ./ sub &&\n \tgit config index.skipHash false &&\n \tgit -C sub config index.skipHash true &&\n-\trm -f .git/modules/sub/index &&\n+\trm -f .git/submodules/sub/index &&\n \t>sub/file &&\n \tgit -C sub add a &&\n-\ttest_trailing_hash .git/modules/sub/index >hash &&\n+\ttest_trailing_hash .git/submodules/sub/index >hash &&\n \ttest_cmp expect hash &&\n \tgit -C sub fsck\n '\ndiff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\nindex 11018f37c7..c18c2efca5 100755\n--- a/t/t2405-worktree-submodule.sh\n+++ b/t/t2405-worktree-submodule.sh\n@@ -62,7 +62,7 @@ test_expect_success 'submodule is checked out after manually adding submodule wo\n test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules in a linked worktree' '\n \tgit -C main worktree add \"$base_path/checkout-recurse\" --detach  &&\n \tgit -C checkout-recurse submodule update --init &&\n-\techo \"gitdir: ../../main/.git/worktrees/checkout-recurse/modules/sub\" >expect-gitfile &&\n+\techo \"gitdir: ../../main/.git/worktrees/checkout-recurse/submodules/sub\" >expect-gitfile &&\n \tcat checkout-recurse/sub/.git >actual-gitfile &&\n \ttest_cmp expect-gitfile actual-gitfile &&\n \tgit -C main/sub rev-parse HEAD >expect-head-main &&\n@@ -73,7 +73,7 @@ test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules\n \ttest_cmp expect-head-main actual-head-main\n '\n \n-test_expect_success 'core.worktree is removed in $GIT_DIR/modules/<name>/config, not in $GIT_COMMON_DIR/modules/<name>/config' '\n+test_expect_success 'core.worktree is removed in $GIT_DIR/submodules/<name>/config, not in $GIT_COMMON_DIR/submodules/<name>/config' '\n \techo \"../../../sub\" >expect-main &&\n \tgit -C main/sub config --get core.worktree >actual-main &&\n \ttest_cmp expect-main actual-main &&\n@@ -81,14 +81,14 @@ test_expect_success 'core.worktree is removed in $GIT_DIR/modules/<name>/config,\n \tgit -C checkout-recurse/sub config --get core.worktree >actual-linked &&\n \ttest_cmp expect-linked actual-linked &&\n \tgit -C checkout-recurse checkout --recurse-submodules first &&\n-\ttest_expect_code 1 git -C main/.git/worktrees/checkout-recurse/modules/sub config --get core.worktree >linked-config &&\n+\ttest_expect_code 1 git -C main/.git/worktrees/checkout-recurse/submodules/sub config --get core.worktree >linked-config &&\n \ttest_must_be_empty linked-config &&\n \tgit -C main/sub config --get core.worktree >actual-main &&\n \ttest_cmp expect-main actual-main\n '\n \n test_expect_success 'unsetting core.worktree does not prevent running commands directly against the submodule repository' '\n-\tgit -C main/.git/worktrees/checkout-recurse/modules/sub log\n+\tgit -C main/.git/worktrees/checkout-recurse/submodules/sub log\n '\n \n test_done\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex be9140bbaa..bb8751433f 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -239,7 +239,7 @@ test_submodule_removal () {\n \ttest \"$path_status\" = dir && test_status=test_must_fail\n \n \ttest_when_finished \"git reset --hard HEAD~1\" &&\n-\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n+\ttest_when_finished \"rm -rf .git/submodules/my_submodule\" &&\n \n \tgit checkout foo/bar/baz &&\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 1f16e6b522..5b8ed57538 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -582,7 +582,7 @@ test_expect_success 'rm of a conflicted populated submodule with a .git director\n \t(\n \t\tcd submod &&\n \t\trm .git &&\n-\t\tcp -R ../.git/modules/sub .git &&\n+\t\tcp -R ../.git/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \ttest_must_fail git merge conflict2 &&\n@@ -617,9 +617,9 @@ test_expect_success 'rm of a populated submodule with a .git directory migrates\n \t(\n \t\tcd submod &&\n \t\trm .git &&\n-\t\tcp -R ../.git/modules/sub .git &&\n+\t\tcp -R ../.git/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree &&\n-\t\trm -r ../.git/modules/sub\n+\t\trm -r ../.git/submodules/sub\n \t) &&\n \tgit rm submod 2>output.err &&\n \ttest_path_is_missing submod &&\n@@ -709,7 +709,7 @@ test_expect_success \"rm absorbs submodule's nested .git directory\" '\n \t(\n \t\tcd submod/subsubmod &&\n \t\trm .git &&\n-\t\tmv ../../.git/modules/sub/modules/sub .git &&\n+\t\tmv ../../.git/submodules/sub/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \tgit rm submod 2>output.err &&\ndiff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh\nindex 5e566205ba..b7385bc088 100755\n--- a/t/t5526-fetch-submodules.sh\n+++ b/t/t5526-fetch-submodules.sh\n@@ -1143,7 +1143,7 @@ test_expect_success 'fetch --recurse-submodules updates name-conflicted, unpopul\n \thead1=$(git -C same-name-1/submodule rev-parse HEAD) &&\n \thead2=$(git -C same-name-2/submodule rev-parse HEAD) &&\n \t(\n-\t\tcd same-name-downstream/.git/modules/submodule &&\n+\t\tcd same-name-downstream/.git/submodules/submodule &&\n \t\t# The submodule has core.worktree pointing to the \"git\n \t\t# rm\"-ed directory, overwrite the invalid value. See\n \t\t# comment in get_fetch_task_from_changed() for more\ndiff --git a/t/t5619-clone-local-ambiguous-transport.sh b/t/t5619-clone-local-ambiguous-transport.sh\nindex cce62bf78d..cf2d5e7bfb 100755\n--- a/t/t5619-clone-local-ambiguous-transport.sh\n+++ b/t/t5619-clone-local-ambiguous-transport.sh\n@@ -38,7 +38,7 @@ test_expect_success 'setup' '\n \t\tln -s \"$(cd .. && pwd)/sensitive\" repo/objects &&\n \n \t\tmkdir -p \"$HTTPD_URL/dumb\" &&\n-\t\tln -s \"../../../.git/modules/sub/../../../repo/\" \"$URI\" &&\n+\t\tln -s \"../../../.git/submodules/sub/../../../repo/\" \"$URI\" &&\n \n \t\tgit add . &&\n \t\tgit commit -m \"initial commit\"\n@@ -57,7 +57,7 @@ test_expect_success 'ambiguous transport does not lead to arbitrary file-inclusi\n \tgit clone malicious clone &&\n \ttest_must_fail git -C clone submodule update --init 2>err &&\n \n-\ttest_path_is_missing clone/.git/modules/sub/objects/secret &&\n+\ttest_path_is_missing clone/.git/submodules/sub/objects/secret &&\n \t# We would actually expect \"transport .file. not allowed\" here,\n \t# but due to quirks of the URL detection in Git, we mis-parse\n \t# the absolute path as a bogus URL and die before that step.\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 256ccaefb7..56460ae8b5 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -357,7 +357,7 @@ test_expect_success 'setup and absorb a submodule' '\n '\n \n test_expect_success 'describe chokes on severely broken submodules' '\n-\tmv .git/modules/sub1/ .git/modules/sub_moved &&\n+\tmv .git/submodules/sub1/ .git/submodules/sub_moved &&\n \ttest_must_fail git describe --dirty\n '\n \n@@ -371,7 +371,7 @@ test_expect_success 'describe with --work-tree ignoring a broken submodule' '\n \t\tcd \"$TEST_DIRECTORY\" &&\n \t\tgit --git-dir \"$TRASH_DIRECTORY/.git\" --work-tree \"$TRASH_DIRECTORY\" describe --broken >\"$TRASH_DIRECTORY/out\"\n \t) &&\n-\ttest_when_finished \"mv .git/modules/sub_moved .git/modules/sub1\" &&\n+\ttest_when_finished \"mv .git/submodules/sub_moved .git/submodules/sub1\" &&\n \tgrep broken out\n '\n \ndiff --git a/t/t7001-mv.sh b/t/t7001-mv.sh\nindex 920479e925..89b06ae3c1 100755\n--- a/t/t7001-mv.sh\n+++ b/t/t7001-mv.sh\n@@ -360,7 +360,7 @@ test_expect_success 'git mv moves a submodule with a .git directory and no .gitm\n \t(\n \t\tcd sub &&\n \t\trm -f .git &&\n-\t\tcp -R -P -p ../.git/modules/sub .git &&\n+\t\tcp -R -P -p ../.git/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \tmkdir mod &&\n@@ -380,7 +380,7 @@ test_expect_success 'git mv moves a submodule with a .git directory and .gitmodu\n \t(\n \t\tcd sub &&\n \t\trm -f .git &&\n-\t\tcp -R -P -p ../.git/modules/sub .git &&\n+\t\tcp -R -P -p ../.git/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \tmkdir mod &&\ndiff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\nindex fd3e7e355e..178c386212 100755\n--- a/t/t7400-submodule-basic.sh\n+++ b/t/t7400-submodule-basic.sh\n@@ -163,7 +163,7 @@ test_expect_success 'submodule add' '\n \t\tcd addtest &&\n \t\tgit submodule add -q \"$submodurl\" submod >actual &&\n \t\ttest_must_be_empty actual &&\n-\t\techo \"gitdir: ../.git/modules/submod\" >expect &&\n+\t\techo \"gitdir: ../.git/submodules/submod\" >expect &&\n \t\ttest_cmp expect submod/.git &&\n \t\t(\n \t\t\tcd submod &&\n@@ -976,21 +976,21 @@ test_expect_success 'submodule add --name allows to replace a submodule with ano\n \t\t\techo \"$submodurl/repo\" >expect &&\n \t\t\tgit config remote.origin.url >actual &&\n \t\t\ttest_cmp expect actual &&\n-\t\t\techo \"gitdir: ../.git/modules/repo\" >expect &&\n+\t\t\techo \"gitdir: ../.git/submodules/repo\" >expect &&\n \t\t\ttest_cmp expect .git\n \t\t) &&\n \t\trm -rf repo &&\n \t\tgit rm repo &&\n \t\tgit submodule add -q --name repo_new \"$submodurl/bare.git\" repo >actual &&\n \t\ttest_must_be_empty actual &&\n-\t\techo \"gitdir: ../.git/modules/submod\" >expect &&\n+\t\techo \"gitdir: ../.git/submodules/submod\" >expect &&\n \t\ttest_cmp expect submod/.git &&\n \t\t(\n \t\t\tcd repo &&\n \t\t\techo \"$submodurl/bare.git\" >expect &&\n \t\t\tgit config remote.origin.url >actual &&\n \t\t\ttest_cmp expect actual &&\n-\t\t\techo \"gitdir: ../.git/modules/repo_new\" >expect &&\n+\t\t\techo \"gitdir: ../.git/submodules/repo_new\" >expect &&\n \t\t\ttest_cmp expect .git\n \t\t) &&\n \t\techo \"repo\" >expect &&\n@@ -1045,8 +1045,8 @@ test_expect_success 'recursive relative submodules stay relative' '\n \t(\n \t\tcd clone2 &&\n \t\tgit submodule update --init --recursive &&\n-\t\techo \"gitdir: ../.git/modules/sub3\" >./sub3/.git_expect &&\n-\t\techo \"gitdir: ../../../.git/modules/sub3/modules/dirdir/subsub\" >./sub3/dirdir/subsub/.git_expect\n+\t\techo \"gitdir: ../.git/submodules/sub3\" >./sub3/.git_expect &&\n+\t\techo \"gitdir: ../../../.git/submodules/sub3/submodules/dirdir/subsub\" >./sub3/dirdir/subsub/.git_expect\n \t) &&\n \ttest_cmp clone2/sub3/.git_expect clone2/sub3/.git &&\n \ttest_cmp clone2/sub3/dirdir/subsub/.git_expect clone2/sub3/dirdir/subsub/.git\n@@ -1108,8 +1108,8 @@ test_expect_success 'submodule deinit should remove the whole submodule section\n '\n \n test_expect_success 'submodule deinit should unset core.worktree' '\n-\ttest_path_is_file .git/modules/example/config &&\n-\ttest_must_fail git config -f .git/modules/example/config core.worktree\n+\ttest_path_is_file .git/submodules/example/config &&\n+\ttest_must_fail git config -f .git/submodules/example/config core.worktree\n '\n \n test_expect_success 'submodule deinit from subdirectory' '\n@@ -1231,7 +1231,7 @@ test_expect_success 'submodule deinit absorbs .git directory if .git is a direct\n \t(\n \t\tcd init &&\n \t\trm .git &&\n-\t\tmv ../.git/modules/example .git &&\n+\t\tmv ../.git/submodules/example .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \tgit submodule deinit init &&\ndiff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh\nindex 3adab12091..f0c4da1ffa 100755\n--- a/t/t7406-submodule-update.sh\n+++ b/t/t7406-submodule-update.sh\n@@ -864,7 +864,7 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir' '\n \t (cd deeper/submodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/modules/deeper/submodule &&\n+\t (cd .git/submodules/deeper/submodule &&\n \t  git log > ../../../../actual\n \t ) &&\n \t test_cmp expected actual\n@@ -882,7 +882,7 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir' '\n \t (cd deeper/submodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/modules/deeper/submodule &&\n+\t (cd .git/submodules/deeper/submodule &&\n \t  git log > ../../../../actual\n \t ) &&\n \t test_cmp expected actual\n@@ -899,7 +899,7 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir recur\n \t  git commit -m \"added subsubmodule\" &&\n \t  git push origin :\n \t ) &&\n-\t (cd .git/modules/deeper/submodule/modules/subsubmodule &&\n+\t (cd .git/submodules/deeper/submodule/submodules/subsubmodule &&\n \t  git log > ../../../../../actual\n \t ) &&\n \t git add deeper/submodule &&\n@@ -949,7 +949,7 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir re\n \t (cd submodule/subsubmodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/modules/submodule/modules/subsubmodule &&\n+\t (cd .git/submodules/submodule/submodules/subsubmodule &&\n \t  git log > ../../../../../actual\n \t ) &&\n \t test_cmp expected actual\n@@ -1298,7 +1298,7 @@ test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \\\n \tgit init captain &&\n \t(\n \t\tcd captain &&\n-\t\tgit submodule add --name x/y \"$hook_repo_path\" A/modules/x &&\n+\t\tgit submodule add --name x/y \"$hook_repo_path\" A/submodules/x &&\n \t\ttest_tick &&\n \t\tgit commit -m add-submodule &&\n \ndiff --git a/t/t7407-submodule-foreach.sh b/t/t7407-submodule-foreach.sh\nindex 77b6d0040e..75ba826968 100755\n--- a/t/t7407-submodule-foreach.sh\n+++ b/t/t7407-submodule-foreach.sh\n@@ -368,9 +368,9 @@ test_expect_success 'test \"update --recursive\" with a flag with spaces' '\n \t\tgit rev-parse --resolve-git-dir nested1/.git &&\n \t\tgit rev-parse --resolve-git-dir nested1/nested2/.git &&\n \t\tgit rev-parse --resolve-git-dir nested1/nested2/nested3/.git &&\n-\t\ttest -f .git/modules/nested1/objects/info/alternates &&\n-\t\ttest -f .git/modules/nested1/modules/nested2/objects/info/alternates &&\n-\t\ttest -f .git/modules/nested1/modules/nested2/modules/nested3/objects/info/alternates\n+\t\ttest -f .git/submodules/nested1/objects/info/alternates &&\n+\t\ttest -f .git/submodules/nested1/submodules/nested2/objects/info/alternates &&\n+\t\ttest -f .git/submodules/nested1/submodules/nested2/submodules/nested3/objects/info/alternates\n \t)\n '\n \ndiff --git a/t/t7408-submodule-reference.sh b/t/t7408-submodule-reference.sh\nindex f860e7bbf4..25f4aec57e 100755\n--- a/t/t7408-submodule-reference.sh\n+++ b/t/t7408-submodule-reference.sh\n@@ -61,7 +61,7 @@ test_expect_success 'submodule add --reference uses alternates' '\n \t\tgit commit -m B-super-added &&\n \t\tgit repack -ad\n \t) &&\n-\ttest_alternate_is_used super/.git/modules/sub/objects/info/alternates super/sub\n+\ttest_alternate_is_used super/.git/submodules/sub/objects/info/alternates super/sub\n '\n \n test_expect_success 'submodule add --reference with --dissociate does not use alternates' '\n@@ -71,7 +71,7 @@ test_expect_success 'submodule add --reference with --dissociate does not use al\n \t\tgit commit -m B-super-added &&\n \t\tgit repack -ad\n \t) &&\n-\ttest_path_is_missing super/.git/modules/sub-dissociate/objects/info/alternates\n+\ttest_path_is_missing super/.git/submodules/sub-dissociate/objects/info/alternates\n '\n \n test_expect_success 'that reference gets used with add' '\n@@ -94,14 +94,14 @@ test_expect_success 'updating superproject keeps alternates' '\n \ttest_when_finished \"rm -rf super-clone\" &&\n \tgit clone super super-clone &&\n \tgit -C super-clone submodule update --init --reference ../B &&\n-\ttest_alternate_is_used super-clone/.git/modules/sub/objects/info/alternates super-clone/sub\n+\ttest_alternate_is_used super-clone/.git/submodules/sub/objects/info/alternates super-clone/sub\n '\n \n test_expect_success 'updating superproject with --dissociate does not keep alternates' '\n \ttest_when_finished \"rm -rf super-clone\" &&\n \tgit clone super super-clone &&\n \tgit -C super-clone submodule update --init --reference ../B --dissociate &&\n-\ttest_path_is_missing super-clone/.git/modules/sub/objects/info/alternates\n+\ttest_path_is_missing super-clone/.git/submodules/sub/objects/info/alternates\n '\n \n test_expect_success 'submodules use alternates when cloning a superproject' '\n@@ -112,7 +112,7 @@ test_expect_success 'submodules use alternates when cloning a superproject' '\n \t\t# test superproject has alternates setup correctly\n \t\ttest_alternate_is_used .git/objects/info/alternates . &&\n \t\t# test submodule has correct setup\n-\t\ttest_alternate_is_used .git/modules/sub/objects/info/alternates sub\n+\t\ttest_alternate_is_used .git/submodules/sub/objects/info/alternates sub\n \t)\n '\n \n@@ -127,7 +127,7 @@ test_expect_success 'missing submodule alternate fails clone and submodule updat\n \t\t# update of the submodule succeeds\n \t\ttest_must_fail git submodule update --init &&\n \t\t# and we have no alternates:\n-\t\ttest_path_is_missing .git/modules/sub/objects/info/alternates &&\n+\t\ttest_path_is_missing .git/submodules/sub/objects/info/alternates &&\n \t\ttest_path_is_missing sub/file1\n \t)\n '\n@@ -142,7 +142,7 @@ test_expect_success 'ignoring missing submodule alternates passes clone and subm\n \t\t# update of the submodule succeeds\n \t\tgit submodule update --init &&\n \t\t# and we have no alternates:\n-\t\ttest_path_is_missing .git/modules/sub/objects/info/alternates &&\n+\t\ttest_path_is_missing .git/submodules/sub/objects/info/alternates &&\n \t\ttest_path_is_file sub/file1\n \t)\n '\n@@ -176,18 +176,18 @@ test_expect_success 'nested submodule alternate in works and is actually used' '\n \t\t# test superproject has alternates setup correctly\n \t\ttest_alternate_is_used .git/objects/info/alternates . &&\n \t\t# immediate submodule has alternate:\n-\t\ttest_alternate_is_used .git/modules/subwithsub/objects/info/alternates subwithsub &&\n+\t\ttest_alternate_is_used .git/submodules/subwithsub/objects/info/alternates subwithsub &&\n \t\t# nested submodule also has alternate:\n-\t\ttest_alternate_is_used .git/modules/subwithsub/modules/sub/objects/info/alternates subwithsub/sub\n+\t\ttest_alternate_is_used .git/submodules/subwithsub/submodules/sub/objects/info/alternates subwithsub/sub\n \t)\n '\n \n check_that_two_of_three_alternates_are_used() {\n \ttest_alternate_is_used .git/objects/info/alternates . &&\n \t# immediate submodule has alternate:\n-\ttest_alternate_is_used .git/modules/subwithsub/objects/info/alternates subwithsub &&\n+\ttest_alternate_is_used .git/submodules/subwithsub/objects/info/alternates subwithsub &&\n \t# but nested submodule has no alternate:\n-\ttest_path_is_missing .git/modules/subwithsub/modules/sub/objects/info/alternates\n+\ttest_path_is_missing .git/submodules/subwithsub/submodules/sub/objects/info/alternates\n }\n \n \ndiff --git a/t/t7412-submodule-absorbgitdirs.sh b/t/t7412-submodule-absorbgitdirs.sh\nindex 0490499573..dbaca9c69f 100755\n--- a/t/t7412-submodule-absorbgitdirs.sh\n+++ b/t/t7412-submodule-absorbgitdirs.sh\n@@ -29,13 +29,13 @@ test_expect_success 'absorb the git dir' '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''sub1'\\'' from\n \t'\\''$cwd/sub1/.git'\\'' to\n-\t'\\''$cwd/.git/modules/sub1'\\''\n+\t'\\''$cwd/.git/submodules/sub1'\\''\n \tEOF\n \tgit submodule absorbgitdirs 2>actual &&\n \ttest_cmp expect actual &&\n \tgit fsck &&\n \ttest -f sub1/.git &&\n-\ttest -d .git/modules/sub1 &&\n+\ttest -d .git/submodules/sub1 &&\n \tgit status >actual.1 &&\n \tgit -C sub1 rev-parse HEAD >actual.2 &&\n \ttest_cmp expect.1 actual.1 &&\n@@ -47,7 +47,7 @@ test_expect_success 'absorbing does not fail for deinitialized submodules' '\n \tgit submodule deinit --all &&\n \tgit submodule absorbgitdirs 2>err &&\n \ttest_must_be_empty err &&\n-\ttest -d .git/modules/sub1 &&\n+\ttest -d .git/submodules/sub1 &&\n \ttest -d sub1 &&\n \t! test -e sub1/.git\n '\n@@ -68,12 +68,12 @@ test_expect_success 'absorb the git dir in a nested submodule' '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''sub1/nested'\\'' from\n \t'\\''$cwd/sub1/nested/.git'\\'' to\n-\t'\\''$cwd/.git/modules/sub1/modules/nested'\\''\n+\t'\\''$cwd/.git/submodules/sub1/submodules/nested'\\''\n \tEOF\n \tgit submodule absorbgitdirs 2>actual &&\n \ttest_cmp expect actual &&\n \ttest -f sub1/nested/.git &&\n-\ttest -d .git/modules/sub1/modules/nested &&\n+\ttest -d .git/submodules/sub1/submodules/nested &&\n \tgit status >actual.1 &&\n \tgit -C sub1/nested rev-parse HEAD >actual.2 &&\n \ttest_cmp expect.1 actual.1 &&\n@@ -84,11 +84,11 @@ test_expect_success 're-setup nested submodule' '\n \t# un-absorb the direct submodule, to test if the nested submodule\n \t# is still correct (needs a rewrite of the gitfile only)\n \trm -rf sub1/.git &&\n-\tmv .git/modules/sub1 sub1/.git &&\n+\tmv .git/submodules/sub1 sub1/.git &&\n \tGIT_WORK_TREE=. git -C sub1 config --unset core.worktree &&\n \t# fixup the nested submodule\n-\techo \"gitdir: ../.git/modules/nested\" >sub1/nested/.git &&\n-\tGIT_WORK_TREE=../../../nested git -C sub1/.git/modules/nested config \\\n+\techo \"gitdir: ../.git/submodules/nested\" >sub1/nested/.git &&\n+\tGIT_WORK_TREE=../../../nested git -C sub1/.git/submodules/nested config \\\n \t\tcore.worktree \"../../../nested\" &&\n \t# make sure this re-setup is correct\n \tgit status --ignore-submodules=none &&\n@@ -105,13 +105,13 @@ test_expect_success 'absorb the git dir in a nested submodule' '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''sub1'\\'' from\n \t'\\''$cwd/sub1/.git'\\'' to\n-\t'\\''$cwd/.git/modules/sub1'\\''\n+\t'\\''$cwd/.git/submodules/sub1'\\''\n \tEOF\n \tgit submodule absorbgitdirs 2>actual &&\n \ttest_cmp expect actual &&\n \ttest -f sub1/.git &&\n \ttest -f sub1/nested/.git &&\n-\ttest -d .git/modules/sub1/modules/nested &&\n+\ttest -d .git/submodules/sub1/submodules/nested &&\n \tgit status >actual.1 &&\n \tgit -C sub1/nested rev-parse HEAD >actual.2 &&\n \ttest_cmp expect.1 actual.1 &&\n@@ -133,7 +133,7 @@ test_expect_success 'absorb the git dir outside of primary worktree' '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''sub2'\\'' from\n \t'\\''$cwd/repo-wt/sub2/.git'\\'' to\n-\t'\\''$cwd/repo-bare.git/worktrees/repo-wt/modules/sub2'\\''\n+\t'\\''$cwd/repo-bare.git/worktrees/repo-wt/submodules/sub2'\\''\n \tEOF\n \tgit -C repo-wt submodule absorbgitdirs 2>actual &&\n \ttest_cmp expect actual\ndiff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh\nindex 3d3c7af3ce..a51235136d 100755\n--- a/t/t7423-submodule-symlinks.sh\n+++ b/t/t7423-submodule-symlinks.sh\n@@ -49,19 +49,19 @@ test_expect_success SYMLINKS 'git restore --recurse-submodules must not be confu\n \n test_expect_success SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' '\n \tprepare_symlink_to_repo &&\n-\trm -rf .git/modules &&\n+\trm -rf .git/submodules &&\n \ttest_must_fail git restore --recurse-submodules a/sm &&\n \ttest_path_is_dir a/target/.git &&\n-\ttest_path_is_missing .git/modules/a/sm &&\n+\ttest_path_is_missing .git/submodules/a/sm &&\n \ttest_path_is_missing a/target/submodule_file\n '\n \n test_expect_success SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' '\n \tprepare_symlink_to_repo &&\n-\trm -rf .git/modules &&\n+\trm -rf .git/submodules &&\n \ttest_must_fail git checkout -f --recurse-submodules initial &&\n \ttest_path_is_dir a/target/.git &&\n-\ttest_path_is_missing .git/modules/a/sm\n+\ttest_path_is_missing .git/submodules/a/sm\n '\n \n test_done\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex f512eed278..4e2ced3636 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -77,28 +77,28 @@ test_expect_success 'create innocent subrepo' '\n \n test_expect_success 'submodule add refuses invalid names' '\n \ttest_must_fail \\\n-\t\tgit submodule add --name ../../modules/evil \"$PWD/innocent\" evil\n+\t\tgit submodule add --name ../../submodules/evil \"$PWD/innocent\" evil\n '\n \n test_expect_success 'add evil submodule' '\n \tgit submodule add \"$PWD/innocent\" evil &&\n \n-\tmkdir modules &&\n-\tcp -r .git/modules/evil modules &&\n-\twrite_script modules/evil/hooks/post-checkout <<-\\EOF &&\n+\tmkdir submodules &&\n+\tcp -r .git/submodules/evil submodules &&\n+\twrite_script submodules/evil/hooks/post-checkout <<-\\EOF &&\n \techo >&2 \"RUNNING POST CHECKOUT\"\n \tEOF\n \n \tgit config -f .gitmodules submodule.evil.update checkout &&\n \tgit config -f .gitmodules --rename-section \\\n-\t\tsubmodule.evil submodule.../../modules/evil &&\n-\tgit add modules &&\n+\t\tsubmodule.evil submodule.../../submodules/evil &&\n+\tgit add submodules &&\n \tgit commit -am evil\n '\n \n # This step seems like it shouldn't be necessary, since the payload is\n # contained entirely in the evil submodule. But due to the vagaries of the\n-# submodule code, checking out the evil module will fail unless \".git/modules\"\n+# submodule code, checking out the evil module will fail unless \".git/submodules\"\n # exists. Adding another submodule (with a name that sorts before \"evil\") is an\n # easy way to make sure this is the case in the victim clone.\n test_expect_success 'add other submodule' '\n@@ -350,8 +350,8 @@ test_expect_success 'submodule git dir nesting detection must work with parallel\n \tcat err &&\n \tgrep -E \"(already exists|is inside git dir|not a git repository)\" err &&\n \t{\n-\t\ttest_path_is_missing .git/modules/hippo/HEAD ||\n-\t\ttest_path_is_missing .git/modules/hippo/hooks/HEAD\n+\t\ttest_path_is_missing .git/submodules/hippo/HEAD ||\n+\t\ttest_path_is_missing .git/submodules/hippo/hooks/HEAD\n \t}\n '\n \n@@ -361,10 +361,10 @@ test_expect_success 'checkout -f --recurse-submodules must not use a nested gitd\n \t\tcd nested_checkout &&\n \t\tgit submodule init &&\n \t\tgit submodule update thing1 &&\n-\t\tmkdir -p .git/modules/hippo/hooks/refs &&\n-\t\tmkdir -p .git/modules/hippo/hooks/objects/info &&\n-\t\techo \"../../../../objects\" >.git/modules/hippo/hooks/objects/info/alternates &&\n-\t\techo \"ref: refs/heads/master\" >.git/modules/hippo/hooks/HEAD\n+\t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n+\t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n+\t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n+\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD\n \t) &&\n \ttest_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&\n \tcat err &&\n@@ -390,13 +390,13 @@ test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into d\n \tgit config unset -f repo/.gitmodules submodule.sub.path &&\n \tprintf \"\\tpath = \\\"sub\\r\\\"\\n\" >>repo/.gitmodules &&\n \n-\tgit config unset -f repo/.git/modules/sub/config core.worktree &&\n+\tgit config unset -f repo/.git/submodules/sub/config core.worktree &&\n \t{\n \t\tprintf \"[core]\\n\" &&\n \t\tprintf \"\\tworktree = \\\"../../../sub\\r\\\"\\n\"\n-\t} >>repo/.git/modules/sub/config &&\n+\t} >>repo/.git/submodules/sub/config &&\n \n-\tln -s .git/modules/sub/hooks repo/sub &&\n+\tln -s .git/submodules/sub/hooks repo/sub &&\n \tgit -C repo add -A &&\n \tgit -C repo commit -m submodule &&\n \ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..ded482fdf2 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -866,7 +866,7 @@ test_expect_success 'submodule always visited' '\n '\n \n # If a submodule has a `sub/.git/` directory (rather than a file\n-# pointing to the super's `.git/modules/sub`) and `core.fsmonitor`\n+# pointing to the super's `.git/submodules/sub`) and `core.fsmonitor`\n # turned on in the submodule and the daemon is not yet started in\n # the submodule, and someone does a `git submodule absorbgitdirs`\n # in the super, Git will recursively invoke `git submodule--helper`\n@@ -895,7 +895,7 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''dir_1/dir_2/sub'\\'' from\n \t'\\''$cwd/dir_1/dir_2/sub/.git'\\'' to\n-\t'\\''$cwd/.git/modules/dir_1/dir_2/sub'\\''\n+\t'\\''$cwd/.git/submodules/dir_1/dir_2/sub'\\''\n \tEOF\n \tGIT_TRACE2_EVENT=\"$PWD/super-sub.trace\" \\\n \t\tgit -C super submodule absorbgitdirs >out 2>actual &&\n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524298","messageId":"20250816213642.3517822-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 3/9] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:36Z","receivedAt":"2025-08-16T21:37:40Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This adds an ability to override gitdir paths via config files\n(not .gitmodules), such that any encoding scheme can be changed\nand JGit & co don't need to exactly match the default encoding.\n\nA new test and a helper are added. The helper will be used by\nfurther tests exercising gitdir paths & encodings.\n\nBased-on-patch-by: Brandon Williams <bmwill@google.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c           | 17 +++++++++++++++++\n submodule.c                           | 11 +++++++++++\n t/lib-verify-submodule-gitdir-path.sh | 15 +++++++++++++++\n t/t7400-submodule-basic.sh            | 15 +++++++++++++++\n 4 files changed, 58 insertions(+)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 7243429c6f..30e40d6c79 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1214,6 +1214,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo UNUSED)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, the_repository, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3597,6 +3613,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\ndiff --git a/submodule.c b/submodule.c\nindex dbf2244e60..bf78636195 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2611,6 +2611,17 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t * administrators can explicitly set. Nothing has been decided,\n \t * so for now, just append the name at the end of the path.\n \t */\n+\tchar *gitdir_path, *key;\n+\n+\t/* Allow config override. */\n+\tkey = xstrfmt(\"submodule.%s.gitdirpath\", submodule_name);\n+\tif (!repo_config_get_string(r, key, &gitdir_path)) {\n+\t\tstrbuf_addstr(buf, gitdir_path);\n+\t\tfree(key);\n+\t\tfree(gitdir_path);\n+\t\treturn;\n+\t}\n+\tfree(key);\n \n \t/* Legacy behavior: allow existing paths under modules/<name>. */\n \trepo_git_path_append(r, buf, \"modules/\");\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..fb5cb8eea4\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,15 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir in path $3\n+\n+verify_submodule_gitdir_path() {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\tcat >expect <<-EOF &&\n+\t\t\t$(git rev-parse --git-common-dir)/$path\n+\t\tEOF\n+\t\tgit submodule--helper gitdir \"$name\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\nindex 178c386212..f4d4fb8397 100755\n--- a/t/t7400-submodule-basic.sh\n+++ b/t/t7400-submodule-basic.sh\n@@ -13,6 +13,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup - enable local submodules' '\n \tgit config --global protocol.file.allow always\n@@ -1505,4 +1506,18 @@ test_expect_success 'submodule add fails when name is reused' '\n \t)\n '\n \n+test_expect_success 'submodule helper gitdir config overrides' '\n+\tverify_submodule_gitdir_path test-submodule child submodules/child &&\n+\t(\n+\t\tcd test-submodule &&\n+\t\tgit config submodule.child.gitdirpath \".git/submodules/custom-child\"\n+\t) &&\n+\tverify_submodule_gitdir_path test-submodule child submodules/custom-child &&\n+\t(\n+\t\tcd test-submodule &&\n+\t\tgit config --unset submodule.child.gitdirpath\n+\t) &&\n+\tverify_submodule_gitdir_path test-submodule child submodules/child\n+'\n+\n test_done\n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524300","messageId":"20250816213642.3517822-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 4/9] t: submodules: add basic mixed gitdir path tests","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:37Z","receivedAt":"2025-08-16T21:37:40Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add some basic submodule tests for mixed gitdir path handling of\nlegacy (.git/modules) and new-style (.git/submodule) paths.\n\nFor now these just test the coexistence, creation and push/pull of\nsubmodules using mixed paths.\n\nMore tests will be added later, especially for new-style encoding.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/meson.build                           |   1 +\n t/t7425-submodule-mixed-gitdir-paths.sh | 101 ++++++++++++++++++++++++\n 2 files changed, 102 insertions(+)\n create mode 100755 t/t7425-submodule-mixed-gitdir-paths.sh\n\ndiff --git a/t/meson.build b/t/meson.build\nindex bbeba1a8d5..ffd74f1d3b 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -874,6 +874,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-mixed-gitdir-paths.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\nnew file mode 100755\nindex 0000000000..801e90522a\n--- /dev/null\n+++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n@@ -0,0 +1,101 @@\n+#!/bin/sh\n+\n+test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+\tgit config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed new and legacy submodules' '\n+\tgit init legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init main &&\n+\t(\n+\t\tcd main &&\n+\n+\t\tgit config receive.denyCurrentBranch updateInstead &&\n+\n+\t\tgit submodule add ../new-sub new &&\n+\t\ttest_commit new-sub &&\n+\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\t# simulate legacy .git/modules path by moving submodule\n+\t\tmkdir -p .git/modules &&\n+\t\tmv .git/submodules/legacy .git/modules/ &&\n+\t\techo \"gitdir: ../.git/modules/legacy\" > legacy/.git\n+\t)\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned &&\n+\t(\n+\t\tcd cloned &&\n+\n+\t\t# At this point, .git/modules/<name> should not exist as\n+\t\t# submodules are checked out into the new path\n+\t\ttest_path_is_dir .git/submodules/legacy &&\n+\t\ttest_path_is_dir .git/submodules/new &&\n+\n+\t\tgit submodule status >list &&\n+\t\tgrep \"$legacy_rev legacy\" list &&\n+\t\tgrep \"$new_rev new\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to submodules' '\n+\t(\n+\t\tcd cloned &&\n+\n+\t\tgit -C legacy switch --track -C master origin/master  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C new switch --track -C master origin/master &&\n+\t\ttest_commit -C new second-commit &&\n+\t\tgit -C new push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C master origin/master  &&\n+\t\tgit add legacy new &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/submodules/new &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\tgrep \"$legacy_rev legacy\" list &&\n+\t\tgrep \"$new_rev new\" list\n+\t)\n+'\n+\n+test_done\n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524301","messageId":"20250816213642.3517822-6-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 5/9] strbuf: bring back is_rfc3986_unreserved","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:38Z","receivedAt":"2025-08-16T21:37:43Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Commit f89854362c (\"credential-store: move related functions to...\")\nmoved the function inside credential-store.c, making it static under\nthe correct assumption (at the time) that it's the only place used.\n\nHowever now we need it to apply url encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c | 6 ------\n strbuf.c                   | 6 ++++++\n strbuf.h                   | 2 ++\n 3 files changed, 8 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..0acaf1cc82 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -76,12 +76,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/strbuf.c b/strbuf.c\nindex 6c3851a7f8..e8d84cbb6d 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -817,6 +817,12 @@ void strbuf_addstr_xml_quoted(struct strbuf *buf, const char *s)\n \t}\n }\n \n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n static void strbuf_add_urlencode(struct strbuf *sb, const char *s, size_t len,\n \t\t\t\t char_predicate allow_unencoded_fn)\n {\ndiff --git a/strbuf.h b/strbuf.h\nindex a580ac6084..5139269039 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -640,6 +640,8 @@ static inline void strbuf_complete_line(struct strbuf *sb)\n \n typedef int (*char_predicate)(char ch);\n \n+int is_rfc3986_unreserved(char ch);\n+\n void strbuf_addstr_urlencode(struct strbuf *sb, const char *name,\n \t\t\t     char_predicate allow_unencoded_fn);\n \n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524302","messageId":"20250816213642.3517822-7-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 6/9] submodule: encode gitdir paths to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:39Z","receivedAt":"2025-08-16T21:37:47Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This follows up commit ce125d431a (\"submodule: extract path to submodule\ngitdir func\") to resolve gitdir path conflicts, eg. names \"a\" and \"a/b\",\nby encoding the submodule names before the paths are built.\n\nBased on previous work by Brandon & all [1].\n\nA custom encoding can become unnecesarily complex, while url-encoding is\nrelatively well-known, however it needs some extending to support case\ninsensitive filesystems and quirks like Windows reserving \"COM1\" names.\nHence why I opted to encode A as _a, B as _b and so on, which also fixes\nthe COM1 case, as suggested in [1].\n\nThe current implementation errors out if the encoded name is too long.\nThis can be improved, for e.g. the encoded name could be trimmed as the\nconflict probability is low at the NAME_MAX length mark, or long names\ncould be sharded into subdirectories in the future.\n\nThis also fixes the existing affected tests and adds a TODO to cleanup\nthe short-circuit in validate_submodule_git_dir().\n\nA further commit will some more tests to exercise these codepaths.\n\nLink: https://lore.kernel.org/git/20180807230637.247200-1-bmwill@google.com/ [1]\nBased-on-patch-by: Brandon Williams <bmwill@google.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n submodule.c                 | 65 ++++++++++++++++++++++++-------------\n t/t7400-submodule-basic.sh  |  2 +-\n t/t7406-submodule-update.sh | 10 +++---\n t/t7450-bad-git-dotfiles.sh | 39 ++++++++++++----------\n 4 files changed, 69 insertions(+), 47 deletions(-)\n\ndiff --git a/submodule.c b/submodule.c\nindex bf78636195..722a8e4f2a 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2271,8 +2271,13 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \n \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n \t    strcmp(p, submodule_name))\n-\t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n-\t\t    submodule_name, git_dir);\n+\t\t/*\n+\t\t * TODO: revisit and cleanup this test short-circuit, because\n+\t\t * submodules with encoded names are expected to take this path.\n+\t\t * Likely just move the invariants to submodule_name_to_gitdir()\n+\t\t * and delete this entire function in a future commit.\n+\t\t */\n+\t\treturn 0;\n \n \t/*\n \t * We prevent the contents of sibling submodules' git directories to\n@@ -2588,30 +2593,26 @@ int submodule_to_gitdir(struct repository *repo,\n \treturn ret;\n }\n \n+static void strbuf_addstr_case_encode(struct strbuf *dst, const char *src)\n+{\n+\tfor (; *src; src++) {\n+\t\tunsigned char c = *src;\n+\t\tif (c >= 'A' && c <= 'Z') {\n+\t\t\tstrbuf_addch(dst, '_');\n+\t\t\tstrbuf_addch(dst, c - 'A' + 'a');\n+\t\t} else {\n+\t\t\tstrbuf_addch(dst, c);\n+\t\t}\n+\t}\n+}\n+\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n-\t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n-\t */\n+\tstruct strbuf encoded_sub_name = STRBUF_INIT, tmp = STRBUF_INIT;\n+\tsize_t base_len, encoded_len;\n \tchar *gitdir_path, *key;\n+\tlong name_max;\n \n \t/* Allow config override. */\n \tkey = xstrfmt(\"submodule.%s.gitdirpath\", submodule_name);\n@@ -2632,5 +2633,23 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t/* New style (encoded) paths go under submodules/<encoded>. */\n \tstrbuf_reset(buf);\n \trepo_git_path_append(r, buf, \"submodules/\");\n-\tstrbuf_addstr(buf, submodule_name);\n+\tbase_len = buf->len;\n+\n+\t/* URL-encode then case case-encode A to _a, B to _b and so on */\n+\tstrbuf_addstr_urlencode(&tmp, submodule_name, is_rfc3986_unreserved);\n+\tstrbuf_addstr_case_encode(&encoded_sub_name, tmp.buf);\n+\tstrbuf_release(&tmp);\n+\tstrbuf_addbuf(buf, &encoded_sub_name);\n+\n+\t/* Ensure final path length is below NAME_MAX after encoding */\n+\tname_max = pathconf(buf->buf, _PC_NAME_MAX);\n+\tif (name_max == -1)\n+\t\tname_max = NAME_MAX;\n+\n+\tencoded_len = buf->len - base_len;\n+\tif (encoded_len >= name_max)\n+\t\tdie(_(\"encoded submodule name '%s' is too long (%zu bytes, limit is %ld)\"),\n+\t\t    encoded_sub_name.buf, encoded_len, name_max);\n+\n+\tstrbuf_release(&encoded_sub_name);\n }\ndiff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\nindex f4d4fb8397..607fdd26bd 100755\n--- a/t/t7400-submodule-basic.sh\n+++ b/t/t7400-submodule-basic.sh\n@@ -1047,7 +1047,7 @@ test_expect_success 'recursive relative submodules stay relative' '\n \t\tcd clone2 &&\n \t\tgit submodule update --init --recursive &&\n \t\techo \"gitdir: ../.git/submodules/sub3\" >./sub3/.git_expect &&\n-\t\techo \"gitdir: ../../../.git/submodules/sub3/submodules/dirdir/subsub\" >./sub3/dirdir/subsub/.git_expect\n+\t\techo \"gitdir: ../../../.git/submodules/sub3/submodules/dirdir%2fsubsub\" >./sub3/dirdir/subsub/.git_expect\n \t) &&\n \ttest_cmp clone2/sub3/.git_expect clone2/sub3/.git &&\n \ttest_cmp clone2/sub3/dirdir/subsub/.git_expect clone2/sub3/dirdir/subsub/.git\ndiff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh\nindex f0c4da1ffa..c44a7e9513 100755\n--- a/t/t7406-submodule-update.sh\n+++ b/t/t7406-submodule-update.sh\n@@ -864,8 +864,8 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir' '\n \t (cd deeper/submodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/submodules/deeper/submodule &&\n-\t  git log > ../../../../actual\n+\t (cd .git/submodules/deeper%2fsubmodule &&\n+\t  git log > ../../../actual\n \t ) &&\n \t test_cmp expected actual\n \t)\n@@ -882,8 +882,8 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir' '\n \t (cd deeper/submodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/submodules/deeper/submodule &&\n-\t  git log > ../../../../actual\n+\t (cd .git/submodules/deeper%2fsubmodule &&\n+\t  git log > ../../../actual\n \t ) &&\n \t test_cmp expected actual\n \t)\n@@ -899,7 +899,7 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir recur\n \t  git commit -m \"added subsubmodule\" &&\n \t  git push origin :\n \t ) &&\n-\t (cd .git/submodules/deeper/submodule/submodules/subsubmodule &&\n+\t (cd .git/submodules/deeper%2fsubmodule/submodules/subsubmodule &&\n \t  git log > ../../../../../actual\n \t ) &&\n \t git add deeper/submodule &&\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex 4e2ced3636..27254300f8 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -15,6 +15,7 @@ Such as:\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-pack.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup' '\n \tgit config --global protocol.file.allow always\n@@ -319,6 +320,8 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' '\n \tfi\n '\n \n+# TODO: move these nested gitdir tests to another location in a later commit because\n+# they are not pathological cases anymore: by encoding the gitdir paths do not conflict.\n test_expect_success 'setup submodules with nested git dirs' '\n \tgit init nested &&\n \ttest_commit -C nested nested &&\n@@ -341,35 +344,35 @@ test_expect_success 'setup submodules with nested git dirs' '\n '\n \n test_expect_success 'git dirs of sibling submodules must not be nested' '\n-\ttest_must_fail git clone --recurse-submodules nested clone 2>err &&\n-\ttest_grep \"is inside git dir\" err\n+\tgit clone --recurse-submodules nested clone_nested &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n '\n \n test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n-\ttest_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err &&\n-\tcat err &&\n-\tgrep -E \"(already exists|is inside git dir|not a git repository)\" err &&\n-\t{\n-\t\ttest_path_is_missing .git/submodules/hippo/HEAD ||\n-\t\ttest_path_is_missing .git/submodules/hippo/hooks/HEAD\n-\t}\n+\tgit clone --recurse-submodules --jobs=2 nested clone_parallel &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n '\n \n-test_expect_success 'checkout -f --recurse-submodules must not use a nested gitdir' '\n-\tgit clone nested nested_checkout &&\n+test_expect_success 'checkout -f --recurse-submodules must corectly handle nested gitdirs' '\n+\tgit clone nested clone_recursive_checkout &&\n \t(\n-\t\tcd nested_checkout &&\n+\t\tcd clone_recursive_checkout &&\n+\n \t\tgit submodule init &&\n-\t\tgit submodule update thing1 &&\n+\t\tgit submodule update thing1 thing2 &&\n+\n+\t\t# simulate a malicious nested alternate which git should not follow\n \t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n \t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n \t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n-\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD\n+\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD &&\n+\n+\t\tgit checkout -f --recurse-submodules HEAD\n \t) &&\n-\ttest_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&\n-\tcat err &&\n-\tgrep \"is inside git dir\" err &&\n-\ttest_path_is_missing nested_checkout/thing2/.git\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n '\n \n test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into different directory' '\n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524303","messageId":"20250816213642.3517822-8-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 7/9] submodule: remove validate_submodule_git_dir()","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:40Z","receivedAt":"2025-08-16T21:37:50Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"The validate_submodule_git_dir test is not very useful anymore, after\nsubmodule names are encoded to resolve gitdir path conflicts.\n\nIn other words, the purpouse of gitdir path encoding is precisely to\navoid such conflicts as this function tries to also prevent.\n\nThe first test from the function can be kept though, because it just\nverifies invariants which should always be true and raise a BUG if:\n\n  - no \"/\" separator is between dirs/names.\n  - len(full_gitdir) < len(name).\n  - name does not match the gitdir path suffix.\n\nThus we move the invariant checks to submodule_name_to_gitdir() and\nclean up the rest of validate_submodule_git_dir() and its uses.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 21 -----------\n submodule.c                 | 74 ++++---------------------------------\n submodule.h                 |  5 ---\n 3 files changed, 7 insertions(+), 93 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 30e40d6c79..d1ae864e8f 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1725,10 +1725,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n \t\t\t\t\t\t    clone_data->path);\n \n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\n \tif (!file_exists(sm_gitdir)) {\n \t\tif (clone_data->require_init && !stat(clone_data_path, &st) &&\n \t\t    !is_empty_dir(clone_data_path))\n@@ -1802,23 +1798,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tfree(path);\n \t}\n \n-\t/*\n-\t * We already performed this check at the beginning of this function,\n-\t * before cloning the objects. This tries to detect racy behavior e.g.\n-\t * in parallel clones, where another process could easily have made the\n-\t * gitdir nested _after_ it was created.\n-\t *\n-\t * To prevent further harm coming from this unintentionally-nested\n-\t * gitdir, let's disable it by deleting the `HEAD` file.\n-\t */\n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {\n-\t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n-\t\tunlink(head);\n-\t\tfree(head);\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\t}\n-\n \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n \n \tp = repo_submodule_path(the_repository, clone_data_path, \"config\");\ndiff --git a/submodule.c b/submodule.c\nindex 722a8e4f2a..cfb45a8f9f 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2163,27 +2163,10 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\tif (!submodule_uses_gitfile(path))\n \t\t\t\tabsorb_git_dir_into_superproject(path,\n \t\t\t\t\t\t\t\t super_prefix);\n-\t\t\telse {\n-\t\t\t\tchar *dotgit = xstrfmt(\"%s/.git\", path);\n-\t\t\t\tchar *git_dir = xstrdup(read_gitfile(dotgit));\n-\n-\t\t\t\tfree(dotgit);\n-\t\t\t\tif (validate_submodule_git_dir(git_dir,\n-\t\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n-\t\t\t\t\t      \"another submodule's git dir\"),\n-\t\t\t\t\t    git_dir);\n-\t\t\t\tfree(git_dir);\n-\t\t\t}\n \t\t} else {\n \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n \t\t\t\t\t\t sub->name);\n-\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n-\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n-\t\t\t\t      \"submodule's git dir\"),\n-\t\t\t\t    gitdir.buf);\n \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n \t\t\tstrbuf_release(&gitdir);\n \n@@ -2263,52 +2246,6 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n-{\n-\tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n-\tchar *p;\n-\tint ret = 0;\n-\n-\tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n-\t    strcmp(p, submodule_name))\n-\t\t/*\n-\t\t * TODO: revisit and cleanup this test short-circuit, because\n-\t\t * submodules with encoded names are expected to take this path.\n-\t\t * Likely just move the invariants to submodule_name_to_gitdir()\n-\t\t * and delete this entire function in a future commit.\n-\t\t */\n-\t\treturn 0;\n-\n-\t/*\n-\t * We prevent the contents of sibling submodules' git directories to\n-\t * clash.\n-\t *\n-\t * Example: having a submodule named `hippo` and another one named\n-\t * `hippo/hooks` would result in the git directories\n-\t * `.git/submodules/hippo/` and `.git/submodules/hippo/hooks/`, respectively,\n-\t * but the latter directory is already designated to contain the hooks\n-\t * of the former.\n-\t */\n-\tfor (; *p; p++) {\n-\t\tif (is_dir_sep(*p)) {\n-\t\t\tchar c = *p;\n-\n-\t\t\t*p = '\\0';\n-\t\t\tif (is_git_directory(git_dir))\n-\t\t\t\tret = -1;\n-\t\t\t*p = c;\n-\n-\t\t\tif (ret < 0)\n-\t\t\t\treturn error(_(\"submodule git dir '%s' is \"\n-\t\t\t\t\t       \"inside git dir '%.*s'\"),\n-\t\t\t\t\t     git_dir,\n-\t\t\t\t\t     (int)(p - git_dir), git_dir);\n-\t\t}\n-\t}\n-\n-\treturn 0;\n-}\n-\n int validate_submodule_path(const char *path)\n {\n \tchar *p = xstrdup(path);\n@@ -2367,9 +2304,6 @@ static void relocate_single_git_dir_into_superproject(const char *path,\n \t\tdie(_(\"could not lookup name for submodule '%s'\"), path);\n \n \tsubmodule_name_to_gitdir(&new_gitdir, the_repository, sub->name);\n-\tif (validate_submodule_git_dir(new_gitdir.buf, sub->name) < 0)\n-\t\tdie(_(\"refusing to move '%s' into an existing git dir\"),\n-\t\t    real_old_git_dir);\n \tif (safe_create_leading_directories_const(the_repository, new_gitdir.buf) < 0)\n \t\tdie(_(\"could not create directory '%s'\"), new_gitdir.buf);\n \treal_new_git_dir = real_pathdup(new_gitdir.buf, 1);\n@@ -2611,7 +2545,7 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n {\n \tstruct strbuf encoded_sub_name = STRBUF_INIT, tmp = STRBUF_INIT;\n \tsize_t base_len, encoded_len;\n-\tchar *gitdir_path, *key;\n+\tchar *gitdir_path, *key, *p;\n \tlong name_max;\n \n \t/* Allow config override. */\n@@ -2651,5 +2585,11 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\tdie(_(\"encoded submodule name '%s' is too long (%zu bytes, limit is %ld)\"),\n \t\t    encoded_sub_name.buf, encoded_len, name_max);\n \n+\t/* Trigger a BUG if these invariants do not hold */\n+\tp = buf->buf + buf->len - encoded_len;\n+\tif (buf->len <= encoded_len || p[-1] != '/' || strcmp(p, encoded_sub_name.buf))\n+\t\tBUG(\"encoded submodule name '%s' is not a suffix of git dir '%s'\",\n+\t\t    encoded_sub_name.buf, buf->buf);\n+\n \tstrbuf_release(&encoded_sub_name);\n }\ndiff --git a/submodule.h b/submodule.h\nindex b10e16e6c0..0b7692bc20 100644\n--- a/submodule.h\n+++ b/submodule.h\n@@ -137,11 +137,6 @@ int submodule_to_gitdir(struct repository *repo,\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name);\n \n-/*\n- * Make sure that no submodule's git dir is nested in a sibling submodule's.\n- */\n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name);\n-\n /*\n  * Make sure that the given submodule path does not follow symlinks.\n  */\n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524304","messageId":"20250816213642.3517822-9-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 8/9] t: move nested gitdir tests to proper location","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:41Z","receivedAt":"2025-08-16T21:37:54Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Now that we are encoding gitdir paths, these tests are not handling\npathological cases anymore, because nested git dirs shouldn't cause\nconflicts, so move them from t7450-bad-git-dotfiles.sh to a more\nappropriate location where we test mixed gitdir path & encoding use.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/t7425-submodule-mixed-gitdir-paths.sh | 54 ++++++++++++++++++++++++\n t/t7450-bad-git-dotfiles.sh             | 56 -------------------------\n 2 files changed, 54 insertions(+), 56 deletions(-)\n\ndiff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\nindex 801e90522a..902b2560ca 100755\n--- a/t/t7425-submodule-mixed-gitdir-paths.sh\n+++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n@@ -3,6 +3,7 @@\n test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup: allow file protocol' '\n \tgit config --global protocol.file.allow always\n@@ -98,4 +99,57 @@ test_expect_success 'fetch mixed submodule changes and verify updates' '\n \t)\n '\n \n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of sibling submodules must not be nested' '\n+\tgit clone --recurse-submodules nested clone_nested &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone --recurse-submodules --jobs=2 nested clone_parallel &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n+'\n+\n+test_expect_success 'checkout -f --recurse-submodules must corectly handle nested gitdirs' '\n+\tgit clone nested clone_recursive_checkout &&\n+\t(\n+\t\tcd clone_recursive_checkout &&\n+\n+\t\tgit submodule init &&\n+\t\tgit submodule update thing1 thing2 &&\n+\n+\t\t# simulate a malicious nested alternate which git should not follow\n+\t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n+\t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n+\t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n+\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD &&\n+\n+\t\tgit checkout -f --recurse-submodules HEAD\n+\t) &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n+'\n+\n test_done\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex 27254300f8..18624fabc4 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -15,7 +15,6 @@ Such as:\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-pack.sh\n-. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup' '\n \tgit config --global protocol.file.allow always\n@@ -320,61 +319,6 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' '\n \tfi\n '\n \n-# TODO: move these nested gitdir tests to another location in a later commit because\n-# they are not pathological cases anymore: by encoding the gitdir paths do not conflict.\n-test_expect_success 'setup submodules with nested git dirs' '\n-\tgit init nested &&\n-\ttest_commit -C nested nested &&\n-\t(\n-\t\tcd nested &&\n-\t\tcat >.gitmodules <<-EOF &&\n-\t\t[submodule \"hippo\"]\n-\t\t\turl = .\n-\t\t\tpath = thing1\n-\t\t[submodule \"hippo/hooks\"]\n-\t\t\turl = .\n-\t\t\tpath = thing2\n-\t\tEOF\n-\t\tgit clone . thing1 &&\n-\t\tgit clone . thing2 &&\n-\t\tgit add .gitmodules thing1 thing2 &&\n-\t\ttest_tick &&\n-\t\tgit commit -m nested\n-\t)\n-'\n-\n-test_expect_success 'git dirs of sibling submodules must not be nested' '\n-\tgit clone --recurse-submodules nested clone_nested &&\n-\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n-\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n-'\n-\n-test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n-\tgit clone --recurse-submodules --jobs=2 nested clone_parallel &&\n-\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n-\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n-'\n-\n-test_expect_success 'checkout -f --recurse-submodules must corectly handle nested gitdirs' '\n-\tgit clone nested clone_recursive_checkout &&\n-\t(\n-\t\tcd clone_recursive_checkout &&\n-\n-\t\tgit submodule init &&\n-\t\tgit submodule update thing1 thing2 &&\n-\n-\t\t# simulate a malicious nested alternate which git should not follow\n-\t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n-\t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n-\t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n-\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD &&\n-\n-\t\tgit checkout -f --recurse-submodules HEAD\n-\t) &&\n-\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n-\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n-'\n-\n test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into different directory' '\n \ttest_when_finished \"rm -rf sub repo bad-clone\" &&\n \n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524305","messageId":"20250816213642.3517822-10-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH 9/9] t: add gitdir encoding tests","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-16T21:36:42Z","receivedAt":"2025-08-16T21:37:56Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add some tests to further exercise the gitdir encoding functionality\nalongside the existing mixed directory and nested gitdir tests.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/t7425-submodule-mixed-gitdir-paths.sh | 52 +++++++++++++++++++++++++\n 1 file changed, 52 insertions(+)\n\ndiff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\nindex 902b2560ca..cfdf487a56 100755\n--- a/t/t7425-submodule-mixed-gitdir-paths.sh\n+++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n@@ -152,4 +152,56 @@ test_expect_success 'checkout -f --recurse-submodules must corectly handle neste\n \tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n '\n \n+test_expect_success 'new style submodule gitdir paths are properly encoded' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# add new-style submodule name containing /\n+\t\tgit submodule add ../new-sub foo/bar &&\n+\t\tgit commit -m \"add foo/bar\" &&\n+\n+\t\t# simulate existing legacy submodule name containing escaping char %\n+\t\tgit clone --separate-git-dir .git/modules/foo%bar ../legacy-sub foo%bar  &&\n+\t\tcat >>.gitmodules <<-EOF &&\n+\t\t[submodule \"foo%bar\"]\n+\t\t\tpath = foo%bar\n+\t\t\turl = ../legacy-sub\n+\t\tEOF\n+\t\tgit add .gitmodules &&\n+\t\tgit commit -m \"add foo%bar\" &&\n+\n+\t\t# add new style submodule name containing escaping char %\n+\t\tgit submodule add ../new-sub fooish%bar &&\n+\t\tgit commit -m \"add fooish%bar\" &&\n+\n+\t\t# add a mixed case submdule name\n+\t\tgit submodule add ../new-sub FooBar &&\n+\t\tgit commit -m \"add FooBar\" &&\n+\n+\t\t# add a reserved name on Windows\n+\t\tgit submodule add ../new-sub COM1 &&\n+\t\tgit commit -m \"add COM1\"\n+\t) &&\n+\tverify_submodule_gitdir_path main foo/bar submodules/foo%2fbar &&\n+\tverify_submodule_gitdir_path main foo%bar modules/foo%bar &&\n+\tverify_submodule_gitdir_path main fooish%bar submodules/fooish%25bar &&\n+\tverify_submodule_gitdir_path main FooBar submodules/_foo_bar &&\n+\tverify_submodule_gitdir_path main COM1 submodules/_c_o_m1\n+'\n+\n+test_expect_success 'submodule encoded name exceeds max name limit' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# find the system NAME_MAX (fall back to 255 if unknown)\n+\t\tname_max=$(getconf NAME_MAX . 2>/dev/null || echo 255) &&\n+\n+\t\t# each \"%\" char encodes to \"%25\" (3 chars), ensure we exceed NAME_MAX\n+\t\tcount=$((name_max + 10)) &&\n+\t\tlongname=$(printf \"%%%0.s\" $(seq 1 $count)) &&\n+\n+\t\ttest_must_fail git submodule add ../new-sub \"$longname\"\n+\t)\n+'\n+\n test_done\n-- \n2.50.1.679.gbf363a8fbb.dirty\n\n"},{"id":"524306","messageId":"AF619536-A31E-4D5C-A553-4CDF8D05FCB0@gmail.com","threadId":"63967","inReplyTo":"20250816213642.3517822-6-adrian.ratiu@collabora.com","subject":"Re: [PATCH 5/9] strbuf: bring back is_rfc3986_unreserved","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-16T21:56:49Z","receivedAt":"2025-08-16T21:57:01Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 16 août 2025 à 17:39, Adrian Ratiu <adrian.ratiu@collabora.com> a écrit :\n> \n> ﻿Commit f89854362c (\"credential-store: move related functions to...\")\n\nHere and elsewhere, we refer to commits by the output of “git show -s --format=reference <object>”\n\nBest,\nBen"},{"id":"524318","messageId":"198b81e845f.3d0b85c81892243.3876804306135930880@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH 0/9] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-17T13:01:04Z","receivedAt":"2025-08-17T13:01:26Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":" ---- On Sun, 17 Aug 2025 00:36:33 +0300  Adrian Ratiu <adrian.ratiu@collabora.com> wrote --- \n > Hello,\n > \n > This is a continuation of work done back in 2018 [1], so a big thank you to\n > everyone who participated in the initial thread, especially Brandon on whose\n > code this is partially based upon. Hope you are still around and doing well. :)\n > \n > It's mostly a rewrite from scratch addressig open feedback. I decided to\n > iterate upen Brandon's url-encoding design instead of pursuing alternatives\n > like a custom encoding, name hashing or round-trip encoding/decoding using\n > an in-memory git mapping (we'd still have to encode/hash the paths to avoid\n > colflicts so IIUC this last one is more complicated for little gain).\n > \n > I tried to organize and explain the commits in a logical way which is also\n > easy to review, keeping the encoding parts, new tests, code moving around\n > and path update churn as clearly separated as possible.\n > \n > This is based on master and I've merged and succesfully run all tests in\n > both the next and seen branches.\n\nI also ran the GitHub CI pipeline and noticed there are failures on Win + Mac.\n\nI will address those in v2.\n\nIn the meantime I'll leave v1 for a while on the ML to gather more feedback.\n"},{"id":"524374","messageId":"xmqqplcsgu7w.fsf@gitster.g","threadId":"63967","inReplyTo":"20250816213642.3517822-10-adrian.ratiu@collabora.com","subject":"Re: [PATCH 9/9] t: add gitdir encoding tests","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-18T22:06:59Z","receivedAt":"2025-08-18T22:07:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Add some tests to further exercise the gitdir encoding functionality\n> alongside the existing mixed directory and nested gitdir tests.\n>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>  t/t7425-submodule-mixed-gitdir-paths.sh | 52 +++++++++++++++++++++++++\n>  1 file changed, 52 insertions(+)\n>\n> diff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\n> index 902b2560ca..cfdf487a56 100755\n> --- a/t/t7425-submodule-mixed-gitdir-paths.sh\n> +++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n> @@ -152,4 +152,56 @@ test_expect_success 'checkout -f --recurse-submodules must corectly handle neste\n> ...\n> +\t\tlongname=$(printf \"%%%0.s\" $(seq 1 $count)) &&\n\nUse of 'seq' gets complaint from\n\n    $ make -C t test-lint-shell-syntax\n\nSee the commit message of d17cf5f3 (tests: Introduce test_seq,\n2012-08-04) and b32c7ec0 (test-lib: teach test_seq the -f option,\n2025-06-23).  I think you should be able to do something like\n\n\tlongname=$(test_seq -f \"%%%0.s\" 1 $count) &&\n\nbut I haven't even run the test with such a fix, so take it with a\ngrain of salt, please.\n\n\n\n"},{"id":"524547","messageId":"qcpwoggznb2hj4kegtnouh3ty2sepuhmqlhhzfbpvm7d2yt33y@6vnui5pnnl7x","threadId":"63967","inReplyTo":"20250816213642.3517822-2-adrian.ratiu@collabora.com","subject":"Re: [PATCH 1/9] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2025-08-20T19:04:16Z","receivedAt":"2025-08-20T19:04:26Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2025.08.17 00:36, Adrian Ratiu wrote:\n> While testing submodule gitdir path encoding, I noticed submodule--helper\n> is still using a hardcoded name-based path leading to test failures, so\n> convert it to the common helper function introduced by commit ce125d431a\n> (\"submodule: extract path to submodule gitdir func\") and used in other\n> locations accross the source tree.\n> \n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>  builtin/submodule--helper.c | 11 ++++++-----\n>  1 file changed, 6 insertions(+), 5 deletions(-)\n> \n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index 07a1935cbe..7243429c6f 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -3213,10 +3213,11 @@ static int add_submodule(const struct add_data *add_data)\n>  \t\tfree(submod_gitdir_path);\n>  \t} else {\n>  \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n> +\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n>  \n> -\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n> +\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n\nI believe submod_gitdir_path is now only used in the `if (...) {...}`\nside corresponding to this `else` branch, so perhaps we should make it\nlocal to that block?\n\n\n> -\t\tif (is_directory(submod_gitdir_path)) {\n> +\t\tif (is_directory(submod_gitdir.buf)) {\n>  \t\t\tif (!add_data->force) {\n>  \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n>  \t\t\t\tchar *die_msg;\n> @@ -3225,8 +3226,8 @@ static int add_submodule(const struct add_data *add_data)\n>  \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n>  \t\t\t\t\t    add_data->sm_name);\n>  \n> -\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n> -\t\t\t\tfree(submod_gitdir_path);\n> +\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n> +\t\t\t\tstrbuf_release(&submod_gitdir);\n>  \n>  \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n>  \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n> @@ -3244,7 +3245,7 @@ static int add_submodule(const struct add_data *add_data)\n>  \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n>  \t\t\t}\n>  \t\t}\n> -\t\tfree(submod_gitdir_path);\n> +\t\tstrbuf_release(&submod_gitdir);\n>  \n>  \t\tclone_data.prefix = add_data->prefix;\n>  \t\tclone_data.path = add_data->sm_path;\n> -- \n> 2.50.1.679.gbf363a8fbb.dirty\n> \n> \n"},{"id":"524551","messageId":"20250820192947.GA1663047@coredump.intra.peff.net","threadId":"63967","inReplyTo":"20250816213642.3517822-7-adrian.ratiu@collabora.com","subject":"Re: [PATCH 6/9] submodule: encode gitdir paths to avoid conflicts","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-08-20T19:29:47Z","receivedAt":"2025-08-20T19:29:49Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Sun, Aug 17, 2025 at 12:36:39AM +0300, Adrian Ratiu wrote:\n\n> @@ -2632,5 +2633,23 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \t/* New style (encoded) paths go under submodules/<encoded>. */\n>  \tstrbuf_reset(buf);\n>  \trepo_git_path_append(r, buf, \"submodules/\");\n> -\tstrbuf_addstr(buf, submodule_name);\n> +\tbase_len = buf->len;\n> +\n> +\t/* URL-encode then case case-encode A to _a, B to _b and so on */\n> +\tstrbuf_addstr_urlencode(&tmp, submodule_name, is_rfc3986_unreserved);\n> +\tstrbuf_addstr_case_encode(&encoded_sub_name, tmp.buf);\n> +\tstrbuf_release(&tmp);\n> +\tstrbuf_addbuf(buf, &encoded_sub_name);\n> +\n> +\t/* Ensure final path length is below NAME_MAX after encoding */\n> +\tname_max = pathconf(buf->buf, _PC_NAME_MAX);\n> +\tif (name_max == -1)\n> +\t\tname_max = NAME_MAX;\n\nThis patch seems to break the Windows CI builds, as they don't have\npathconf() there. I guess we'd need a compat wrapper that returns -1 in\nthis case. And likewise protects _PC_NAME_MAX from being seen on systems\nthat don't have it.\n\n> +\tencoded_len = buf->len - base_len;\n> +\tif (encoded_len >= name_max)\n> +\t\tdie(_(\"encoded submodule name '%s' is too long (%zu bytes, limit is %ld)\"),\n> +\t\t    encoded_sub_name.buf, encoded_len, name_max);\n\nIt also complained about %z here. I think you have to use PRIuMAX\ninstead. Likewise size_t is a \"long long\" on Windows (LLP64). So \"%ld\"\nprobably also needs to be PRIuMAX.\n\nI also saw failures on the osx jobs for t7527.62 (submodule\nabsorbgitdirs implicitly starts daemon). I didn't dig in, but I can\nguess they may be related to this series.\n\n-Peff\n"},{"id":"524552","messageId":"l4bmyst4qtew7kv7sdgzw5hibwor34zrh3c4jib7i6vlsniey6@jtku24uffrmo","threadId":"63967","inReplyTo":"20250816213642.3517822-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH 3/9] submodule: add gitdir path config override","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2025-08-20T19:37:36Z","receivedAt":"2025-08-20T19:37:43Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2025.08.17 00:36, Adrian Ratiu wrote:\n[snip]\n> diff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\n> new file mode 100644\n> index 0000000000..fb5cb8eea4\n> --- /dev/null\n> +++ b/t/lib-verify-submodule-gitdir-path.sh\n> @@ -0,0 +1,15 @@\n> +# Helper to verify if repo $1 contains a submodule named $2 with gitdir in path $3\n\nThis comment is a bit inaccurate, right? If I'm reading correctly, we\nonly verify that the submodule's gitdir actually exists in the \"legacy\"\n.git/modules/$path case. If we don't see anything there, we fall through\nto .git/submodules/$encoded_path, but we never verify it actually\nexists.\n\n\n> +\n> +verify_submodule_gitdir_path() {\n> +\trepo=\"$1\" &&\n> +\tname=\"$2\" &&\n> +\tpath=\"$3\" &&\n> +\t(\n> +\t\tcd \"$repo\" &&\n> +\t\tcat >expect <<-EOF &&\n> +\t\t\t$(git rev-parse --git-common-dir)/$path\n> +\t\tEOF\n> +\t\tgit submodule--helper gitdir \"$name\" >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +}\n> diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\n> index 178c386212..f4d4fb8397 100755\n> --- a/t/t7400-submodule-basic.sh\n> +++ b/t/t7400-submodule-basic.sh\n> @@ -13,6 +13,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n>  export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>  \n>  . ./test-lib.sh\n> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n>  \n>  test_expect_success 'setup - enable local submodules' '\n>  \tgit config --global protocol.file.allow always\n> @@ -1505,4 +1506,18 @@ test_expect_success 'submodule add fails when name is reused' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'submodule helper gitdir config overrides' '\n> +\tverify_submodule_gitdir_path test-submodule child submodules/child &&\n> +\t(\n> +\t\tcd test-submodule &&\n> +\t\tgit config submodule.child.gitdirpath \".git/submodules/custom-child\"\n> +\t) &&\n> +\tverify_submodule_gitdir_path test-submodule child submodules/custom-child &&\n> +\t(\n> +\t\tcd test-submodule &&\n> +\t\tgit config --unset submodule.child.gitdirpath\n> +\t) &&\n> +\tverify_submodule_gitdir_path test-submodule child submodules/child\n> +'\n> +\n>  test_done\n> -- \n> 2.50.1.679.gbf363a8fbb.dirty\n> \n> \n"},{"id":"524580","messageId":"3hzj6k4yxfcvpt33jkblcafljhbj5npjia2u7sprqbkfcxoxwa@6qmaqwj3x7hc","threadId":"63967","inReplyTo":"20250816213642.3517822-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH 3/9] submodule: add gitdir path config override","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2025-08-20T21:38:56Z","receivedAt":"2025-08-20T21:39:02Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2025.08.17 00:36, Adrian Ratiu wrote:\n[snip]\n> diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\n> index 178c386212..f4d4fb8397 100755\n> --- a/t/t7400-submodule-basic.sh\n> +++ b/t/t7400-submodule-basic.sh\n> @@ -13,6 +13,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n>  export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>  \n>  . ./test-lib.sh\n> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n>  \n>  test_expect_success 'setup - enable local submodules' '\n>  \tgit config --global protocol.file.allow always\n> @@ -1505,4 +1506,18 @@ test_expect_success 'submodule add fails when name is reused' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'submodule helper gitdir config overrides' '\n> +\tverify_submodule_gitdir_path test-submodule child submodules/child &&\n> +\t(\n> +\t\tcd test-submodule &&\n> +\t\tgit config submodule.child.gitdirpath \".git/submodules/custom-child\"\n> +\t) &&\n> +\tverify_submodule_gitdir_path test-submodule child submodules/custom-child &&\n> +\t(\n> +\t\tcd test-submodule &&\n> +\t\tgit config --unset submodule.child.gitdirpath\n> +\t) &&\n> +\tverify_submodule_gitdir_path test-submodule child submodules/child\n> +'\n> +\n\nRather than `( cd test-submodule && git config ... )` here, you should\nuse `test_config -C test-submodule ...` and `test_unconfig -C\ntest-submodule ...`\n"},{"id":"524581","messageId":"km4qhwxpmwld2qw4ihmzjtk7o5yeblfhhm6hqsh4nmzdbkdyyc@mwqb7a76liop","threadId":"63967","inReplyTo":"20250816213642.3517822-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH 3/9] submodule: add gitdir path config override","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2025-08-20T21:50:31Z","receivedAt":"2025-08-20T21:50:38Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2025.08.17 00:36, Adrian Ratiu wrote:\n> This adds an ability to override gitdir paths via config files\n> (not .gitmodules), such that any encoding scheme can be changed\n> and JGit & co don't need to exactly match the default encoding.\n> \n> A new test and a helper are added. The helper will be used by\n> further tests exercising gitdir paths & encodings.\n> \n> Based-on-patch-by: Brandon Williams <bmwill@google.com>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>  builtin/submodule--helper.c           | 17 +++++++++++++++++\n>  submodule.c                           | 11 +++++++++++\n>  t/lib-verify-submodule-gitdir-path.sh | 15 +++++++++++++++\n>  t/t7400-submodule-basic.sh            | 15 +++++++++++++++\n>  4 files changed, 58 insertions(+)\n>  create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n\nSorry to keep sending piecemeal feedback. You should also document the\nnew config option in `Documentation/config/submodule.adoc`\n"},{"id":"524583","messageId":"dm32eyyidns26e7swqmdxjgxgbrlkq65fwpxfvsaapdgpbkfgq@iy2dxxcwigtj","threadId":"63967","inReplyTo":"20250816213642.3517822-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH 4/9] t: submodules: add basic mixed gitdir path tests","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2025-08-20T22:07:13Z","receivedAt":"2025-08-20T22:07:21Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2025.08.17 00:36, Adrian Ratiu wrote:\n> Add some basic submodule tests for mixed gitdir path handling of\n> legacy (.git/modules) and new-style (.git/submodule) paths.\n> \n> For now these just test the coexistence, creation and push/pull of\n> submodules using mixed paths.\n> \n> More tests will be added later, especially for new-style encoding.\n> \n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>  t/meson.build                           |   1 +\n>  t/t7425-submodule-mixed-gitdir-paths.sh | 101 ++++++++++++++++++++++++\n>  2 files changed, 102 insertions(+)\n>  create mode 100755 t/t7425-submodule-mixed-gitdir-paths.sh\n> \n> diff --git a/t/meson.build b/t/meson.build\n> index bbeba1a8d5..ffd74f1d3b 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -874,6 +874,7 @@ integration_tests = [\n>    't7422-submodule-output.sh',\n>    't7423-submodule-symlinks.sh',\n>    't7424-submodule-mixed-ref-formats.sh',\n> +  't7425-submodule-mixed-gitdir-paths.sh',\n>    't7450-bad-git-dotfiles.sh',\n>    't7500-commit-template-squash-signoff.sh',\n>    't7501-commit-basic-functionality.sh',\n> diff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\n> new file mode 100755\n> index 0000000000..801e90522a\n> --- /dev/null\n> +++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n> @@ -0,0 +1,101 @@\n> +#!/bin/sh\n> +\n> +test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup: allow file protocol' '\n> +\tgit config --global protocol.file.allow always\n> +'\n> +\n> +test_expect_success 'create repo with mixed new and legacy submodules' '\n> +\tgit init legacy-sub &&\n> +\ttest_commit -C legacy-sub legacy-initial &&\n> +\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n> +\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n> +\n> +\tgit init new-sub &&\n> +\ttest_commit -C new-sub new-initial &&\n> +\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n> +\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n> +\n> +\tgit init main &&\n> +\t(\n> +\t\tcd main &&\n> +\n> +\t\tgit config receive.denyCurrentBranch updateInstead &&\n> +\n> +\t\tgit submodule add ../new-sub new &&\n> +\t\ttest_commit new-sub &&\n> +\n> +\t\tgit submodule add ../legacy-sub legacy &&\n> +\t\ttest_commit legacy-sub &&\n> +\n> +\t\t# simulate legacy .git/modules path by moving submodule\n> +\t\tmkdir -p .git/modules &&\n> +\t\tmv .git/submodules/legacy .git/modules/ &&\n> +\t\techo \"gitdir: ../.git/modules/legacy\" > legacy/.git\n> +\t)\n> +'\n> +\n> +test_expect_success 'clone from repo with both legacy and new-style submodules' '\n> +\tgit clone --recurse-submodules main cloned &&\n> +\t(\n> +\t\tcd cloned &&\n> +\n> +\t\t# At this point, .git/modules/<name> should not exist as\n> +\t\t# submodules are checked out into the new path\n> +\t\ttest_path_is_dir .git/submodules/legacy &&\n> +\t\ttest_path_is_dir .git/submodules/new &&\n> +\n> +\t\tgit submodule status >list &&\n> +\t\tgrep \"$legacy_rev legacy\" list &&\n> +\t\tgrep \"$new_rev new\" list\n> +\t)\n\nYou probably want to use `test_grep` here and below.\n"},{"id":"524644","messageId":"877byw9aqp.fsf@collabora.com","threadId":"63967","inReplyTo":"qcpwoggznb2hj4kegtnouh3ty2sepuhmqlhhzfbpvm7d2yt33y@6vnui5pnnl7x","subject":"Re: [PATCH 1/9] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-21T11:26:22Z","receivedAt":"2025-08-21T11:26:56Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Wed, 20 Aug 2025, Josh Steadmon <steadmon@google.com> wrote:\n> On 2025.08.17 00:36, Adrian Ratiu wrote:\n>> While testing submodule gitdir path encoding, I noticed submodule--helper\n>> is still using a hardcoded name-based path leading to test failures, so\n>> convert it to the common helper function introduced by commit ce125d431a\n>> (\"submodule: extract path to submodule gitdir func\") and used in other\n>> locations accross the source tree.\n>> \n>> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n>> ---\n>>  builtin/submodule--helper.c | 11 ++++++-----\n>>  1 file changed, 6 insertions(+), 5 deletions(-)\n>> \n>> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n>> index 07a1935cbe..7243429c6f 100644\n>> --- a/builtin/submodule--helper.c\n>> +++ b/builtin/submodule--helper.c\n>> @@ -3213,10 +3213,11 @@ static int add_submodule(const struct add_data *add_data)\n>>  \t\tfree(submod_gitdir_path);\n>>  \t} else {\n>>  \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n>> +\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n>>  \n>> -\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n>> +\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n>\n> I believe submod_gitdir_path is now only used in the `if (...) {...}`\n> side corresponding to this `else` branch, so perhaps we should make it\n> local to that block?\n\nYes, I'll move it in v2. Thanks!\n"},{"id":"524646","messageId":"874iu098cn.fsf@collabora.com","threadId":"63967","inReplyTo":"l4bmyst4qtew7kv7sdgzw5hibwor34zrh3c4jib7i6vlsniey6@jtku24uffrmo","subject":"Re: [PATCH 3/9] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-21T12:18:00Z","receivedAt":"2025-08-21T12:18:29Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Wed, 20 Aug 2025, Josh Steadmon <steadmon@google.com> wrote:\n> On 2025.08.17 00:36, Adrian Ratiu wrote: [snip] \n>> diff --git a/t/lib-verify-submodule-gitdir-path.sh \n>> b/t/lib-verify-submodule-gitdir-path.sh new file mode 100644 \n>> index 0000000000..fb5cb8eea4 --- /dev/null +++ \n>> b/t/lib-verify-submodule-gitdir-path.sh @@ -0,0 +1,15 @@ +# \n>> Helper to verify if repo $1 contains a submodule named $2 with \n>> gitdir in path $3 \n> \n> This comment is a bit inaccurate, right? If I'm reading \n> correctly, we only verify that the submodule's gitdir actually \n> exists in the \"legacy\" .git/modules/$path case. If we don't see \n> anything there, we fall through to \n> .git/submodules/$encoded_path, but we never verify it actually \n> exists. \n> \n \nIt was not my intention to imply gitdir existence is verified \nhere. :)\n\nWe just verify where the gitdirs are expected vs configured, \nregardless of existence (eg when adding a new submodule it won't \nexist beforehand).\n\nThe behavior you describe is correct: we only verify existence in \nthe legacy case, yes, in submodule_name_to_gitdir(), because only \nthose must exist beforehand and that's how we know we're in the \nlegacy (backwards compatibility) case, otherwise we default to the \nnewly encoded paths.\n\nHope this makes sense. I'll expand the description to clarify in v2.\n"},{"id":"524651","messageId":"871pp4967w.fsf@collabora.com","threadId":"63967","inReplyTo":"3hzj6k4yxfcvpt33jkblcafljhbj5npjia2u7sprqbkfcxoxwa@6qmaqwj3x7hc","subject":"Re: [PATCH 3/9] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-21T13:04:03Z","receivedAt":"2025-08-21T13:04:29Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Wed, 20 Aug 2025, Josh Steadmon <steadmon@google.com> wrote:\n> On 2025.08.17 00:36, Adrian Ratiu wrote: [snip] \n>> diff --git a/t/t7400-submodule-basic.sh \n>> b/t/t7400-submodule-basic.sh index 178c386212..f4d4fb8397 \n>> 100755 --- a/t/t7400-submodule-basic.sh +++ \n>> b/t/t7400-submodule-basic.sh @@ -13,6 +13,7 @@ \n>> GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main \n>>  export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME  . ./test-lib.sh \n>> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh \n>>   test_expect_success 'setup - enable local submodules' ' git \n>>  config --global protocol.file.allow always \n>> @@ -1505,4 +1506,18 @@ test_expect_success 'submodule add fails \n>> when name is reused' ' \n>>  \t) '  \n>> +test_expect_success 'submodule helper gitdir config overrides' \n>> ' +\tverify_submodule_gitdir_path test-submodule child \n>> submodules/child && +\t( +\t\tcd test-submodule \n>> && +\t\tgit config submodule.child.gitdirpath \n>> \".git/submodules/custom-child\" +\t) && + \n>> verify_submodule_gitdir_path test-submodule child \n>> submodules/custom-child && +\t( +\t\tcd test-submodule \n>> && +\t\tgit config --unset submodule.child.gitdirpath +\t) \n>> && +\tverify_submodule_gitdir_path test-submodule child \n>> submodules/child +' + \n> \n> Rather than `( cd test-submodule && git config ... )` here, you \n> should use `test_config -C test-submodule ...` and \n> `test_unconfig -C test-submodule ...` \n\nack, will do in v2.\n"},{"id":"524652","messageId":"87y0rc7rl1.fsf@collabora.com","threadId":"63967","inReplyTo":"km4qhwxpmwld2qw4ihmzjtk7o5yeblfhhm6hqsh4nmzdbkdyyc@mwqb7a76liop","subject":"Re: [PATCH 3/9] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-21T13:05:30Z","receivedAt":"2025-08-21T13:05:52Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Wed, 20 Aug 2025, Josh Steadmon <steadmon@google.com> wrote:\n> On 2025.08.17 00:36, Adrian Ratiu wrote: \n>> This adds an ability to override gitdir paths via config files \n>> (not .gitmodules), such that any encoding scheme can be changed \n>> and JGit & co don't need to exactly match the default encoding. \n>> A new test and a helper are added. The helper will be used by \n>> further tests exercising gitdir paths & encodings. \n>> Based-on-patch-by: Brandon Williams <bmwill@google.com> \n>> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> --- \n>>  builtin/submodule--helper.c           | 17 +++++++++++++++++ \n>>  submodule.c                           | 11 +++++++++++ \n>>  t/lib-verify-submodule-gitdir-path.sh | 15 +++++++++++++++ \n>>  t/t7400-submodule-basic.sh            | 15 +++++++++++++++ 4 \n>>  files changed, 58 insertions(+) create mode 100644 \n>>  t/lib-verify-submodule-gitdir-path.sh \n> \n> Sorry to keep sending piecemeal feedback. You should also \n> document the new config option in \n> `Documentation/config/submodule.adoc` \n\nNo problem, thank you for taking the time to review & give \nfeedback.\n\nWill do in v2.\n"},{"id":"524653","messageId":"87v7mg7rgg.fsf@collabora.com","threadId":"63967","inReplyTo":"AF619536-A31E-4D5C-A553-4CDF8D05FCB0@gmail.com","subject":"Re: [PATCH 5/9] strbuf: bring back is_rfc3986_unreserved","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-21T13:08:15Z","receivedAt":"2025-08-21T13:08:37Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Sat, 16 Aug 2025, Ben Knoble <ben.knoble@gmail.com> wrote:\n>> Le 16 août 2025 à 17:39, Adrian Ratiu \n>> <adrian.ratiu@collabora.com> a écrit :  ﻿Commit f89854362c \n>> (\"credential-store: move related functions to...\") \n> \n> Here and elsewhere, we refer to commits by the output of “git \n> show -s --format=reference <object>” \n> \n> Best, Ben \n\nAck, will fix in v2.\n"},{"id":"524654","messageId":"87sehk7r66.fsf@collabora.com","threadId":"63967","inReplyTo":"20250820192947.GA1663047@coredump.intra.peff.net","subject":"Re: [PATCH 6/9] submodule: encode gitdir paths to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-21T13:14:25Z","receivedAt":"2025-08-21T13:14:47Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Wed, 20 Aug 2025, Jeff King <peff@peff.net> wrote:\n> On Sun, Aug 17, 2025 at 12:36:39AM +0300, Adrian Ratiu wrote: \n> \n>> @@ -2632,5 +2633,23 @@ void submodule_name_to_gitdir(struct \n>> strbuf *buf, struct repository *r, \n>>  \t/* New style (encoded) paths go under \n>>  submodules/<encoded>. */ strbuf_reset(buf); \n>>  repo_git_path_append(r, buf, \"submodules/\"); \n>> -\tstrbuf_addstr(buf, submodule_name); +\tbase_len = \n>> buf->len; + +\t/* URL-encode then case case-encode A to \n>> _a, B to _b and so on */ +\tstrbuf_addstr_urlencode(&tmp, \n>> submodule_name, is_rfc3986_unreserved); + \n>> strbuf_addstr_case_encode(&encoded_sub_name, tmp.buf); + \n>> strbuf_release(&tmp); +\tstrbuf_addbuf(buf, \n>> &encoded_sub_name); + +\t/* Ensure final path length is \n>> below NAME_MAX after encoding */ +\tname_max = \n>> pathconf(buf->buf, _PC_NAME_MAX); +\tif (name_max == -1) + \n>> name_max = NAME_MAX; \n> \n> This patch seems to break the Windows CI builds, as they don't \n> have pathconf() there. I guess we'd need a compat wrapper that \n> returns -1 in this case. And likewise protects _PC_NAME_MAX from \n> being seen on systems that don't have it. \n>\n\nAck, will fix in v2.\n \n>> +\tencoded_len = buf->len - base_len; +\tif (encoded_len >= \n>> name_max) +\t\tdie(_(\"encoded submodule name '%s' is too \n>> long (%zu bytes, limit is %ld)\"), + \n>> encoded_sub_name.buf, encoded_len, name_max); \n> \n> It also complained about %z here. I think you have to use \n> PRIuMAX instead. Likewise size_t is a \"long long\" on Windows \n> (LLP64). So \"%ld\" probably also needs to be PRIuMAX. \n> \n> I also saw failures on the osx jobs for t7527.62 (submodule \n> absorbgitdirs implicitly starts daemon). I didn't dig in, but I \n> can guess they may be related to this series. \n\nIt's possible. I'm so sorry for these breakages and will address \nthem in v2. I'll use the GitHub CI since I don't have access to \nwin+mac systems.\n\nAlso thank you for your patience, this is my first git project patch. :)\n"},{"id":"524655","messageId":"87plco7r0t.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqplcsgu7w.fsf@gitster.g","subject":"Re: [PATCH 9/9] t: add gitdir encoding tests","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-08-21T13:17:38Z","receivedAt":"2025-08-21T13:17:57Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 18 Aug 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> Add some tests to further exercise the gitdir encoding \n>> functionality alongside the existing mixed directory and nested \n>> gitdir tests. \n>> \n>> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> --- \n>>  t/t7425-submodule-mixed-gitdir-paths.sh | 52 \n>>  +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) \n>> \n>> diff --git a/t/t7425-submodule-mixed-gitdir-paths.sh \n>> b/t/t7425-submodule-mixed-gitdir-paths.sh index \n>> 902b2560ca..cfdf487a56 100755 --- \n>> a/t/t7425-submodule-mixed-gitdir-paths.sh +++ \n>> b/t/t7425-submodule-mixed-gitdir-paths.sh @@ -152,4 +152,56 @@ \n>> test_expect_success 'checkout -f --recurse-submodules must \n>> corectly handle neste ...  +\t\tlongname=$(printf \"%%%0.s\" \n>> $(seq 1 $count)) && \n> \n> Use of 'seq' gets complaint from \n> \n>     $ make -C t test-lint-shell-syntax \n> \n> See the commit message of d17cf5f3 (tests: Introduce test_seq, \n> 2012-08-04) and b32c7ec0 (test-lib: teach test_seq the -f \n> option, 2025-06-23).  I think you should be able to do something \n> like \n> \n> \tlongname=$(test_seq -f \"%%%0.s\" 1 $count) && \n> \n> but I haven't even run the test with such a fix, so take it with \n> a grain of salt, please. \n\nAck and thank you for the pointers. Will address in v2.\n"},{"id":"525385","messageId":"xmqqjz2gh2zs.fsf@gitster.g","threadId":"63967","inReplyTo":"20250816213642.3517822-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH 4/9] t: submodules: add basic mixed gitdir path tests","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-09-02T23:02:47Z","receivedAt":"2025-09-02T23:02:50Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> +test_expect_success 'commit and push changes to submodules' '\n> +\t(\n> +\t\tcd cloned &&\n> +\n> +\t\tgit -C legacy switch --track -C master origin/master  &&\n\nThis test needs to future-proof itself, perhaps with something like\nto force the initial branch name to a known value.\n\n t/t7425-submodule-mixed-gitdir-paths.sh | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\nindex 8a2d2e917f..02bd48aeeb 100755\n--- a/t/t7425-submodule-mixed-gitdir-paths.sh\n+++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n@@ -2,9 +2,13 @@\n \n test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n \n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=master\n+\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n+\n test_expect_success 'setup: allow file protocol' '\n \tgit config --global protocol.file.allow always\n '\n-- \n2.51.0-302-ga83f9e55f9\n\n"},{"id":"525801","messageId":"20250908140117.262205-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 01/10] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:08Z","receivedAt":"2025-09-08T14:02:27Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded name-based path leading to test failures, so\nconvert it to the common helper function introduced by commit ce125d431a\n(submodule: extract path to submodule gitdir func, 2021-09-15)  and used\nin other locations accross the source tree.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 13 +++++++------\n 1 file changed, 7 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 07a1935cbe..d06e2fe265 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3193,13 +3193,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n \n static int add_submodule(const struct add_data *add_data)\n {\n-\tchar *submod_gitdir_path;\n \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n \tint ret = -1;\n \n \t/* perhaps the path already exists and is already a git repo, else clone it */\n \tif (is_directory(add_data->sm_path)) {\n+\t\tchar *submod_gitdir_path;\n \t\tstruct strbuf sm_path = STRBUF_INIT;\n \t\tstrbuf_addstr(&sm_path, add_data->sm_path);\n \t\tsubmod_gitdir_path = xstrfmt(\"%s/.git\", add_data->sm_path);\n@@ -3213,10 +3213,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3225,8 +3226,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3244,7 +3245,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.51.GIT\n\n"},{"id":"525802","messageId":"20250908140117.262205-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 00/10] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:07Z","receivedAt":"2025-09-08T14:02:27Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello all,\n\nThis is v2 of the submodule encoding topic which fixes filesystem conflicts\ndue to plain-text module name clashes (nested dirs, case insensitive FS).\n\nThis is based on the master branch and I've merged and ensured all CI passes\nwith both next and seen branches, with all combinations (linux, win, osx, meson\nmakefile, etc).\n\nChanges between v1 -> v2:\n* Added test name prefixes to commits modifying tests (eg \"t7454: modify ...\")\n* Fixed all GitHub CI failures, including Win+Linux+Mac for both make and meson\n* Test branch names now use main, which also fixes linux-breaking-changes (Junio)\n* Replaced seq with test_seq for portability (Junio)\n* Moved submod_gitdir_path variable declaration inside if block (Josh)\n* Clarify FS existence check inside lib-verify-submodule-gitdir-path.sh (Josh)\n* Replaced (cd submod && git config) with test_config -C submod (Josh)\n* Document the new submodule--helper gitdir path option (Josh)\n* Replaced grep -> test_grep (Josh)\n* Reworded commit messages to use the proper commit reference format (Ben)\n* Removed claim this fixes the Windows reserved file names (Mark)\n* Split NAME_MAX logic into separate commit & added compat stub for pathconf (Peff)\n\nMany thanks to all who have reviewed v1.\n\nAdrian Ratiu (10):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  submodule: create new gitdirs under submodules path\n  submodule: add gitdir path config override\n  t7425: add basic mixed submodule gitdir path tests\n  strbuf: bring back is_rfc3986_unreserved\n  submodule: encode gitdir paths to avoid conflicts\n  submodule: error out if gitdir name is too long\n  submodule: remove validate_submodule_git_dir()\n  t7450: move nested gitdir tests to t7425\n  t7425: add gitdir encoding tests\n\n Documentation/config/submodule.adoc        |   4 +\n Documentation/fetch-options.adoc           |   2 +-\n Documentation/git-fetch.adoc               |   2 +-\n Documentation/git-submodule.adoc           |   2 +-\n Documentation/gitsubmodules.adoc           |   8 +-\n Makefile                                   |   5 +\n builtin/credential-store.c                 |   6 -\n builtin/submodule--helper.c                |  51 +++---\n compat/pathconf.c                          |  10 +\n compat/posix.h                             |   8 +\n config.mak.uname                           |   2 +\n meson.build                                |   1 +\n setup.c                                    |   2 +-\n strbuf.c                                   |   6 +\n strbuf.h                                   |   2 +\n submodule.c                                | 162 ++++++++---------\n submodule.h                                |   5 -\n t/lib-submodule-update.sh                  |  50 ++---\n t/lib-verify-submodule-gitdir-path.sh      |  20 ++\n t/meson.build                              |   1 +\n t/t0035-safe-bare-repository.sh            |   4 +-\n t/t1600-index.sh                           |   4 +-\n t/t2405-worktree-submodule.sh              |   8 +-\n t/t2501-cwd-empty.sh                       |   2 +-\n t/t3600-rm.sh                              |   8 +-\n t/t5526-fetch-submodules.sh                |   2 +-\n t/t5619-clone-local-ambiguous-transport.sh |   4 +-\n t/t6120-describe.sh                        |   4 +-\n t/t7001-mv.sh                              |   4 +-\n t/t7400-submodule-basic.sh                 |  27 ++-\n t/t7406-submodule-update.sh                |  14 +-\n t/t7407-submodule-foreach.sh               |   6 +-\n t/t7408-submodule-reference.sh             |  22 +--\n t/t7412-submodule-absorbgitdirs.sh         |  22 +--\n t/t7423-submodule-symlinks.sh              |   8 +-\n t/t7425-submodule-mixed-gitdir-paths.sh    | 202 +++++++++++++++++++++\n t/t7450-bad-git-dotfiles.sh                |  73 +-------\n t/t7527-builtin-fsmonitor.sh               |   4 +-\n t/t9902-completion.sh                      |   1 +\n 39 files changed, 481 insertions(+), 287 deletions(-)\n create mode 100644 compat/pathconf.c\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-mixed-gitdir-paths.sh\n\n-- \n2.51.GIT\n\n"},{"id":"525803","messageId":"20250908140117.262205-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 03/10] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:10Z","receivedAt":"2025-09-08T14:02:33Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This adds an ability to override gitdir paths via config files\n(not .gitmodules), such that any encoding scheme can be changed\nand JGit & co don't need to exactly match the default encoding.\n\nA new test and a helper are added. The helper will be used by\nfurther tests exercising gitdir paths & encodings.\n\nBased-on-patch-by: Brandon Williams <bmwill@google.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/submodule.adoc   |  4 ++++\n builtin/submodule--helper.c           | 17 +++++++++++++++++\n submodule.c                           | 11 +++++++++++\n t/lib-verify-submodule-gitdir-path.sh | 20 ++++++++++++++++++++\n t/t7400-submodule-basic.sh            |  9 +++++++++\n t/t9902-completion.sh                 |  1 +\n 6 files changed, 62 insertions(+)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n\ndiff --git a/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 0672d99117..8f64adfbe3 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -52,6 +52,10 @@ submodule.<name>.active::\n \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n \tdetails.\n \n+submodule.<name>.gitdir::\n+\tThis option sets the gitdir path for submodule <name>, allowing users\n+\tto override the default path or change the default path name encoding.\n+\n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\n \tsubmodule's path to determine if the submodule is of interest to git\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex d06e2fe265..564f7aadf8 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1214,6 +1214,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo UNUSED)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, the_repository, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3597,6 +3613,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\ndiff --git a/submodule.c b/submodule.c\nindex dbf2244e60..bf78636195 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2611,6 +2611,17 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t * administrators can explicitly set. Nothing has been decided,\n \t * so for now, just append the name at the end of the path.\n \t */\n+\tchar *gitdir_path, *key;\n+\n+\t/* Allow config override. */\n+\tkey = xstrfmt(\"submodule.%s.gitdirpath\", submodule_name);\n+\tif (!repo_config_get_string(r, key, &gitdir_path)) {\n+\t\tstrbuf_addstr(buf, gitdir_path);\n+\t\tfree(key);\n+\t\tfree(gitdir_path);\n+\t\treturn;\n+\t}\n+\tfree(key);\n \n \t/* Legacy behavior: allow existing paths under modules/<name>. */\n \trepo_git_path_append(r, buf, \"modules/\");\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..ef2a8a47a7\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,20 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n+\n+# This does not check filesystem existence. That is done in submodule.c via the\n+# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n+# might or might not exist (eg. when adding a new submodule), so this only checks\n+# the expected configuration path, which might be overridden by the user.\n+\n+verify_submodule_gitdir_path() {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\tcat >expect <<-EOF &&\n+\t\t\t$(git rev-parse --git-common-dir)/$path\n+\t\tEOF\n+\t\tgit submodule--helper gitdir \"$name\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\nindex 178c386212..a632f47b73 100755\n--- a/t/t7400-submodule-basic.sh\n+++ b/t/t7400-submodule-basic.sh\n@@ -13,6 +13,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup - enable local submodules' '\n \tgit config --global protocol.file.allow always\n@@ -1505,4 +1506,12 @@ test_expect_success 'submodule add fails when name is reused' '\n \t)\n '\n \n+test_expect_success 'submodule helper gitdir config overrides' '\n+\tverify_submodule_gitdir_path test-submodule child submodules/child &&\n+\ttest_config -C test-submodule submodule.child.gitdirpath \".git/submodules/custom-child\" &&\n+\tverify_submodule_gitdir_path test-submodule child submodules/custom-child &&\n+\ttest_unconfig -C test-submodule submodule.child.gitdirpath &&\n+\tverify_submodule_gitdir_path test-submodule child submodules/child\n+'\n+\n test_done\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 6650d33fba..928e519267 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n \tsubmodule.sub.fetchRecurseSubmodules Z\n \tsubmodule.sub.ignore Z\n \tsubmodule.sub.active Z\n+\tsubmodule.sub.gitdir Z\n \tEOF\n '\n \n-- \n2.51.GIT\n\n"},{"id":"525804","messageId":"20250908140117.262205-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 02/10] submodule: create new gitdirs under submodules path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:09Z","receivedAt":"2025-09-08T14:02:35Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This is in preparation for encoding the submodule names to avoid conflicts\nlike submodules named foo and foo/bar together with case-insensitive file-\nsystem handling and other corner cases like reserved filenames on Windows.\n\nBackward compatibility is kept with plain-name modules already existing at\npaths like .git/modules/<name>, however a clear separation between legacy\n(plain) and new (encoded) namespaces is desirable, to avoid situations like\nan existing plain-name module containing the encoding escape character/\n\nThus we split the new-style (encoded) gitdir name paths to .git/submodules,\nwhile legacy-style paths remain under .git/modules.\n\nThis is just a default directory change with the accompanying test updates,\nin preparation for the actual encoding additions in future commits.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/fetch-options.adoc           |  2 +-\n Documentation/git-fetch.adoc               |  2 +-\n Documentation/git-submodule.adoc           |  2 +-\n Documentation/gitsubmodules.adoc           |  8 ++--\n setup.c                                    |  2 +-\n submodule.c                                | 28 +++++++++---\n t/lib-submodule-update.sh                  | 50 +++++++++++-----------\n t/t0035-safe-bare-repository.sh            |  4 +-\n t/t1600-index.sh                           |  4 +-\n t/t2405-worktree-submodule.sh              |  8 ++--\n t/t2501-cwd-empty.sh                       |  2 +-\n t/t3600-rm.sh                              |  8 ++--\n t/t5526-fetch-submodules.sh                |  2 +-\n t/t5619-clone-local-ambiguous-transport.sh |  4 +-\n t/t6120-describe.sh                        |  4 +-\n t/t7001-mv.sh                              |  4 +-\n t/t7400-submodule-basic.sh                 | 18 ++++----\n t/t7406-submodule-update.sh                | 10 ++---\n t/t7407-submodule-foreach.sh               |  6 +--\n t/t7408-submodule-reference.sh             | 22 +++++-----\n t/t7412-submodule-absorbgitdirs.sh         | 22 +++++-----\n t/t7423-submodule-symlinks.sh              |  8 ++--\n t/t7450-bad-git-dotfiles.sh                | 32 +++++++-------\n t/t7527-builtin-fsmonitor.sh               |  4 +-\n 24 files changed, 136 insertions(+), 120 deletions(-)\n\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex d3ac31f4e2..2605a58b72 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -215,7 +215,7 @@ ifndef::git-pull[]\n \tsubmodule that has commits that are referenced by a newly fetched\n \tsuperproject commit but are missing in the local submodule clone. A\n \tchanged submodule can be fetched as long as it is present locally e.g.\n-\tin `$GIT_DIR/modules/` (see linkgit:gitsubmodules[7]); if the upstream\n+\tin `$GIT_DIR/submodules/` (see linkgit:gitsubmodules[7]); if the upstream\n \tadds a new submodule, that submodule cannot be fetched until it is\n \tcloned e.g. by `git submodule update`.\n +\ndiff --git a/Documentation/git-fetch.adoc b/Documentation/git-fetch.adoc\nindex 16f5d9d69a..2923a29bef 100644\n--- a/Documentation/git-fetch.adoc\n+++ b/Documentation/git-fetch.adoc\n@@ -304,7 +304,7 @@ include::config/fetch.adoc[]\n BUGS\n ----\n Using --recurse-submodules can only fetch new commits in submodules that are\n-present locally e.g. in `$GIT_DIR/modules/`. If the upstream adds a new\n+present locally e.g. in `$GIT_DIR/submodules/`. If the upstream adds a new\n submodule, that submodule cannot be fetched until it is cloned e.g. by `git\n submodule update`. This is expected to be fixed in a future Git version.\n \ndiff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc\nindex 95beaee561..08008eeb62 100644\n--- a/Documentation/git-submodule.adoc\n+++ b/Documentation/git-submodule.adoc\n@@ -266,7 +266,7 @@ registered submodules, and sync any nested submodules within.\n absorbgitdirs::\n \tIf a git directory of a submodule is inside the submodule,\n \tmove the git directory of the submodule into its superproject's\n-\t`$GIT_DIR/modules` path and then connect the git directory and\n+\t`$GIT_DIR/submodules` path and then connect the git directory and\n \tits working directory by setting the `core.worktree` and adding\n \ta .git file pointing to the git directory embedded in the\n \tsuperprojects git directory.\ndiff --git a/Documentation/gitsubmodules.adoc b/Documentation/gitsubmodules.adoc\nindex 2082296199..c6b07847bb 100644\n--- a/Documentation/gitsubmodules.adoc\n+++ b/Documentation/gitsubmodules.adoc\n@@ -22,12 +22,12 @@ The submodule has its own history; the repository it is embedded\n in is called a superproject.\n \n On the filesystem, a submodule usually (but not always - see FORMS below)\n-consists of (i) a Git directory located under the `$GIT_DIR/modules/`\n+consists of (i) a Git directory located under the `$GIT_DIR/submodules/`\n directory of its superproject, (ii) a working directory inside the\n superproject's working directory, and a `.git` file at the root of\n the submodule's working directory pointing to (i).\n \n-Assuming the submodule has a Git directory at `$GIT_DIR/modules/foo/`\n+Assuming the submodule has a Git directory at `$GIT_DIR/submodules/foo/`\n and a working directory at `path/to/bar/`, the superproject tracks the\n submodule via a `gitlink` entry in the tree at `path/to/bar` and an entry\n in its `.gitmodules` file (see linkgit:gitmodules[5]) of the form\n@@ -138,7 +138,7 @@ using older versions of Git.\n It is possible to construct these old form repositories manually.\n +\n When deinitialized or deleted (see below), the submodule's Git\n-directory is automatically moved to `$GIT_DIR/modules/<name>/`\n+directory is automatically moved to `$GIT_DIR/submodules/<name>/`\n of the superproject.\n \n  * Deinitialized submodule: A `gitlink`, and a `.gitmodules` entry,\n@@ -163,7 +163,7 @@ possible to checkout past commits without requiring fetching\n from another repository.\n +\n To completely remove a submodule, manually delete\n-`$GIT_DIR/modules/<name>/`.\n+`$GIT_DIR/submodules/<name>/`.\n \n ACTIVE SUBMODULES\n -----------------\ndiff --git a/setup.c b/setup.c\nindex 7086741e6c..433beebc51 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1416,7 +1416,7 @@ static int is_implicit_bare_repo(const char *path)\n \t * we are inside $GIT_DIR of a worktree of a non-embedded\n \t * submodule, whose superproject is not a bare repository.\n \t */\n-\tif (strstr(path, \"/.git/modules/\"))\n+\tif (strstr(path, \"/.git/modules/\") || strstr(path, \"/.git/submodules/\"))\n \t\treturn 1;\n \n \treturn 0;\ndiff --git a/submodule.c b/submodule.c\nindex fff3c75570..dbf2244e60 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -1278,22 +1278,29 @@ void check_for_new_submodule_commits(struct object_id *oid)\n \n /*\n  * Returns 1 if there is at least one submodule gitdir in\n- * $GIT_DIR/modules and 0 otherwise. This follows\n+ * $GIT_DIR/(sub)modules and 0 otherwise. This follows\n  * submodule_name_to_gitdir(), which looks for submodules in\n- * $GIT_DIR/modules, not $GIT_COMMON_DIR.\n+ * $GIT_DIR/(sub)modules, not $GIT_COMMON_DIR.\n  *\n- * A submodule can be moved to $GIT_DIR/modules manually by running \"git\n- * submodule absorbgitdirs\", or it may be initialized there by \"git\n- * submodule update\".\n+ * A submodule can be moved to $GIT_DIR/(sub)modules manually by running\n+ * \"git submodule absorbgitdirs\", or it may be initialized there by\n+ * \"git submodule update\".\n  */\n static int repo_has_absorbed_submodules(struct repository *r)\n {\n \tint ret;\n \tstruct strbuf buf = STRBUF_INIT;\n \n+\t/* check legacy path */\n \trepo_git_path_append(r, &buf, \"modules/\");\n \tret = file_exists(buf.buf) && !is_empty_dir(buf.buf);\n+\tstrbuf_reset(&buf);\n+\n+\t/* new (encoded name) path */\n+\trepo_git_path_append(r, &buf, \"submodules/\");\n+\tret |= file_exists(buf.buf) && !is_empty_dir(buf.buf);\n \tstrbuf_release(&buf);\n+\n \treturn ret;\n }\n \n@@ -2273,7 +2280,7 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \t *\n \t * Example: having a submodule named `hippo` and another one named\n \t * `hippo/hooks` would result in the git directories\n-\t * `.git/modules/hippo/` and `.git/modules/hippo/hooks/`, respectively,\n+\t * `.git/submodules/hippo/` and `.git/submodules/hippo/hooks/`, respectively,\n \t * but the latter directory is already designated to contain the hooks\n \t * of the former.\n \t */\n@@ -2604,6 +2611,15 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t * administrators can explicitly set. Nothing has been decided,\n \t * so for now, just append the name at the end of the path.\n \t */\n+\n+\t/* Legacy behavior: allow existing paths under modules/<name>. */\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n+\tif (!access(buf->buf, F_OK))\n+\t\treturn;\n+\n+\t/* New style (encoded) paths go under submodules/<encoded>. */\n+\tstrbuf_reset(buf);\n+\trepo_git_path_append(r, buf, \"submodules/\");\n+\tstrbuf_addstr(buf, submodule_name);\n }\ndiff --git a/t/lib-submodule-update.sh b/t/lib-submodule-update.sh\nindex 36f767cb74..b6b2be1df5 100644\n--- a/t/lib-submodule-update.sh\n+++ b/t/lib-submodule-update.sh\n@@ -161,7 +161,7 @@ replace_gitfile_with_git_dir () {\n }\n \n # Test that the .git directory in the submodule is unchanged (except for the\n-# core.worktree setting, which appears only in $GIT_DIR/modules/$1/config).\n+# core.worktree setting, which appears only in $GIT_DIR/submodules/$1/config).\n # Call this function before test_submodule_content as the latter might\n # write the index file leading to false positive index differences.\n #\n@@ -170,23 +170,23 @@ replace_gitfile_with_git_dir () {\n test_git_directory_is_unchanged () {\n \t# does core.worktree point at the right place?\n \techo \"../../../$1\" >expect &&\n-\tgit -C \".git/modules/$1\" config core.worktree >actual &&\n+\tgit -C \".git/submodules/$1\" config core.worktree >actual &&\n \ttest_cmp expect actual &&\n \t# remove it temporarily before comparing, as\n \t# \"$1/.git/config\" lacks it...\n-\tgit -C \".git/modules/$1\" config --unset core.worktree &&\n-\tdiff -r \".git/modules/$1\" \"$1/.git\" &&\n+\tgit -C \".git/submodules/$1\" config --unset core.worktree &&\n+\tdiff -r \".git/submodules/$1\" \"$1/.git\" &&\n \t# ... and then restore.\n-\tgit -C \".git/modules/$1\" config core.worktree \"../../../$1\"\n+\tgit -C \".git/submodules/$1\" config core.worktree \"../../../$1\"\n }\n \n test_git_directory_exists () {\n-\ttest -e \".git/modules/$1\" &&\n+\ttest -e \".git/submodules/$1\" &&\n \tif test -f sub1/.git\n \tthen\n \t\t# does core.worktree point at the right place?\n \t\techo \"../../../$1\" >expect &&\n-\t\tgit -C \".git/modules/$1\" config core.worktree >actual &&\n+\t\tgit -C \".git/submodules/$1\" config core.worktree >actual &&\n \t\ttest_cmp expect actual\n \tfi\n }\n@@ -225,22 +225,22 @@ reset_work_tree_to () {\n reset_work_tree_to_interested () {\n \treset_work_tree_to $1 &&\n \t# make the submodule git dirs available\n-\tif ! test -d submodule_update/.git/modules/sub1\n+\tif ! test -d submodule_update/.git/submodules/sub1\n \tthen\n-\t\tmkdir -p submodule_update/.git/modules &&\n-\t\tcp -r submodule_update_repo/.git/modules/sub1 submodule_update/.git/modules/sub1\n-\t\tGIT_WORK_TREE=. git -C submodule_update/.git/modules/sub1 config --unset core.worktree\n+\t\tmkdir -p submodule_update/.git/submodules &&\n+\t\tcp -r submodule_update_repo/.git/submodules/sub1 submodule_update/.git/submodules/sub1\n+\t\tGIT_WORK_TREE=. git -C submodule_update/.git/submodules/sub1 config --unset core.worktree\n \tfi &&\n-\tif ! test -d submodule_update/.git/modules/sub1/modules/sub2\n+\tif ! test -d submodule_update/.git/submodules/sub1/submodules/sub2\n \tthen\n-\t\tmkdir -p submodule_update/.git/modules/sub1/modules &&\n-\t\tcp -r submodule_update_repo/.git/modules/sub1/modules/sub2 submodule_update/.git/modules/sub1/modules/sub2\n+\t\tmkdir -p submodule_update/.git/submodules/sub1/submodules &&\n+\t\tcp -r submodule_update_repo/.git/submodules/sub1/submodules/sub2 submodule_update/.git/submodules/sub1/submodules/sub2\n \t\t# core.worktree is unset for sub2 as it is not checked out\n \tfi &&\n \t# indicate we are interested in the submodule:\n \tgit -C submodule_update config submodule.sub1.url \"bogus\" &&\n \t# sub1 might not be checked out, so use the git dir\n-\tgit -C submodule_update/.git/modules/sub1 config submodule.sub2.url \"bogus\"\n+\tgit -C submodule_update/.git/submodules/sub1 config submodule.sub2.url \"bogus\"\n }\n \n # Test that the superproject contains the content according to commit \"$1\"\n@@ -742,7 +742,7 @@ test_submodule_recursing_with_args_common () {\n \t\t\t$command remove_sub1 &&\n \t\t\ttest_superproject_content origin/remove_sub1 &&\n \t\t\t! test -e sub1 &&\n-\t\t\ttest_must_fail git config -f .git/modules/sub1/config core.worktree\n+\t\t\ttest_must_fail git config -f .git/submodules/sub1/config core.worktree\n \t\t)\n \t'\n \t# ... absorbing a .git directory along the way.\n@@ -753,7 +753,7 @@ test_submodule_recursing_with_args_common () {\n \t\t\tcd submodule_update &&\n \t\t\tgit branch -t remove_sub1 origin/remove_sub1 &&\n \t\t\treplace_gitfile_with_git_dir sub1 &&\n-\t\t\trm -rf .git/modules &&\n+\t\t\trm -rf .git/submodules &&\n \t\t\t$command remove_sub1 &&\n \t\t\ttest_superproject_content origin/remove_sub1 &&\n \t\t\t! test -e sub1 &&\n@@ -803,8 +803,8 @@ test_submodule_recursing_with_args_common () {\n \t\t\t$command no_submodule &&\n \t\t\ttest_superproject_content origin/no_submodule &&\n \t\t\ttest_path_is_missing sub1 &&\n-\t\t\ttest_must_fail git config -f .git/modules/sub1/config core.worktree &&\n-\t\t\ttest_must_fail git config -f .git/modules/sub1/modules/sub2/config core.worktree\n+\t\t\ttest_must_fail git config -f .git/submodules/sub1/config core.worktree &&\n+\t\t\ttest_must_fail git config -f .git/submodules/sub1/submodules/sub2/config core.worktree\n \t\t)\n \t'\n \n@@ -937,7 +937,7 @@ test_submodule_switch_recursing_with_args () {\n \t\t\tcd submodule_update &&\n \t\t\tgit branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&\n \t\t\treplace_gitfile_with_git_dir sub1 &&\n-\t\t\trm -rf .git/modules &&\n+\t\t\trm -rf .git/submodules &&\n \t\t\t$command replace_sub1_with_directory &&\n \t\t\ttest_superproject_content origin/replace_sub1_with_directory &&\n \t\t\ttest_git_directory_exists sub1\n@@ -946,15 +946,15 @@ test_submodule_switch_recursing_with_args () {\n \n \t# ... and ignored files are ignored\n \ttest_expect_success \"$command: replace submodule with a file works ignores ignored files in submodule\" '\n-\t\ttest_when_finished \"rm submodule_update/.git/modules/sub1/info/exclude\" &&\n+\t\ttest_when_finished \"rm submodule_update/.git/submodules/sub1/info/exclude\" &&\n \t\tprolog &&\n \t\treset_work_tree_to_interested add_sub1 &&\n \t\t(\n \t\t\tcd submodule_update &&\n-\t\t\trm -rf .git/modules/sub1/info &&\n+\t\t\trm -rf .git/submodules/sub1/info &&\n \t\t\tgit branch -t replace_sub1_with_file origin/replace_sub1_with_file &&\n-\t\t\tmkdir .git/modules/sub1/info &&\n-\t\t\techo ignored >.git/modules/sub1/info/exclude &&\n+\t\t\tmkdir .git/submodules/sub1/info &&\n+\t\t\techo ignored >.git/submodules/sub1/info/exclude &&\n \t\t\t: >sub1/ignored &&\n \t\t\t$command replace_sub1_with_file &&\n \t\t\ttest_superproject_content origin/replace_sub1_with_file &&\n@@ -1034,7 +1034,7 @@ test_submodule_forced_switch_recursing_with_args () {\n \t\t\tcd submodule_update &&\n \t\t\tgit branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&\n \t\t\treplace_gitfile_with_git_dir sub1 &&\n-\t\t\trm -rf .git/modules/sub1 &&\n+\t\t\trm -rf .git/submodules/sub1 &&\n \t\t\t$command replace_sub1_with_directory &&\n \t\t\ttest_superproject_content origin/replace_sub1_with_directory &&\n \t\t\ttest_git_directory_exists sub1\ndiff --git a/t/t0035-safe-bare-repository.sh b/t/t0035-safe-bare-repository.sh\nindex ae7ef092ab..a480ddf8d6 100755\n--- a/t/t0035-safe-bare-repository.sh\n+++ b/t/t0035-safe-bare-repository.sh\n@@ -41,7 +41,7 @@ test_expect_success 'setup an embedded bare repo, secondary worktree and submodu\n \t\t\tsubmodule add --name subn -- ./bare-repo subd\n \t) &&\n \ttest_path_is_dir outer-repo/.git/worktrees/outer-secondary &&\n-\ttest_path_is_dir outer-repo/.git/modules/subn\n+\ttest_path_is_dir outer-repo/.git/submodules/subn\n '\n \n test_expect_success 'safe.bareRepository unset' '\n@@ -100,7 +100,7 @@ test_expect_success 'no trace in $GIT_DIR of secondary worktree' '\n '\n \n test_expect_success 'no trace in $GIT_DIR of a submodule' '\n-\texpect_accepted_implicit -C outer-repo/.git/modules/subn\n+\texpect_accepted_implicit -C outer-repo/.git/submodules/subn\n '\n \n test_done\ndiff --git a/t/t1600-index.sh b/t/t1600-index.sh\nindex 03239e9faa..0e5e8efb20 100755\n--- a/t/t1600-index.sh\n+++ b/t/t1600-index.sh\n@@ -87,10 +87,10 @@ test_expect_success 'index.skipHash config option' '\n \tgit -c protocol.file.allow=always submodule add ./ sub &&\n \tgit config index.skipHash false &&\n \tgit -C sub config index.skipHash true &&\n-\trm -f .git/modules/sub/index &&\n+\trm -f .git/submodules/sub/index &&\n \t>sub/file &&\n \tgit -C sub add a &&\n-\ttest_trailing_hash .git/modules/sub/index >hash &&\n+\ttest_trailing_hash .git/submodules/sub/index >hash &&\n \ttest_cmp expect hash &&\n \tgit -C sub fsck\n '\ndiff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\nindex 11018f37c7..c18c2efca5 100755\n--- a/t/t2405-worktree-submodule.sh\n+++ b/t/t2405-worktree-submodule.sh\n@@ -62,7 +62,7 @@ test_expect_success 'submodule is checked out after manually adding submodule wo\n test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules in a linked worktree' '\n \tgit -C main worktree add \"$base_path/checkout-recurse\" --detach  &&\n \tgit -C checkout-recurse submodule update --init &&\n-\techo \"gitdir: ../../main/.git/worktrees/checkout-recurse/modules/sub\" >expect-gitfile &&\n+\techo \"gitdir: ../../main/.git/worktrees/checkout-recurse/submodules/sub\" >expect-gitfile &&\n \tcat checkout-recurse/sub/.git >actual-gitfile &&\n \ttest_cmp expect-gitfile actual-gitfile &&\n \tgit -C main/sub rev-parse HEAD >expect-head-main &&\n@@ -73,7 +73,7 @@ test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules\n \ttest_cmp expect-head-main actual-head-main\n '\n \n-test_expect_success 'core.worktree is removed in $GIT_DIR/modules/<name>/config, not in $GIT_COMMON_DIR/modules/<name>/config' '\n+test_expect_success 'core.worktree is removed in $GIT_DIR/submodules/<name>/config, not in $GIT_COMMON_DIR/submodules/<name>/config' '\n \techo \"../../../sub\" >expect-main &&\n \tgit -C main/sub config --get core.worktree >actual-main &&\n \ttest_cmp expect-main actual-main &&\n@@ -81,14 +81,14 @@ test_expect_success 'core.worktree is removed in $GIT_DIR/modules/<name>/config,\n \tgit -C checkout-recurse/sub config --get core.worktree >actual-linked &&\n \ttest_cmp expect-linked actual-linked &&\n \tgit -C checkout-recurse checkout --recurse-submodules first &&\n-\ttest_expect_code 1 git -C main/.git/worktrees/checkout-recurse/modules/sub config --get core.worktree >linked-config &&\n+\ttest_expect_code 1 git -C main/.git/worktrees/checkout-recurse/submodules/sub config --get core.worktree >linked-config &&\n \ttest_must_be_empty linked-config &&\n \tgit -C main/sub config --get core.worktree >actual-main &&\n \ttest_cmp expect-main actual-main\n '\n \n test_expect_success 'unsetting core.worktree does not prevent running commands directly against the submodule repository' '\n-\tgit -C main/.git/worktrees/checkout-recurse/modules/sub log\n+\tgit -C main/.git/worktrees/checkout-recurse/submodules/sub log\n '\n \n test_done\ndiff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\nindex be9140bbaa..bb8751433f 100755\n--- a/t/t2501-cwd-empty.sh\n+++ b/t/t2501-cwd-empty.sh\n@@ -239,7 +239,7 @@ test_submodule_removal () {\n \ttest \"$path_status\" = dir && test_status=test_must_fail\n \n \ttest_when_finished \"git reset --hard HEAD~1\" &&\n-\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n+\ttest_when_finished \"rm -rf .git/submodules/my_submodule\" &&\n \n \tgit checkout foo/bar/baz &&\n \ndiff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\nindex 1f16e6b522..5b8ed57538 100755\n--- a/t/t3600-rm.sh\n+++ b/t/t3600-rm.sh\n@@ -582,7 +582,7 @@ test_expect_success 'rm of a conflicted populated submodule with a .git director\n \t(\n \t\tcd submod &&\n \t\trm .git &&\n-\t\tcp -R ../.git/modules/sub .git &&\n+\t\tcp -R ../.git/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \ttest_must_fail git merge conflict2 &&\n@@ -617,9 +617,9 @@ test_expect_success 'rm of a populated submodule with a .git directory migrates\n \t(\n \t\tcd submod &&\n \t\trm .git &&\n-\t\tcp -R ../.git/modules/sub .git &&\n+\t\tcp -R ../.git/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree &&\n-\t\trm -r ../.git/modules/sub\n+\t\trm -r ../.git/submodules/sub\n \t) &&\n \tgit rm submod 2>output.err &&\n \ttest_path_is_missing submod &&\n@@ -709,7 +709,7 @@ test_expect_success \"rm absorbs submodule's nested .git directory\" '\n \t(\n \t\tcd submod/subsubmod &&\n \t\trm .git &&\n-\t\tmv ../../.git/modules/sub/modules/sub .git &&\n+\t\tmv ../../.git/submodules/sub/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \tgit rm submod 2>output.err &&\ndiff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh\nindex 5e566205ba..b7385bc088 100755\n--- a/t/t5526-fetch-submodules.sh\n+++ b/t/t5526-fetch-submodules.sh\n@@ -1143,7 +1143,7 @@ test_expect_success 'fetch --recurse-submodules updates name-conflicted, unpopul\n \thead1=$(git -C same-name-1/submodule rev-parse HEAD) &&\n \thead2=$(git -C same-name-2/submodule rev-parse HEAD) &&\n \t(\n-\t\tcd same-name-downstream/.git/modules/submodule &&\n+\t\tcd same-name-downstream/.git/submodules/submodule &&\n \t\t# The submodule has core.worktree pointing to the \"git\n \t\t# rm\"-ed directory, overwrite the invalid value. See\n \t\t# comment in get_fetch_task_from_changed() for more\ndiff --git a/t/t5619-clone-local-ambiguous-transport.sh b/t/t5619-clone-local-ambiguous-transport.sh\nindex cce62bf78d..cf2d5e7bfb 100755\n--- a/t/t5619-clone-local-ambiguous-transport.sh\n+++ b/t/t5619-clone-local-ambiguous-transport.sh\n@@ -38,7 +38,7 @@ test_expect_success 'setup' '\n \t\tln -s \"$(cd .. && pwd)/sensitive\" repo/objects &&\n \n \t\tmkdir -p \"$HTTPD_URL/dumb\" &&\n-\t\tln -s \"../../../.git/modules/sub/../../../repo/\" \"$URI\" &&\n+\t\tln -s \"../../../.git/submodules/sub/../../../repo/\" \"$URI\" &&\n \n \t\tgit add . &&\n \t\tgit commit -m \"initial commit\"\n@@ -57,7 +57,7 @@ test_expect_success 'ambiguous transport does not lead to arbitrary file-inclusi\n \tgit clone malicious clone &&\n \ttest_must_fail git -C clone submodule update --init 2>err &&\n \n-\ttest_path_is_missing clone/.git/modules/sub/objects/secret &&\n+\ttest_path_is_missing clone/.git/submodules/sub/objects/secret &&\n \t# We would actually expect \"transport .file. not allowed\" here,\n \t# but due to quirks of the URL detection in Git, we mis-parse\n \t# the absolute path as a bogus URL and die before that step.\ndiff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\nindex 2c70cc561a..e7e8127130 100755\n--- a/t/t6120-describe.sh\n+++ b/t/t6120-describe.sh\n@@ -357,7 +357,7 @@ test_expect_success 'setup and absorb a submodule' '\n '\n \n test_expect_success 'describe chokes on severely broken submodules' '\n-\tmv .git/modules/sub1/ .git/modules/sub_moved &&\n+\tmv .git/submodules/sub1/ .git/submodules/sub_moved &&\n \ttest_must_fail git describe --dirty\n '\n \n@@ -371,7 +371,7 @@ test_expect_success 'describe with --work-tree ignoring a broken submodule' '\n \t\tcd \"$TEST_DIRECTORY\" &&\n \t\tgit --git-dir \"$TRASH_DIRECTORY/.git\" --work-tree \"$TRASH_DIRECTORY\" describe --broken >\"$TRASH_DIRECTORY/out\"\n \t) &&\n-\ttest_when_finished \"mv .git/modules/sub_moved .git/modules/sub1\" &&\n+\ttest_when_finished \"mv .git/submodules/sub_moved .git/submodules/sub1\" &&\n \tgrep broken out\n '\n \ndiff --git a/t/t7001-mv.sh b/t/t7001-mv.sh\nindex 920479e925..89b06ae3c1 100755\n--- a/t/t7001-mv.sh\n+++ b/t/t7001-mv.sh\n@@ -360,7 +360,7 @@ test_expect_success 'git mv moves a submodule with a .git directory and no .gitm\n \t(\n \t\tcd sub &&\n \t\trm -f .git &&\n-\t\tcp -R -P -p ../.git/modules/sub .git &&\n+\t\tcp -R -P -p ../.git/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \tmkdir mod &&\n@@ -380,7 +380,7 @@ test_expect_success 'git mv moves a submodule with a .git directory and .gitmodu\n \t(\n \t\tcd sub &&\n \t\trm -f .git &&\n-\t\tcp -R -P -p ../.git/modules/sub .git &&\n+\t\tcp -R -P -p ../.git/submodules/sub .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \tmkdir mod &&\ndiff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\nindex fd3e7e355e..178c386212 100755\n--- a/t/t7400-submodule-basic.sh\n+++ b/t/t7400-submodule-basic.sh\n@@ -163,7 +163,7 @@ test_expect_success 'submodule add' '\n \t\tcd addtest &&\n \t\tgit submodule add -q \"$submodurl\" submod >actual &&\n \t\ttest_must_be_empty actual &&\n-\t\techo \"gitdir: ../.git/modules/submod\" >expect &&\n+\t\techo \"gitdir: ../.git/submodules/submod\" >expect &&\n \t\ttest_cmp expect submod/.git &&\n \t\t(\n \t\t\tcd submod &&\n@@ -976,21 +976,21 @@ test_expect_success 'submodule add --name allows to replace a submodule with ano\n \t\t\techo \"$submodurl/repo\" >expect &&\n \t\t\tgit config remote.origin.url >actual &&\n \t\t\ttest_cmp expect actual &&\n-\t\t\techo \"gitdir: ../.git/modules/repo\" >expect &&\n+\t\t\techo \"gitdir: ../.git/submodules/repo\" >expect &&\n \t\t\ttest_cmp expect .git\n \t\t) &&\n \t\trm -rf repo &&\n \t\tgit rm repo &&\n \t\tgit submodule add -q --name repo_new \"$submodurl/bare.git\" repo >actual &&\n \t\ttest_must_be_empty actual &&\n-\t\techo \"gitdir: ../.git/modules/submod\" >expect &&\n+\t\techo \"gitdir: ../.git/submodules/submod\" >expect &&\n \t\ttest_cmp expect submod/.git &&\n \t\t(\n \t\t\tcd repo &&\n \t\t\techo \"$submodurl/bare.git\" >expect &&\n \t\t\tgit config remote.origin.url >actual &&\n \t\t\ttest_cmp expect actual &&\n-\t\t\techo \"gitdir: ../.git/modules/repo_new\" >expect &&\n+\t\t\techo \"gitdir: ../.git/submodules/repo_new\" >expect &&\n \t\t\ttest_cmp expect .git\n \t\t) &&\n \t\techo \"repo\" >expect &&\n@@ -1045,8 +1045,8 @@ test_expect_success 'recursive relative submodules stay relative' '\n \t(\n \t\tcd clone2 &&\n \t\tgit submodule update --init --recursive &&\n-\t\techo \"gitdir: ../.git/modules/sub3\" >./sub3/.git_expect &&\n-\t\techo \"gitdir: ../../../.git/modules/sub3/modules/dirdir/subsub\" >./sub3/dirdir/subsub/.git_expect\n+\t\techo \"gitdir: ../.git/submodules/sub3\" >./sub3/.git_expect &&\n+\t\techo \"gitdir: ../../../.git/submodules/sub3/submodules/dirdir/subsub\" >./sub3/dirdir/subsub/.git_expect\n \t) &&\n \ttest_cmp clone2/sub3/.git_expect clone2/sub3/.git &&\n \ttest_cmp clone2/sub3/dirdir/subsub/.git_expect clone2/sub3/dirdir/subsub/.git\n@@ -1108,8 +1108,8 @@ test_expect_success 'submodule deinit should remove the whole submodule section\n '\n \n test_expect_success 'submodule deinit should unset core.worktree' '\n-\ttest_path_is_file .git/modules/example/config &&\n-\ttest_must_fail git config -f .git/modules/example/config core.worktree\n+\ttest_path_is_file .git/submodules/example/config &&\n+\ttest_must_fail git config -f .git/submodules/example/config core.worktree\n '\n \n test_expect_success 'submodule deinit from subdirectory' '\n@@ -1231,7 +1231,7 @@ test_expect_success 'submodule deinit absorbs .git directory if .git is a direct\n \t(\n \t\tcd init &&\n \t\trm .git &&\n-\t\tmv ../.git/modules/example .git &&\n+\t\tmv ../.git/submodules/example .git &&\n \t\tGIT_WORK_TREE=. git config --unset core.worktree\n \t) &&\n \tgit submodule deinit init &&\ndiff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh\nindex 3adab12091..f0c4da1ffa 100755\n--- a/t/t7406-submodule-update.sh\n+++ b/t/t7406-submodule-update.sh\n@@ -864,7 +864,7 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir' '\n \t (cd deeper/submodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/modules/deeper/submodule &&\n+\t (cd .git/submodules/deeper/submodule &&\n \t  git log > ../../../../actual\n \t ) &&\n \t test_cmp expected actual\n@@ -882,7 +882,7 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir' '\n \t (cd deeper/submodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/modules/deeper/submodule &&\n+\t (cd .git/submodules/deeper/submodule &&\n \t  git log > ../../../../actual\n \t ) &&\n \t test_cmp expected actual\n@@ -899,7 +899,7 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir recur\n \t  git commit -m \"added subsubmodule\" &&\n \t  git push origin :\n \t ) &&\n-\t (cd .git/modules/deeper/submodule/modules/subsubmodule &&\n+\t (cd .git/submodules/deeper/submodule/submodules/subsubmodule &&\n \t  git log > ../../../../../actual\n \t ) &&\n \t git add deeper/submodule &&\n@@ -949,7 +949,7 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir re\n \t (cd submodule/subsubmodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/modules/submodule/modules/subsubmodule &&\n+\t (cd .git/submodules/submodule/submodules/subsubmodule &&\n \t  git log > ../../../../../actual\n \t ) &&\n \t test_cmp expected actual\n@@ -1298,7 +1298,7 @@ test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \\\n \tgit init captain &&\n \t(\n \t\tcd captain &&\n-\t\tgit submodule add --name x/y \"$hook_repo_path\" A/modules/x &&\n+\t\tgit submodule add --name x/y \"$hook_repo_path\" A/submodules/x &&\n \t\ttest_tick &&\n \t\tgit commit -m add-submodule &&\n \ndiff --git a/t/t7407-submodule-foreach.sh b/t/t7407-submodule-foreach.sh\nindex 77b6d0040e..75ba826968 100755\n--- a/t/t7407-submodule-foreach.sh\n+++ b/t/t7407-submodule-foreach.sh\n@@ -368,9 +368,9 @@ test_expect_success 'test \"update --recursive\" with a flag with spaces' '\n \t\tgit rev-parse --resolve-git-dir nested1/.git &&\n \t\tgit rev-parse --resolve-git-dir nested1/nested2/.git &&\n \t\tgit rev-parse --resolve-git-dir nested1/nested2/nested3/.git &&\n-\t\ttest -f .git/modules/nested1/objects/info/alternates &&\n-\t\ttest -f .git/modules/nested1/modules/nested2/objects/info/alternates &&\n-\t\ttest -f .git/modules/nested1/modules/nested2/modules/nested3/objects/info/alternates\n+\t\ttest -f .git/submodules/nested1/objects/info/alternates &&\n+\t\ttest -f .git/submodules/nested1/submodules/nested2/objects/info/alternates &&\n+\t\ttest -f .git/submodules/nested1/submodules/nested2/submodules/nested3/objects/info/alternates\n \t)\n '\n \ndiff --git a/t/t7408-submodule-reference.sh b/t/t7408-submodule-reference.sh\nindex f860e7bbf4..25f4aec57e 100755\n--- a/t/t7408-submodule-reference.sh\n+++ b/t/t7408-submodule-reference.sh\n@@ -61,7 +61,7 @@ test_expect_success 'submodule add --reference uses alternates' '\n \t\tgit commit -m B-super-added &&\n \t\tgit repack -ad\n \t) &&\n-\ttest_alternate_is_used super/.git/modules/sub/objects/info/alternates super/sub\n+\ttest_alternate_is_used super/.git/submodules/sub/objects/info/alternates super/sub\n '\n \n test_expect_success 'submodule add --reference with --dissociate does not use alternates' '\n@@ -71,7 +71,7 @@ test_expect_success 'submodule add --reference with --dissociate does not use al\n \t\tgit commit -m B-super-added &&\n \t\tgit repack -ad\n \t) &&\n-\ttest_path_is_missing super/.git/modules/sub-dissociate/objects/info/alternates\n+\ttest_path_is_missing super/.git/submodules/sub-dissociate/objects/info/alternates\n '\n \n test_expect_success 'that reference gets used with add' '\n@@ -94,14 +94,14 @@ test_expect_success 'updating superproject keeps alternates' '\n \ttest_when_finished \"rm -rf super-clone\" &&\n \tgit clone super super-clone &&\n \tgit -C super-clone submodule update --init --reference ../B &&\n-\ttest_alternate_is_used super-clone/.git/modules/sub/objects/info/alternates super-clone/sub\n+\ttest_alternate_is_used super-clone/.git/submodules/sub/objects/info/alternates super-clone/sub\n '\n \n test_expect_success 'updating superproject with --dissociate does not keep alternates' '\n \ttest_when_finished \"rm -rf super-clone\" &&\n \tgit clone super super-clone &&\n \tgit -C super-clone submodule update --init --reference ../B --dissociate &&\n-\ttest_path_is_missing super-clone/.git/modules/sub/objects/info/alternates\n+\ttest_path_is_missing super-clone/.git/submodules/sub/objects/info/alternates\n '\n \n test_expect_success 'submodules use alternates when cloning a superproject' '\n@@ -112,7 +112,7 @@ test_expect_success 'submodules use alternates when cloning a superproject' '\n \t\t# test superproject has alternates setup correctly\n \t\ttest_alternate_is_used .git/objects/info/alternates . &&\n \t\t# test submodule has correct setup\n-\t\ttest_alternate_is_used .git/modules/sub/objects/info/alternates sub\n+\t\ttest_alternate_is_used .git/submodules/sub/objects/info/alternates sub\n \t)\n '\n \n@@ -127,7 +127,7 @@ test_expect_success 'missing submodule alternate fails clone and submodule updat\n \t\t# update of the submodule succeeds\n \t\ttest_must_fail git submodule update --init &&\n \t\t# and we have no alternates:\n-\t\ttest_path_is_missing .git/modules/sub/objects/info/alternates &&\n+\t\ttest_path_is_missing .git/submodules/sub/objects/info/alternates &&\n \t\ttest_path_is_missing sub/file1\n \t)\n '\n@@ -142,7 +142,7 @@ test_expect_success 'ignoring missing submodule alternates passes clone and subm\n \t\t# update of the submodule succeeds\n \t\tgit submodule update --init &&\n \t\t# and we have no alternates:\n-\t\ttest_path_is_missing .git/modules/sub/objects/info/alternates &&\n+\t\ttest_path_is_missing .git/submodules/sub/objects/info/alternates &&\n \t\ttest_path_is_file sub/file1\n \t)\n '\n@@ -176,18 +176,18 @@ test_expect_success 'nested submodule alternate in works and is actually used' '\n \t\t# test superproject has alternates setup correctly\n \t\ttest_alternate_is_used .git/objects/info/alternates . &&\n \t\t# immediate submodule has alternate:\n-\t\ttest_alternate_is_used .git/modules/subwithsub/objects/info/alternates subwithsub &&\n+\t\ttest_alternate_is_used .git/submodules/subwithsub/objects/info/alternates subwithsub &&\n \t\t# nested submodule also has alternate:\n-\t\ttest_alternate_is_used .git/modules/subwithsub/modules/sub/objects/info/alternates subwithsub/sub\n+\t\ttest_alternate_is_used .git/submodules/subwithsub/submodules/sub/objects/info/alternates subwithsub/sub\n \t)\n '\n \n check_that_two_of_three_alternates_are_used() {\n \ttest_alternate_is_used .git/objects/info/alternates . &&\n \t# immediate submodule has alternate:\n-\ttest_alternate_is_used .git/modules/subwithsub/objects/info/alternates subwithsub &&\n+\ttest_alternate_is_used .git/submodules/subwithsub/objects/info/alternates subwithsub &&\n \t# but nested submodule has no alternate:\n-\ttest_path_is_missing .git/modules/subwithsub/modules/sub/objects/info/alternates\n+\ttest_path_is_missing .git/submodules/subwithsub/submodules/sub/objects/info/alternates\n }\n \n \ndiff --git a/t/t7412-submodule-absorbgitdirs.sh b/t/t7412-submodule-absorbgitdirs.sh\nindex 0490499573..dbaca9c69f 100755\n--- a/t/t7412-submodule-absorbgitdirs.sh\n+++ b/t/t7412-submodule-absorbgitdirs.sh\n@@ -29,13 +29,13 @@ test_expect_success 'absorb the git dir' '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''sub1'\\'' from\n \t'\\''$cwd/sub1/.git'\\'' to\n-\t'\\''$cwd/.git/modules/sub1'\\''\n+\t'\\''$cwd/.git/submodules/sub1'\\''\n \tEOF\n \tgit submodule absorbgitdirs 2>actual &&\n \ttest_cmp expect actual &&\n \tgit fsck &&\n \ttest -f sub1/.git &&\n-\ttest -d .git/modules/sub1 &&\n+\ttest -d .git/submodules/sub1 &&\n \tgit status >actual.1 &&\n \tgit -C sub1 rev-parse HEAD >actual.2 &&\n \ttest_cmp expect.1 actual.1 &&\n@@ -47,7 +47,7 @@ test_expect_success 'absorbing does not fail for deinitialized submodules' '\n \tgit submodule deinit --all &&\n \tgit submodule absorbgitdirs 2>err &&\n \ttest_must_be_empty err &&\n-\ttest -d .git/modules/sub1 &&\n+\ttest -d .git/submodules/sub1 &&\n \ttest -d sub1 &&\n \t! test -e sub1/.git\n '\n@@ -68,12 +68,12 @@ test_expect_success 'absorb the git dir in a nested submodule' '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''sub1/nested'\\'' from\n \t'\\''$cwd/sub1/nested/.git'\\'' to\n-\t'\\''$cwd/.git/modules/sub1/modules/nested'\\''\n+\t'\\''$cwd/.git/submodules/sub1/submodules/nested'\\''\n \tEOF\n \tgit submodule absorbgitdirs 2>actual &&\n \ttest_cmp expect actual &&\n \ttest -f sub1/nested/.git &&\n-\ttest -d .git/modules/sub1/modules/nested &&\n+\ttest -d .git/submodules/sub1/submodules/nested &&\n \tgit status >actual.1 &&\n \tgit -C sub1/nested rev-parse HEAD >actual.2 &&\n \ttest_cmp expect.1 actual.1 &&\n@@ -84,11 +84,11 @@ test_expect_success 're-setup nested submodule' '\n \t# un-absorb the direct submodule, to test if the nested submodule\n \t# is still correct (needs a rewrite of the gitfile only)\n \trm -rf sub1/.git &&\n-\tmv .git/modules/sub1 sub1/.git &&\n+\tmv .git/submodules/sub1 sub1/.git &&\n \tGIT_WORK_TREE=. git -C sub1 config --unset core.worktree &&\n \t# fixup the nested submodule\n-\techo \"gitdir: ../.git/modules/nested\" >sub1/nested/.git &&\n-\tGIT_WORK_TREE=../../../nested git -C sub1/.git/modules/nested config \\\n+\techo \"gitdir: ../.git/submodules/nested\" >sub1/nested/.git &&\n+\tGIT_WORK_TREE=../../../nested git -C sub1/.git/submodules/nested config \\\n \t\tcore.worktree \"../../../nested\" &&\n \t# make sure this re-setup is correct\n \tgit status --ignore-submodules=none &&\n@@ -105,13 +105,13 @@ test_expect_success 'absorb the git dir in a nested submodule' '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''sub1'\\'' from\n \t'\\''$cwd/sub1/.git'\\'' to\n-\t'\\''$cwd/.git/modules/sub1'\\''\n+\t'\\''$cwd/.git/submodules/sub1'\\''\n \tEOF\n \tgit submodule absorbgitdirs 2>actual &&\n \ttest_cmp expect actual &&\n \ttest -f sub1/.git &&\n \ttest -f sub1/nested/.git &&\n-\ttest -d .git/modules/sub1/modules/nested &&\n+\ttest -d .git/submodules/sub1/submodules/nested &&\n \tgit status >actual.1 &&\n \tgit -C sub1/nested rev-parse HEAD >actual.2 &&\n \ttest_cmp expect.1 actual.1 &&\n@@ -133,7 +133,7 @@ test_expect_success 'absorb the git dir outside of primary worktree' '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''sub2'\\'' from\n \t'\\''$cwd/repo-wt/sub2/.git'\\'' to\n-\t'\\''$cwd/repo-bare.git/worktrees/repo-wt/modules/sub2'\\''\n+\t'\\''$cwd/repo-bare.git/worktrees/repo-wt/submodules/sub2'\\''\n \tEOF\n \tgit -C repo-wt submodule absorbgitdirs 2>actual &&\n \ttest_cmp expect actual\ndiff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh\nindex 3d3c7af3ce..a51235136d 100755\n--- a/t/t7423-submodule-symlinks.sh\n+++ b/t/t7423-submodule-symlinks.sh\n@@ -49,19 +49,19 @@ test_expect_success SYMLINKS 'git restore --recurse-submodules must not be confu\n \n test_expect_success SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' '\n \tprepare_symlink_to_repo &&\n-\trm -rf .git/modules &&\n+\trm -rf .git/submodules &&\n \ttest_must_fail git restore --recurse-submodules a/sm &&\n \ttest_path_is_dir a/target/.git &&\n-\ttest_path_is_missing .git/modules/a/sm &&\n+\ttest_path_is_missing .git/submodules/a/sm &&\n \ttest_path_is_missing a/target/submodule_file\n '\n \n test_expect_success SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' '\n \tprepare_symlink_to_repo &&\n-\trm -rf .git/modules &&\n+\trm -rf .git/submodules &&\n \ttest_must_fail git checkout -f --recurse-submodules initial &&\n \ttest_path_is_dir a/target/.git &&\n-\ttest_path_is_missing .git/modules/a/sm\n+\ttest_path_is_missing .git/submodules/a/sm\n '\n \n test_done\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex f512eed278..4e2ced3636 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -77,28 +77,28 @@ test_expect_success 'create innocent subrepo' '\n \n test_expect_success 'submodule add refuses invalid names' '\n \ttest_must_fail \\\n-\t\tgit submodule add --name ../../modules/evil \"$PWD/innocent\" evil\n+\t\tgit submodule add --name ../../submodules/evil \"$PWD/innocent\" evil\n '\n \n test_expect_success 'add evil submodule' '\n \tgit submodule add \"$PWD/innocent\" evil &&\n \n-\tmkdir modules &&\n-\tcp -r .git/modules/evil modules &&\n-\twrite_script modules/evil/hooks/post-checkout <<-\\EOF &&\n+\tmkdir submodules &&\n+\tcp -r .git/submodules/evil submodules &&\n+\twrite_script submodules/evil/hooks/post-checkout <<-\\EOF &&\n \techo >&2 \"RUNNING POST CHECKOUT\"\n \tEOF\n \n \tgit config -f .gitmodules submodule.evil.update checkout &&\n \tgit config -f .gitmodules --rename-section \\\n-\t\tsubmodule.evil submodule.../../modules/evil &&\n-\tgit add modules &&\n+\t\tsubmodule.evil submodule.../../submodules/evil &&\n+\tgit add submodules &&\n \tgit commit -am evil\n '\n \n # This step seems like it shouldn't be necessary, since the payload is\n # contained entirely in the evil submodule. But due to the vagaries of the\n-# submodule code, checking out the evil module will fail unless \".git/modules\"\n+# submodule code, checking out the evil module will fail unless \".git/submodules\"\n # exists. Adding another submodule (with a name that sorts before \"evil\") is an\n # easy way to make sure this is the case in the victim clone.\n test_expect_success 'add other submodule' '\n@@ -350,8 +350,8 @@ test_expect_success 'submodule git dir nesting detection must work with parallel\n \tcat err &&\n \tgrep -E \"(already exists|is inside git dir|not a git repository)\" err &&\n \t{\n-\t\ttest_path_is_missing .git/modules/hippo/HEAD ||\n-\t\ttest_path_is_missing .git/modules/hippo/hooks/HEAD\n+\t\ttest_path_is_missing .git/submodules/hippo/HEAD ||\n+\t\ttest_path_is_missing .git/submodules/hippo/hooks/HEAD\n \t}\n '\n \n@@ -361,10 +361,10 @@ test_expect_success 'checkout -f --recurse-submodules must not use a nested gitd\n \t\tcd nested_checkout &&\n \t\tgit submodule init &&\n \t\tgit submodule update thing1 &&\n-\t\tmkdir -p .git/modules/hippo/hooks/refs &&\n-\t\tmkdir -p .git/modules/hippo/hooks/objects/info &&\n-\t\techo \"../../../../objects\" >.git/modules/hippo/hooks/objects/info/alternates &&\n-\t\techo \"ref: refs/heads/master\" >.git/modules/hippo/hooks/HEAD\n+\t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n+\t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n+\t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n+\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD\n \t) &&\n \ttest_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&\n \tcat err &&\n@@ -390,13 +390,13 @@ test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into d\n \tgit config unset -f repo/.gitmodules submodule.sub.path &&\n \tprintf \"\\tpath = \\\"sub\\r\\\"\\n\" >>repo/.gitmodules &&\n \n-\tgit config unset -f repo/.git/modules/sub/config core.worktree &&\n+\tgit config unset -f repo/.git/submodules/sub/config core.worktree &&\n \t{\n \t\tprintf \"[core]\\n\" &&\n \t\tprintf \"\\tworktree = \\\"../../../sub\\r\\\"\\n\"\n-\t} >>repo/.git/modules/sub/config &&\n+\t} >>repo/.git/submodules/sub/config &&\n \n-\tln -s .git/modules/sub/hooks repo/sub &&\n+\tln -s .git/submodules/sub/hooks repo/sub &&\n \tgit -C repo add -A &&\n \tgit -C repo commit -m submodule &&\n \ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex 409cd0cd12..ded482fdf2 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -866,7 +866,7 @@ test_expect_success 'submodule always visited' '\n '\n \n # If a submodule has a `sub/.git/` directory (rather than a file\n-# pointing to the super's `.git/modules/sub`) and `core.fsmonitor`\n+# pointing to the super's `.git/submodules/sub`) and `core.fsmonitor`\n # turned on in the submodule and the daemon is not yet started in\n # the submodule, and someone does a `git submodule absorbgitdirs`\n # in the super, Git will recursively invoke `git submodule--helper`\n@@ -895,7 +895,7 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''dir_1/dir_2/sub'\\'' from\n \t'\\''$cwd/dir_1/dir_2/sub/.git'\\'' to\n-\t'\\''$cwd/.git/modules/dir_1/dir_2/sub'\\''\n+\t'\\''$cwd/.git/submodules/dir_1/dir_2/sub'\\''\n \tEOF\n \tGIT_TRACE2_EVENT=\"$PWD/super-sub.trace\" \\\n \t\tgit -C super submodule absorbgitdirs >out 2>actual &&\n-- \n2.51.GIT\n\n"},{"id":"525805","messageId":"20250908140117.262205-6-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 05/10] strbuf: bring back is_rfc3986_unreserved","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:12Z","receivedAt":"2025-09-08T14:02:38Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"is_rfc3986_unreserved() was moved to credential-store.c and was made\nstatic by f89854362c (credential-store: move related functions to\ncredential-store file, 2023-06-06) under a correct assumption, at the\ntime, that it's the only place used.\n\nHowever now we need it to apply url encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c | 6 ------\n strbuf.c                   | 6 ++++++\n strbuf.h                   | 2 ++\n 3 files changed, 8 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..0acaf1cc82 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -76,12 +76,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/strbuf.c b/strbuf.c\nindex 6c3851a7f8..e8d84cbb6d 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -817,6 +817,12 @@ void strbuf_addstr_xml_quoted(struct strbuf *buf, const char *s)\n \t}\n }\n \n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n static void strbuf_add_urlencode(struct strbuf *sb, const char *s, size_t len,\n \t\t\t\t char_predicate allow_unencoded_fn)\n {\ndiff --git a/strbuf.h b/strbuf.h\nindex a580ac6084..5139269039 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -640,6 +640,8 @@ static inline void strbuf_complete_line(struct strbuf *sb)\n \n typedef int (*char_predicate)(char ch);\n \n+int is_rfc3986_unreserved(char ch);\n+\n void strbuf_addstr_urlencode(struct strbuf *sb, const char *name,\n \t\t\t     char_predicate allow_unencoded_fn);\n \n-- \n2.51.GIT\n\n"},{"id":"525806","messageId":"20250908140117.262205-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 04/10] t7425: add basic mixed submodule gitdir path tests","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:11Z","receivedAt":"2025-09-08T14:02:39Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add some basic submodule tests for mixed gitdir path handling of\nlegacy (.git/modules) and new-style (.git/submodule) paths.\n\nFor now these just test the coexistence, creation and push/pull of\nsubmodules using mixed paths.\n\nMore tests will be added later, especially for new-style encoding.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/meson.build                           |   1 +\n t/t7425-submodule-mixed-gitdir-paths.sh | 101 ++++++++++++++++++++++++\n 2 files changed, 102 insertions(+)\n create mode 100755 t/t7425-submodule-mixed-gitdir-paths.sh\n\ndiff --git a/t/meson.build b/t/meson.build\nindex baeeba2ce6..28456158a0 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -879,6 +879,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-mixed-gitdir-paths.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\nnew file mode 100755\nindex 0000000000..31f16d7741\n--- /dev/null\n+++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n@@ -0,0 +1,101 @@\n+#!/bin/sh\n+\n+test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+\tgit config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed new and legacy submodules' '\n+\tgit init -b main legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init -b main new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init -b main main &&\n+\t(\n+\t\tcd main &&\n+\n+\t\tgit config receive.denyCurrentBranch updateInstead &&\n+\n+\t\tgit submodule add ../new-sub new &&\n+\t\ttest_commit new-sub &&\n+\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\t# simulate legacy .git/modules path by moving submodule\n+\t\tmkdir -p .git/modules &&\n+\t\tmv .git/submodules/legacy .git/modules/ &&\n+\t\techo \"gitdir: ../.git/modules/legacy\" > legacy/.git\n+\t)\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned &&\n+\t(\n+\t\tcd cloned &&\n+\n+\t\t# At this point, .git/modules/<name> should not exist as\n+\t\t# submodules are checked out into the new path\n+\t\ttest_path_is_dir .git/submodules/legacy &&\n+\t\ttest_path_is_dir .git/submodules/new &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev new\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to submodules' '\n+\t(\n+\t\tcd cloned &&\n+\n+\t\tgit -C legacy switch --track -C main origin/main  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C new switch --track -C main origin/main &&\n+\t\ttest_commit -C new second-commit &&\n+\t\tgit -C new push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C main origin/main  &&\n+\t\tgit add legacy new &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/submodules/new &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev new\" list\n+\t)\n+'\n+\n+test_done\n-- \n2.51.GIT\n\n"},{"id":"525807","messageId":"20250908140117.262205-7-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 06/10] submodule: encode gitdir paths to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:13Z","receivedAt":"2025-09-08T14:02:49Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Based on previous work by Brandon & all [1].\n\nThis encodes submodule gitdir names to avoid colisions like nested gitdirs\ndue to names like \"foo\" and \"foo/bar\".\n\nA custom encoding can become unnecesarily complex, while url-encoding is\nrelatively well-known, however it needs some extending to support case\ninsensitive filesystems and quirks like Windows reserving \"COM1\" names.\nHence why I opted to encode A as _a, B as _b and so on.\n\nUnfortunately encoding A -> _a (...) is not enough to fix the reserved\nWindows file names (eg COM1) because workdirs still use the name COM1\neven though gitdirs paths encoded, so future work will be needed to\nfully address that case (or just use a different name).\n\nThis affected tests are fixed and a TODO is added to cleanup a hack /\nshort-circuit in validate_submodule_git_dir().\n\nA further commit will add more tests to exercise these codepaths.\n\nLink: https://lore.kernel.org/git/20180807230637.247200-1-bmwill@google.com/ [1]\nBased-on-patch-by: Brandon Williams <bmwill@google.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n submodule.c                  | 55 +++++++++++++++++++++---------------\n t/t7400-submodule-basic.sh   |  2 +-\n t/t7406-submodule-update.sh  | 10 +++----\n t/t7450-bad-git-dotfiles.sh  | 39 +++++++++++++------------\n t/t7527-builtin-fsmonitor.sh |  2 +-\n 5 files changed, 60 insertions(+), 48 deletions(-)\n\ndiff --git a/submodule.c b/submodule.c\nindex bf78636195..8e0fd077db 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2271,8 +2271,13 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \n \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n \t    strcmp(p, submodule_name))\n-\t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n-\t\t    submodule_name, git_dir);\n+\t\t/*\n+\t\t * TODO: revisit and cleanup this test short-circuit, because\n+\t\t * submodules with encoded names are expected to take this path.\n+\t\t * Likely just move the invariants to submodule_name_to_gitdir()\n+\t\t * and delete this entire function in a future commit.\n+\t\t */\n+\t\treturn 0;\n \n \t/*\n \t * We prevent the contents of sibling submodules' git directories to\n@@ -2588,30 +2593,26 @@ int submodule_to_gitdir(struct repository *repo,\n \treturn ret;\n }\n \n+static void strbuf_addstr_case_encode(struct strbuf *dst, const char *src)\n+{\n+\tfor (; *src; src++) {\n+\t\tunsigned char c = *src;\n+\t\tif (c >= 'A' && c <= 'Z') {\n+\t\t\tstrbuf_addch(dst, '_');\n+\t\t\tstrbuf_addch(dst, c - 'A' + 'a');\n+\t\t} else {\n+\t\t\tstrbuf_addch(dst, c);\n+\t\t}\n+\t}\n+}\n+\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n-\t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n-\t */\n+\tstruct strbuf encoded_sub_name = STRBUF_INIT, tmp = STRBUF_INIT;\n+\tsize_t base_len, encoded_len;\n \tchar *gitdir_path, *key;\n+\tlong name_max;\n \n \t/* Allow config override. */\n \tkey = xstrfmt(\"submodule.%s.gitdirpath\", submodule_name);\n@@ -2632,5 +2633,13 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t/* New style (encoded) paths go under submodules/<encoded>. */\n \tstrbuf_reset(buf);\n \trepo_git_path_append(r, buf, \"submodules/\");\n-\tstrbuf_addstr(buf, submodule_name);\n+\tbase_len = buf->len;\n+\n+\t/* URL-encode then case case-encode A to _a, B to _b and so on */\n+\tstrbuf_addstr_urlencode(&tmp, submodule_name, is_rfc3986_unreserved);\n+\tstrbuf_addstr_case_encode(&encoded_sub_name, tmp.buf);\n+\tstrbuf_release(&tmp);\n+\tstrbuf_addbuf(buf, &encoded_sub_name);\n+\n+\tstrbuf_release(&encoded_sub_name);\n }\ndiff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\nindex a632f47b73..fe772fa244 100755\n--- a/t/t7400-submodule-basic.sh\n+++ b/t/t7400-submodule-basic.sh\n@@ -1047,7 +1047,7 @@ test_expect_success 'recursive relative submodules stay relative' '\n \t\tcd clone2 &&\n \t\tgit submodule update --init --recursive &&\n \t\techo \"gitdir: ../.git/submodules/sub3\" >./sub3/.git_expect &&\n-\t\techo \"gitdir: ../../../.git/submodules/sub3/submodules/dirdir/subsub\" >./sub3/dirdir/subsub/.git_expect\n+\t\techo \"gitdir: ../../../.git/submodules/sub3/submodules/dirdir%2fsubsub\" >./sub3/dirdir/subsub/.git_expect\n \t) &&\n \ttest_cmp clone2/sub3/.git_expect clone2/sub3/.git &&\n \ttest_cmp clone2/sub3/dirdir/subsub/.git_expect clone2/sub3/dirdir/subsub/.git\ndiff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh\nindex f0c4da1ffa..c44a7e9513 100755\n--- a/t/t7406-submodule-update.sh\n+++ b/t/t7406-submodule-update.sh\n@@ -864,8 +864,8 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir' '\n \t (cd deeper/submodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/submodules/deeper/submodule &&\n-\t  git log > ../../../../actual\n+\t (cd .git/submodules/deeper%2fsubmodule &&\n+\t  git log > ../../../actual\n \t ) &&\n \t test_cmp expected actual\n \t)\n@@ -882,8 +882,8 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir' '\n \t (cd deeper/submodule &&\n \t  git log > ../../expected\n \t ) &&\n-\t (cd .git/submodules/deeper/submodule &&\n-\t  git log > ../../../../actual\n+\t (cd .git/submodules/deeper%2fsubmodule &&\n+\t  git log > ../../../actual\n \t ) &&\n \t test_cmp expected actual\n \t)\n@@ -899,7 +899,7 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir recur\n \t  git commit -m \"added subsubmodule\" &&\n \t  git push origin :\n \t ) &&\n-\t (cd .git/submodules/deeper/submodule/submodules/subsubmodule &&\n+\t (cd .git/submodules/deeper%2fsubmodule/submodules/subsubmodule &&\n \t  git log > ../../../../../actual\n \t ) &&\n \t git add deeper/submodule &&\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex 4e2ced3636..27254300f8 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -15,6 +15,7 @@ Such as:\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-pack.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup' '\n \tgit config --global protocol.file.allow always\n@@ -319,6 +320,8 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' '\n \tfi\n '\n \n+# TODO: move these nested gitdir tests to another location in a later commit because\n+# they are not pathological cases anymore: by encoding the gitdir paths do not conflict.\n test_expect_success 'setup submodules with nested git dirs' '\n \tgit init nested &&\n \ttest_commit -C nested nested &&\n@@ -341,35 +344,35 @@ test_expect_success 'setup submodules with nested git dirs' '\n '\n \n test_expect_success 'git dirs of sibling submodules must not be nested' '\n-\ttest_must_fail git clone --recurse-submodules nested clone 2>err &&\n-\ttest_grep \"is inside git dir\" err\n+\tgit clone --recurse-submodules nested clone_nested &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n '\n \n test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n-\ttest_must_fail git clone --recurse-submodules --jobs=2 nested clone_parallel 2>err &&\n-\tcat err &&\n-\tgrep -E \"(already exists|is inside git dir|not a git repository)\" err &&\n-\t{\n-\t\ttest_path_is_missing .git/submodules/hippo/HEAD ||\n-\t\ttest_path_is_missing .git/submodules/hippo/hooks/HEAD\n-\t}\n+\tgit clone --recurse-submodules --jobs=2 nested clone_parallel &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n '\n \n-test_expect_success 'checkout -f --recurse-submodules must not use a nested gitdir' '\n-\tgit clone nested nested_checkout &&\n+test_expect_success 'checkout -f --recurse-submodules must corectly handle nested gitdirs' '\n+\tgit clone nested clone_recursive_checkout &&\n \t(\n-\t\tcd nested_checkout &&\n+\t\tcd clone_recursive_checkout &&\n+\n \t\tgit submodule init &&\n-\t\tgit submodule update thing1 &&\n+\t\tgit submodule update thing1 thing2 &&\n+\n+\t\t# simulate a malicious nested alternate which git should not follow\n \t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n \t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n \t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n-\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD\n+\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD &&\n+\n+\t\tgit checkout -f --recurse-submodules HEAD\n \t) &&\n-\ttest_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&\n-\tcat err &&\n-\tgrep \"is inside git dir\" err &&\n-\ttest_path_is_missing nested_checkout/thing2/.git\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n '\n \n test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into different directory' '\ndiff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\nindex ded482fdf2..15e44a6979 100755\n--- a/t/t7527-builtin-fsmonitor.sh\n+++ b/t/t7527-builtin-fsmonitor.sh\n@@ -895,7 +895,7 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n \tcat >expect <<-EOF &&\n \tMigrating git directory of '\\''dir_1/dir_2/sub'\\'' from\n \t'\\''$cwd/dir_1/dir_2/sub/.git'\\'' to\n-\t'\\''$cwd/.git/submodules/dir_1/dir_2/sub'\\''\n+\t'\\''$cwd/.git/submodules/dir_1%2fdir_2%2fsub'\\''\n \tEOF\n \tGIT_TRACE2_EVENT=\"$PWD/super-sub.trace\" \\\n \t\tgit -C super submodule absorbgitdirs >out 2>actual &&\n-- \n2.51.GIT\n\n"},{"id":"525808","messageId":"20250908140117.262205-8-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 07/10] submodule: error out if gitdir name is too long","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:14Z","receivedAt":"2025-09-08T14:02:51Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Encoding submodule names increases their name size, so there is an\nincreased risk to hit the max filename length in the gitdir path.\n(the likelihood is still rather small, so it's an acceptable risk)\n\nThis gitdir file-name-too-long corner case can be be addressed in\nmultiple ways, including sharding or trimming, however for now, just\nadd the portable logic (suggested by Peff) to detect the corner case\nthen error out to avoid comitting to a specific policy (or policies).\n\nIn the future, instead of throwing an error (which we do now anyway\nwithout submodule encoding), we could maybe let the user specify via\nconfigs how to address this case, eg pick trimming or sharding.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Makefile          |  5 +++++\n compat/pathconf.c | 10 ++++++++++\n compat/posix.h    |  8 ++++++++\n config.mak.uname  |  2 ++\n meson.build       |  1 +\n submodule.c       | 14 ++++++++++++++\n 6 files changed, 40 insertions(+)\n create mode 100644 compat/pathconf.c\n\ndiff --git a/Makefile b/Makefile\nindex 555b7f4dc3..1a98eac8a5 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2212,6 +2212,11 @@ ifndef HAVE_PLATFORM_PROCINFO\n \tCOMPAT_OBJS += compat/stub/procinfo.o\n endif\n \n+ifdef NO_PATHCONF\n+\tCOMPAT_CFLAGS += -DNO_PATHCONF\n+\tCOMPAT_OBJS += compat/pathconf.o\n+endif\n+\n ifdef RUNTIME_PREFIX\n \n         ifdef HAVE_BSD_KERN_PROC_SYSCTL\ndiff --git a/compat/pathconf.c b/compat/pathconf.c\nnew file mode 100644\nindex 0000000000..37500cfa0d\n--- /dev/null\n+++ b/compat/pathconf.c\n@@ -0,0 +1,10 @@\n+#include \"git-compat-util.h\"\n+\n+/*\n+ * Minimal stub for platforms without pathconf() (e.g. Windows),\n+ * to fall back to NAME_MAX from limits.h or compat/posix.h.\n+ */\n+long git_pathconf(const char *path UNUSED, int name UNUSED)\n+{\n+\treturn -1;\n+}\ndiff --git a/compat/posix.h b/compat/posix.h\nindex 067a00f33b..aa050fd58c 100644\n--- a/compat/posix.h\n+++ b/compat/posix.h\n@@ -250,6 +250,14 @@ char *gitdirname(char *);\n #define NAME_MAX 255\n #endif\n \n+#ifdef NO_PATHCONF\n+#ifndef _PC_NAME_MAX\n+#define _PC_NAME_MAX 1 /* dummy value, only used for git_pathconf */\n+#endif\n+#define pathconf(a,b) git_pathconf(a,b)\n+long git_pathconf(const char *path, int name);\n+#endif\n+\n typedef uintmax_t timestamp_t;\n #define PRItime PRIuMAX\n #define parse_timestamp strtoumax\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 1691c6ae6e..49ba3de39d 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -473,6 +473,7 @@ ifeq ($(uname_S),Windows)\n \tNEEDS_CRYPTO_WITH_SSL = YesPlease\n \tNO_LIBGEN_H = YesPlease\n \tNO_POLL = YesPlease\n+\tNO_PATHCONF = YesPlease\n \tNO_SYMLINK_HEAD = YesPlease\n \tNO_IPV6 = YesPlease\n \tNO_SETENV = YesPlease\n@@ -688,6 +689,7 @@ ifeq ($(uname_S),MINGW)\n \tNEEDS_CRYPTO_WITH_SSL = YesPlease\n \tNO_LIBGEN_H = YesPlease\n \tNO_POLL = YesPlease\n+\tNO_PATHCONF = YesPlease\n \tNO_SYMLINK_HEAD = YesPlease\n \tNO_SETENV = YesPlease\n \tNO_STRCASESTR = YesPlease\ndiff --git a/meson.build b/meson.build\nindex e8ec0eca16..1fb9300ab1 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1393,6 +1393,7 @@ checkfuncs = {\n   'initgroups' : [],\n   'strtoumax' : ['strtoumax.c', 'strtoimax.c'],\n   'pread' : ['pread.c'],\n+  'pathconf' : ['pathconf.c'],\n }\n \n if host_machine.system() == 'windows'\ndiff --git a/submodule.c b/submodule.c\nindex 8e0fd077db..016509806e 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2641,5 +2641,19 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \tstrbuf_release(&tmp);\n \tstrbuf_addbuf(buf, &encoded_sub_name);\n \n+\t/* Ensure final path length is below NAME_MAX after encoding */\n+\tname_max = pathconf(buf->buf, _PC_NAME_MAX);\n+\tif (name_max == -1)\n+\t\tname_max = NAME_MAX;\n+\n+\tencoded_len = buf->len - base_len;\n+\tif (encoded_len > name_max)\n+\t\t/*\n+\t\t * TODO: make this smarter; instead of erroring out, maybe we could trim or\n+\t\t * shard the gitdir names to make them fit under NAME_MAX.\n+\t\t */\n+\t\tdie(_(\"encoded submodule name '%s' is too long (%\"PRIuMAX\" bytes, limit %\"PRIuMAX\")\"),\n+\t\t    encoded_sub_name.buf, (uintmax_t)encoded_len, (uintmax_t)name_max);\n+\n \tstrbuf_release(&encoded_sub_name);\n }\n-- \n2.51.GIT\n\n"},{"id":"525809","messageId":"20250908140117.262205-9-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 08/10] submodule: remove validate_submodule_git_dir()","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:15Z","receivedAt":"2025-09-08T14:02:52Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"The validate_submodule_git_dir test is not very useful anymore, after\nsubmodule names are encoded to resolve gitdir path conflicts.\n\nIn other words, the purpouse of gitdir path encoding is precisely to\navoid such conflicts as this function tries to also prevent.\n\nThe first test from the function can be kept though, because it just\nverifies invariants which should always be true and raise a BUG if:\n\n  - no \"/\" separator is between dirs/names.\n  - len(full_gitdir) < len(name).\n  - name does not match the gitdir path suffix.\n\nThus we move the invariant checks to submodule_name_to_gitdir() and\nclean up the rest of validate_submodule_git_dir() and its uses.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 21 -----------\n submodule.c                 | 74 ++++---------------------------------\n submodule.h                 |  5 ---\n 3 files changed, 7 insertions(+), 93 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 564f7aadf8..8af7062ff2 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1725,10 +1725,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n \t\t\t\t\t\t    clone_data->path);\n \n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\n \tif (!file_exists(sm_gitdir)) {\n \t\tif (clone_data->require_init && !stat(clone_data_path, &st) &&\n \t\t    !is_empty_dir(clone_data_path))\n@@ -1802,23 +1798,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tfree(path);\n \t}\n \n-\t/*\n-\t * We already performed this check at the beginning of this function,\n-\t * before cloning the objects. This tries to detect racy behavior e.g.\n-\t * in parallel clones, where another process could easily have made the\n-\t * gitdir nested _after_ it was created.\n-\t *\n-\t * To prevent further harm coming from this unintentionally-nested\n-\t * gitdir, let's disable it by deleting the `HEAD` file.\n-\t */\n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {\n-\t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n-\t\tunlink(head);\n-\t\tfree(head);\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\t}\n-\n \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n \n \tp = repo_submodule_path(the_repository, clone_data_path, \"config\");\ndiff --git a/submodule.c b/submodule.c\nindex 016509806e..dbe1a7b091 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2163,27 +2163,10 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\tif (!submodule_uses_gitfile(path))\n \t\t\t\tabsorb_git_dir_into_superproject(path,\n \t\t\t\t\t\t\t\t super_prefix);\n-\t\t\telse {\n-\t\t\t\tchar *dotgit = xstrfmt(\"%s/.git\", path);\n-\t\t\t\tchar *git_dir = xstrdup(read_gitfile(dotgit));\n-\n-\t\t\t\tfree(dotgit);\n-\t\t\t\tif (validate_submodule_git_dir(git_dir,\n-\t\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n-\t\t\t\t\t      \"another submodule's git dir\"),\n-\t\t\t\t\t    git_dir);\n-\t\t\t\tfree(git_dir);\n-\t\t\t}\n \t\t} else {\n \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n \t\t\t\t\t\t sub->name);\n-\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n-\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n-\t\t\t\t      \"submodule's git dir\"),\n-\t\t\t\t    gitdir.buf);\n \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n \t\t\tstrbuf_release(&gitdir);\n \n@@ -2263,52 +2246,6 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n-{\n-\tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n-\tchar *p;\n-\tint ret = 0;\n-\n-\tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n-\t    strcmp(p, submodule_name))\n-\t\t/*\n-\t\t * TODO: revisit and cleanup this test short-circuit, because\n-\t\t * submodules with encoded names are expected to take this path.\n-\t\t * Likely just move the invariants to submodule_name_to_gitdir()\n-\t\t * and delete this entire function in a future commit.\n-\t\t */\n-\t\treturn 0;\n-\n-\t/*\n-\t * We prevent the contents of sibling submodules' git directories to\n-\t * clash.\n-\t *\n-\t * Example: having a submodule named `hippo` and another one named\n-\t * `hippo/hooks` would result in the git directories\n-\t * `.git/submodules/hippo/` and `.git/submodules/hippo/hooks/`, respectively,\n-\t * but the latter directory is already designated to contain the hooks\n-\t * of the former.\n-\t */\n-\tfor (; *p; p++) {\n-\t\tif (is_dir_sep(*p)) {\n-\t\t\tchar c = *p;\n-\n-\t\t\t*p = '\\0';\n-\t\t\tif (is_git_directory(git_dir))\n-\t\t\t\tret = -1;\n-\t\t\t*p = c;\n-\n-\t\t\tif (ret < 0)\n-\t\t\t\treturn error(_(\"submodule git dir '%s' is \"\n-\t\t\t\t\t       \"inside git dir '%.*s'\"),\n-\t\t\t\t\t     git_dir,\n-\t\t\t\t\t     (int)(p - git_dir), git_dir);\n-\t\t}\n-\t}\n-\n-\treturn 0;\n-}\n-\n int validate_submodule_path(const char *path)\n {\n \tchar *p = xstrdup(path);\n@@ -2367,9 +2304,6 @@ static void relocate_single_git_dir_into_superproject(const char *path,\n \t\tdie(_(\"could not lookup name for submodule '%s'\"), path);\n \n \tsubmodule_name_to_gitdir(&new_gitdir, the_repository, sub->name);\n-\tif (validate_submodule_git_dir(new_gitdir.buf, sub->name) < 0)\n-\t\tdie(_(\"refusing to move '%s' into an existing git dir\"),\n-\t\t    real_old_git_dir);\n \tif (safe_create_leading_directories_const(the_repository, new_gitdir.buf) < 0)\n \t\tdie(_(\"could not create directory '%s'\"), new_gitdir.buf);\n \treal_new_git_dir = real_pathdup(new_gitdir.buf, 1);\n@@ -2611,7 +2545,7 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n {\n \tstruct strbuf encoded_sub_name = STRBUF_INIT, tmp = STRBUF_INIT;\n \tsize_t base_len, encoded_len;\n-\tchar *gitdir_path, *key;\n+\tchar *gitdir_path, *key, *p;\n \tlong name_max;\n \n \t/* Allow config override. */\n@@ -2655,5 +2589,11 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\tdie(_(\"encoded submodule name '%s' is too long (%\"PRIuMAX\" bytes, limit %\"PRIuMAX\")\"),\n \t\t    encoded_sub_name.buf, (uintmax_t)encoded_len, (uintmax_t)name_max);\n \n+\t/* Trigger a BUG if these invariants do not hold */\n+\tp = buf->buf + buf->len - encoded_len;\n+\tif (buf->len <= encoded_len || p[-1] != '/' || strcmp(p, encoded_sub_name.buf))\n+\t\tBUG(\"encoded submodule name '%s' is not a suffix of git dir '%s'\",\n+\t\t    encoded_sub_name.buf, buf->buf);\n+\n \tstrbuf_release(&encoded_sub_name);\n }\ndiff --git a/submodule.h b/submodule.h\nindex b10e16e6c0..0b7692bc20 100644\n--- a/submodule.h\n+++ b/submodule.h\n@@ -137,11 +137,6 @@ int submodule_to_gitdir(struct repository *repo,\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name);\n \n-/*\n- * Make sure that no submodule's git dir is nested in a sibling submodule's.\n- */\n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name);\n-\n /*\n  * Make sure that the given submodule path does not follow symlinks.\n  */\n-- \n2.51.GIT\n\n"},{"id":"525810","messageId":"20250908140117.262205-10-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 09/10] t7450: move nested gitdir tests to t7425","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:16Z","receivedAt":"2025-09-08T14:02:56Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Now that we are encoding gitdir paths, these tests are not handling\npathological cases anymore, because nested git dirs shouldn't cause\nconflicts, so move them from t7450-bad-git-dotfiles.sh to a more\nappropriate location where we test mixed gitdir path & encoding use.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/t7425-submodule-mixed-gitdir-paths.sh | 54 ++++++++++++++++++++++++\n t/t7450-bad-git-dotfiles.sh             | 56 -------------------------\n 2 files changed, 54 insertions(+), 56 deletions(-)\n\ndiff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\nindex 31f16d7741..a8c22de070 100755\n--- a/t/t7425-submodule-mixed-gitdir-paths.sh\n+++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n@@ -3,6 +3,7 @@\n test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup: allow file protocol' '\n \tgit config --global protocol.file.allow always\n@@ -98,4 +99,57 @@ test_expect_success 'fetch mixed submodule changes and verify updates' '\n \t)\n '\n \n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of sibling submodules must not be nested' '\n+\tgit clone --recurse-submodules nested clone_nested &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone --recurse-submodules --jobs=2 nested clone_parallel &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n+'\n+\n+test_expect_success 'checkout -f --recurse-submodules must corectly handle nested gitdirs' '\n+\tgit clone nested clone_recursive_checkout &&\n+\t(\n+\t\tcd clone_recursive_checkout &&\n+\n+\t\tgit submodule init &&\n+\t\tgit submodule update thing1 thing2 &&\n+\n+\t\t# simulate a malicious nested alternate which git should not follow\n+\t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n+\t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n+\t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n+\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD &&\n+\n+\t\tgit checkout -f --recurse-submodules HEAD\n+\t) &&\n+\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n+'\n+\n test_done\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex 27254300f8..18624fabc4 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -15,7 +15,6 @@ Such as:\n \n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-pack.sh\n-. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup' '\n \tgit config --global protocol.file.allow always\n@@ -320,61 +319,6 @@ test_expect_success WINDOWS 'prevent git~1 squatting on Windows' '\n \tfi\n '\n \n-# TODO: move these nested gitdir tests to another location in a later commit because\n-# they are not pathological cases anymore: by encoding the gitdir paths do not conflict.\n-test_expect_success 'setup submodules with nested git dirs' '\n-\tgit init nested &&\n-\ttest_commit -C nested nested &&\n-\t(\n-\t\tcd nested &&\n-\t\tcat >.gitmodules <<-EOF &&\n-\t\t[submodule \"hippo\"]\n-\t\t\turl = .\n-\t\t\tpath = thing1\n-\t\t[submodule \"hippo/hooks\"]\n-\t\t\turl = .\n-\t\t\tpath = thing2\n-\t\tEOF\n-\t\tgit clone . thing1 &&\n-\t\tgit clone . thing2 &&\n-\t\tgit add .gitmodules thing1 thing2 &&\n-\t\ttest_tick &&\n-\t\tgit commit -m nested\n-\t)\n-'\n-\n-test_expect_success 'git dirs of sibling submodules must not be nested' '\n-\tgit clone --recurse-submodules nested clone_nested &&\n-\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n-\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n-'\n-\n-test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n-\tgit clone --recurse-submodules --jobs=2 nested clone_parallel &&\n-\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n-\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n-'\n-\n-test_expect_success 'checkout -f --recurse-submodules must corectly handle nested gitdirs' '\n-\tgit clone nested clone_recursive_checkout &&\n-\t(\n-\t\tcd clone_recursive_checkout &&\n-\n-\t\tgit submodule init &&\n-\t\tgit submodule update thing1 thing2 &&\n-\n-\t\t# simulate a malicious nested alternate which git should not follow\n-\t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n-\t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n-\t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n-\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD &&\n-\n-\t\tgit checkout -f --recurse-submodules HEAD\n-\t) &&\n-\tverify_submodule_gitdir_path clone_nested hippo submodules/hippo &&\n-\tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n-'\n-\n test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into different directory' '\n \ttest_when_finished \"rm -rf sub repo bad-clone\" &&\n \n-- \n2.51.GIT\n\n"},{"id":"525811","messageId":"20250908140117.262205-11-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250908140117.262205-1-adrian.ratiu@collabora.com","subject":"[PATCH v2 10/10] t7425: add gitdir encoding tests","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T14:01:17Z","receivedAt":"2025-09-08T14:03:00Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add some tests to further exercise the gitdir encoding functionality\nalongside the existing mixed directory and nested gitdir tests.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n t/t7425-submodule-mixed-gitdir-paths.sh | 47 +++++++++++++++++++++++++\n 1 file changed, 47 insertions(+)\n\ndiff --git a/t/t7425-submodule-mixed-gitdir-paths.sh b/t/t7425-submodule-mixed-gitdir-paths.sh\nindex a8c22de070..f467bafaab 100755\n--- a/t/t7425-submodule-mixed-gitdir-paths.sh\n+++ b/t/t7425-submodule-mixed-gitdir-paths.sh\n@@ -152,4 +152,51 @@ test_expect_success 'checkout -f --recurse-submodules must corectly handle neste\n \tverify_submodule_gitdir_path clone_nested hippo/hooks submodules/hippo%2fhooks\n '\n \n+test_expect_success 'new style submodule gitdir paths are properly encoded' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# add new-style submodule name containing /\n+\t\tgit submodule add ../new-sub foo/bar &&\n+\t\tgit commit -m \"add foo/bar\" &&\n+\n+\t\t# simulate existing legacy submodule name containing escaping char %\n+\t\tgit clone --separate-git-dir .git/modules/foo%bar ../legacy-sub foo%bar  &&\n+\t\tcat >>.gitmodules <<-EOF &&\n+\t\t[submodule \"foo%bar\"]\n+\t\t\tpath = foo%bar\n+\t\t\turl = ../legacy-sub\n+\t\tEOF\n+\t\tgit add .gitmodules &&\n+\t\tgit commit -m \"add foo%bar\" &&\n+\n+\t\t# add new style submodule name containing escaping char %\n+\t\tgit submodule add ../new-sub fooish%bar &&\n+\t\tgit commit -m \"add fooish%bar\" &&\n+\n+\t\t# add a mixed case submdule name\n+\t\tgit submodule add ../new-sub FooBar &&\n+\t\tgit commit -m \"add FooBar\"\n+\t) &&\n+\tverify_submodule_gitdir_path main foo/bar submodules/foo%2fbar &&\n+\tverify_submodule_gitdir_path main foo%bar modules/foo%bar &&\n+\tverify_submodule_gitdir_path main fooish%bar submodules/fooish%25bar &&\n+\tverify_submodule_gitdir_path main FooBar submodules/_foo_bar\n+'\n+\n+test_expect_success 'submodule encoded name exceeds max name limit' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# find the system NAME_MAX (fall back to 255 if unknown)\n+\t\tname_max=$(getconf NAME_MAX . 2>/dev/null || echo 255) &&\n+\n+\t\t# each \"%\" char encodes to \"%25\" (3 chars), ensure we exceed NAME_MAX\n+\t\tcount=$((name_max + 10)) &&\n+\t\tlongname=$(test_seq -f \"%%%0.s\" 1 $count) &&\n+\n+\t\ttest_must_fail git submodule add ../new-sub \"$longname\"\n+\t)\n+'\n+\n test_done\n-- \n2.51.GIT\n\n"},{"id":"525823","messageId":"c64c8c73-13db-49c8-a3d6-a4ce8b554867@gmail.com","threadId":"63967","inReplyTo":"20250816213642.3517822-8-adrian.ratiu@collabora.com","subject":"Re: [PATCH 7/9] submodule: remove validate_submodule_git_dir()","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-08T14:23:10Z","receivedAt":"2025-09-08T14:23:17Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Adrian\n\nOn 16/08/2025 22:36, Adrian Ratiu wrote:\n>   \n> +\t/* Trigger a BUG if these invariants do not hold */\n> +\tp = buf->buf + buf->len - encoded_len;\n> +\tif (buf->len <= encoded_len || p[-1] != '/' || strcmp(p, encoded_sub_name.buf))\n\nif buf->len is less than encoded_len then the pointer p is invalid. As a \nvalid program cannot create an invalid pointer the compiler may assume \nthat buf->len >= encoded_len. We should check the lengths before \ncreating the pointer as the original code in \nvalidate_submodule_git_dir() did which looked like\n\n > if (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n > \t    strcmp(p, submodule_name))\n\n\nThanks\n\nPhillip\n\n> +\t\tBUG(\"encoded submodule name '%s' is not a suffix of git dir '%s'\",\n> +\t\t    encoded_sub_name.buf, buf->buf);\n> +\n>   \tstrbuf_release(&encoded_sub_name);\n>   }\n> diff --git a/submodule.h b/submodule.h\n> index b10e16e6c0..0b7692bc20 100644\n> --- a/submodule.h\n> +++ b/submodule.h\n> @@ -137,11 +137,6 @@ int submodule_to_gitdir(struct repository *repo,\n>   void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>   \t\t\t      const char *submodule_name);\n>   \n> -/*\n> - * Make sure that no submodule's git dir is nested in a sibling submodule's.\n> - */\n> -int validate_submodule_git_dir(char *git_dir, const char *submodule_name);\n> -\n>   /*\n>    * Make sure that the given submodule path does not follow symlinks.\n>    */\n\n"},{"id":"525824","messageId":"ee61c97a-63a5-4c81-8859-09b896bdcf42@gmail.com","threadId":"63967","inReplyTo":"20250816213642.3517822-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH 3/9] submodule: add gitdir path config override","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-08T14:23:53Z","receivedAt":"2025-09-08T14:23:59Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Adrian\n\nOn 16/08/2025 22:36, Adrian Ratiu wrote:\n> This adds an ability to override gitdir paths via config files\n> (not .gitmodules), such that any encoding scheme can be changed\n> and JGit & co don't need to exactly match the default encoding.\n\nReading the old email thread you linked to in the cover letter, my \nunderstanding is that this was suggested as an alternative to changing \nthe gitdir for submodules from \"modules\" to \"submodules\". Do the later \npatches set this key when encoding the gitdir so that JGit can find the \ngitdir by reading the config?\n\nThanks\n\nPhillip\n\n> A new test and a helper are added. The helper will be used by\n> further tests exercising gitdir paths & encodings.\n> \n> Based-on-patch-by: Brandon Williams <bmwill@google.com>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>   builtin/submodule--helper.c           | 17 +++++++++++++++++\n>   submodule.c                           | 11 +++++++++++\n>   t/lib-verify-submodule-gitdir-path.sh | 15 +++++++++++++++\n>   t/t7400-submodule-basic.sh            | 15 +++++++++++++++\n>   4 files changed, 58 insertions(+)\n>   create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n> \n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index 7243429c6f..30e40d6c79 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -1214,6 +1214,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n>   \treturn ret;\n>   }\n>   \n> +static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n> +\t\t\t struct repository *repo UNUSED)\n> +{\n> +\tstruct strbuf gitdir = STRBUF_INIT;\n> +\n> +\tif (argc != 2)\n> +\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n> +\n> +\tsubmodule_name_to_gitdir(&gitdir, the_repository, argv[1]);\n> +\n> +\tprintf(\"%s\\n\", gitdir.buf);\n> +\n> +\tstrbuf_release(&gitdir);\n> +\treturn 0;\n> +}\n> +\n>   struct sync_cb {\n>   \tconst char *prefix;\n>   \tconst char *super_prefix;\n> @@ -3597,6 +3613,7 @@ int cmd_submodule__helper(int argc,\n>   \t\tNULL\n>   \t};\n>   \tstruct option options[] = {\n> +\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n>   \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n>   \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n>   \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\n> diff --git a/submodule.c b/submodule.c\n> index dbf2244e60..bf78636195 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2611,6 +2611,17 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>   \t * administrators can explicitly set. Nothing has been decided,\n>   \t * so for now, just append the name at the end of the path.\n>   \t */\n> +\tchar *gitdir_path, *key;\n> +\n> +\t/* Allow config override. */\n> +\tkey = xstrfmt(\"submodule.%s.gitdirpath\", submodule_name);\n> +\tif (!repo_config_get_string(r, key, &gitdir_path)) {\n> +\t\tstrbuf_addstr(buf, gitdir_path);\n> +\t\tfree(key);\n> +\t\tfree(gitdir_path);\n> +\t\treturn;\n> +\t}\n> +\tfree(key);\n>   \n>   \t/* Legacy behavior: allow existing paths under modules/<name>. */\n>   \trepo_git_path_append(r, buf, \"modules/\");\n> diff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\n> new file mode 100644\n> index 0000000000..fb5cb8eea4\n> --- /dev/null\n> +++ b/t/lib-verify-submodule-gitdir-path.sh\n> @@ -0,0 +1,15 @@\n> +# Helper to verify if repo $1 contains a submodule named $2 with gitdir in path $3\n> +\n> +verify_submodule_gitdir_path() {\n> +\trepo=\"$1\" &&\n> +\tname=\"$2\" &&\n> +\tpath=\"$3\" &&\n> +\t(\n> +\t\tcd \"$repo\" &&\n> +\t\tcat >expect <<-EOF &&\n> +\t\t\t$(git rev-parse --git-common-dir)/$path\n> +\t\tEOF\n> +\t\tgit submodule--helper gitdir \"$name\" >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +}\n> diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\n> index 178c386212..f4d4fb8397 100755\n> --- a/t/t7400-submodule-basic.sh\n> +++ b/t/t7400-submodule-basic.sh\n> @@ -13,6 +13,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n>   export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>   \n>   . ./test-lib.sh\n> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n>   \n>   test_expect_success 'setup - enable local submodules' '\n>   \tgit config --global protocol.file.allow always\n> @@ -1505,4 +1506,18 @@ test_expect_success 'submodule add fails when name is reused' '\n>   \t)\n>   '\n>   \n> +test_expect_success 'submodule helper gitdir config overrides' '\n> +\tverify_submodule_gitdir_path test-submodule child submodules/child &&\n> +\t(\n> +\t\tcd test-submodule &&\n> +\t\tgit config submodule.child.gitdirpath \".git/submodules/custom-child\"\n> +\t) &&\n> +\tverify_submodule_gitdir_path test-submodule child submodules/custom-child &&\n> +\t(\n> +\t\tcd test-submodule &&\n> +\t\tgit config --unset submodule.child.gitdirpath\n> +\t) &&\n> +\tverify_submodule_gitdir_path test-submodule child submodules/child\n> +'\n> +\n>   test_done\n\n"},{"id":"525825","messageId":"fc69ee66-815f-48ec-a5fb-99cac5f4d58c@gmail.com","threadId":"63967","inReplyTo":"20250816213642.3517822-3-adrian.ratiu@collabora.com","subject":"Re: [PATCH 2/9] submodule: create new gitdirs under submodules path","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-08T14:24:25Z","receivedAt":"2025-09-08T14:24:31Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Adrian\n\nOn 16/08/2025 22:36, Adrian Ratiu wrote:\n> This is in preparation for encoding the submodule names to avoid conflicts\n> like submodules named foo and foo/bar together with case-insensitive file-\n> system handling and other corner cases like reserved filenames on Windows.\n> \n> Backward compatibility is kept with plain-name modules already existing at\n> paths like .git/modules/<name>, however a clear separation between legacy\n> (plain) and new (encoded) namespaces is desirable, to avoid situations like\n> an existing plain-name module containing the encoding escape character/\n> \n> Thus we split the new-style (encoded) gitdir name paths to .git/submodules,\n> while legacy-style paths remain under .git/modules.\n> \n> This is just a default directory change with the accompanying test updates,\n> in preparation for the actual encoding additions in future commits.\n\nDoes this need an extentions.submoduleEncoding (name suggestions \nwelcome) config key to stop older versions of git trying to read the \nrepository as they wont be able to locate the gitdir of any submodules \nadded under .git/submodules?\n\nThanks\n\nPhillip\n\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>   Documentation/fetch-options.adoc           |  2 +-\n>   Documentation/git-fetch.adoc               |  2 +-\n>   Documentation/git-submodule.adoc           |  2 +-\n>   Documentation/gitsubmodules.adoc           |  8 ++--\n>   setup.c                                    |  2 +-\n>   submodule.c                                | 28 +++++++++---\n>   t/lib-submodule-update.sh                  | 50 +++++++++++-----------\n>   t/t0035-safe-bare-repository.sh            |  4 +-\n>   t/t1600-index.sh                           |  4 +-\n>   t/t2405-worktree-submodule.sh              |  8 ++--\n>   t/t2501-cwd-empty.sh                       |  2 +-\n>   t/t3600-rm.sh                              |  8 ++--\n>   t/t5526-fetch-submodules.sh                |  2 +-\n>   t/t5619-clone-local-ambiguous-transport.sh |  4 +-\n>   t/t6120-describe.sh                        |  4 +-\n>   t/t7001-mv.sh                              |  4 +-\n>   t/t7400-submodule-basic.sh                 | 18 ++++----\n>   t/t7406-submodule-update.sh                | 10 ++---\n>   t/t7407-submodule-foreach.sh               |  6 +--\n>   t/t7408-submodule-reference.sh             | 22 +++++-----\n>   t/t7412-submodule-absorbgitdirs.sh         | 22 +++++-----\n>   t/t7423-submodule-symlinks.sh              |  8 ++--\n>   t/t7450-bad-git-dotfiles.sh                | 32 +++++++-------\n>   t/t7527-builtin-fsmonitor.sh               |  4 +-\n>   24 files changed, 136 insertions(+), 120 deletions(-)\n> \n> diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\n> index b01372e4b3..f8d3f65009 100644\n> --- a/Documentation/fetch-options.adoc\n> +++ b/Documentation/fetch-options.adoc\n> @@ -210,7 +210,7 @@ ifndef::git-pull[]\n>   \tsubmodule that has commits that are referenced by a newly fetched\n>   \tsuperproject commit but are missing in the local submodule clone. A\n>   \tchanged submodule can be fetched as long as it is present locally e.g.\n> -\tin `$GIT_DIR/modules/` (see linkgit:gitsubmodules[7]); if the upstream\n> +\tin `$GIT_DIR/submodules/` (see linkgit:gitsubmodules[7]); if the upstream\n>   \tadds a new submodule, that submodule cannot be fetched until it is\n>   \tcloned e.g. by `git submodule update`.\n>   +\n> diff --git a/Documentation/git-fetch.adoc b/Documentation/git-fetch.adoc\n> index 16f5d9d69a..2923a29bef 100644\n> --- a/Documentation/git-fetch.adoc\n> +++ b/Documentation/git-fetch.adoc\n> @@ -304,7 +304,7 @@ include::config/fetch.adoc[]\n>   BUGS\n>   ----\n>   Using --recurse-submodules can only fetch new commits in submodules that are\n> -present locally e.g. in `$GIT_DIR/modules/`. If the upstream adds a new\n> +present locally e.g. in `$GIT_DIR/submodules/`. If the upstream adds a new\n>   submodule, that submodule cannot be fetched until it is cloned e.g. by `git\n>   submodule update`. This is expected to be fixed in a future Git version.\n>   \n> diff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc\n> index 503c84a200..9389862208 100644\n> --- a/Documentation/git-submodule.adoc\n> +++ b/Documentation/git-submodule.adoc\n> @@ -266,7 +266,7 @@ registered submodules, and sync any nested submodules within.\n>   absorbgitdirs::\n>   \tIf a git directory of a submodule is inside the submodule,\n>   \tmove the git directory of the submodule into its superproject's\n> -\t`$GIT_DIR/modules` path and then connect the git directory and\n> +\t`$GIT_DIR/submodules` path and then connect the git directory and\n>   \tits working directory by setting the `core.worktree` and adding\n>   \ta .git file pointing to the git directory embedded in the\n>   \tsuperprojects git directory.\n> diff --git a/Documentation/gitsubmodules.adoc b/Documentation/gitsubmodules.adoc\n> index f7b5a25a0c..061e24f316 100644\n> --- a/Documentation/gitsubmodules.adoc\n> +++ b/Documentation/gitsubmodules.adoc\n> @@ -21,12 +21,12 @@ The submodule has its own history; the repository it is embedded\n>   in is called a superproject.\n>   \n>   On the filesystem, a submodule usually (but not always - see FORMS below)\n> -consists of (i) a Git directory located under the `$GIT_DIR/modules/`\n> +consists of (i) a Git directory located under the `$GIT_DIR/submodules/`\n>   directory of its superproject, (ii) a working directory inside the\n>   superproject's working directory, and a `.git` file at the root of\n>   the submodule's working directory pointing to (i).\n>   \n> -Assuming the submodule has a Git directory at `$GIT_DIR/modules/foo/`\n> +Assuming the submodule has a Git directory at `$GIT_DIR/submodules/foo/`\n>   and a working directory at `path/to/bar/`, the superproject tracks the\n>   submodule via a `gitlink` entry in the tree at `path/to/bar` and an entry\n>   in its `.gitmodules` file (see linkgit:gitmodules[5]) of the form\n> @@ -137,7 +137,7 @@ using older versions of Git.\n>   It is possible to construct these old form repositories manually.\n>   +\n>   When deinitialized or deleted (see below), the submodule's Git\n> -directory is automatically moved to `$GIT_DIR/modules/<name>/`\n> +directory is automatically moved to `$GIT_DIR/submodules/<name>/`\n>   of the superproject.\n>   \n>    * Deinitialized submodule: A `gitlink`, and a `.gitmodules` entry,\n> @@ -162,7 +162,7 @@ possible to checkout past commits without requiring fetching\n>   from another repository.\n>   +\n>   To completely remove a submodule, manually delete\n> -`$GIT_DIR/modules/<name>/`.\n> +`$GIT_DIR/submodules/<name>/`.\n>   \n>   ACTIVE SUBMODULES\n>   -----------------\n> diff --git a/setup.c b/setup.c\n> index 98ddbf377f..d054dafa6a 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -1416,7 +1416,7 @@ static int is_implicit_bare_repo(const char *path)\n>   \t * we are inside $GIT_DIR of a worktree of a non-embedded\n>   \t * submodule, whose superproject is not a bare repository.\n>   \t */\n> -\tif (strstr(path, \"/.git/modules/\"))\n> +\tif (strstr(path, \"/.git/modules/\") || strstr(path, \"/.git/submodules/\"))\n>   \t\treturn 1;\n>   \n>   \treturn 0;\n> diff --git a/submodule.c b/submodule.c\n> index fff3c75570..dbf2244e60 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -1278,22 +1278,29 @@ void check_for_new_submodule_commits(struct object_id *oid)\n>   \n>   /*\n>    * Returns 1 if there is at least one submodule gitdir in\n> - * $GIT_DIR/modules and 0 otherwise. This follows\n> + * $GIT_DIR/(sub)modules and 0 otherwise. This follows\n>    * submodule_name_to_gitdir(), which looks for submodules in\n> - * $GIT_DIR/modules, not $GIT_COMMON_DIR.\n> + * $GIT_DIR/(sub)modules, not $GIT_COMMON_DIR.\n>    *\n> - * A submodule can be moved to $GIT_DIR/modules manually by running \"git\n> - * submodule absorbgitdirs\", or it may be initialized there by \"git\n> - * submodule update\".\n> + * A submodule can be moved to $GIT_DIR/(sub)modules manually by running\n> + * \"git submodule absorbgitdirs\", or it may be initialized there by\n> + * \"git submodule update\".\n>    */\n>   static int repo_has_absorbed_submodules(struct repository *r)\n>   {\n>   \tint ret;\n>   \tstruct strbuf buf = STRBUF_INIT;\n>   \n> +\t/* check legacy path */\n>   \trepo_git_path_append(r, &buf, \"modules/\");\n>   \tret = file_exists(buf.buf) && !is_empty_dir(buf.buf);\n> +\tstrbuf_reset(&buf);\n> +\n> +\t/* new (encoded name) path */\n> +\trepo_git_path_append(r, &buf, \"submodules/\");\n> +\tret |= file_exists(buf.buf) && !is_empty_dir(buf.buf);\n>   \tstrbuf_release(&buf);\n> +\n>   \treturn ret;\n>   }\n>   \n> @@ -2273,7 +2280,7 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n>   \t *\n>   \t * Example: having a submodule named `hippo` and another one named\n>   \t * `hippo/hooks` would result in the git directories\n> -\t * `.git/modules/hippo/` and `.git/modules/hippo/hooks/`, respectively,\n> +\t * `.git/submodules/hippo/` and `.git/submodules/hippo/hooks/`, respectively,\n>   \t * but the latter directory is already designated to contain the hooks\n>   \t * of the former.\n>   \t */\n> @@ -2604,6 +2611,15 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>   \t * administrators can explicitly set. Nothing has been decided,\n>   \t * so for now, just append the name at the end of the path.\n>   \t */\n> +\n> +\t/* Legacy behavior: allow existing paths under modules/<name>. */\n>   \trepo_git_path_append(r, buf, \"modules/\");\n>   \tstrbuf_addstr(buf, submodule_name);\n> +\tif (!access(buf->buf, F_OK))\n> +\t\treturn;\n> +\n> +\t/* New style (encoded) paths go under submodules/<encoded>. */\n> +\tstrbuf_reset(buf);\n> +\trepo_git_path_append(r, buf, \"submodules/\");\n> +\tstrbuf_addstr(buf, submodule_name);\n>   }\n> diff --git a/t/lib-submodule-update.sh b/t/lib-submodule-update.sh\n> index 36f767cb74..b6b2be1df5 100644\n> --- a/t/lib-submodule-update.sh\n> +++ b/t/lib-submodule-update.sh\n> @@ -161,7 +161,7 @@ replace_gitfile_with_git_dir () {\n>   }\n>   \n>   # Test that the .git directory in the submodule is unchanged (except for the\n> -# core.worktree setting, which appears only in $GIT_DIR/modules/$1/config).\n> +# core.worktree setting, which appears only in $GIT_DIR/submodules/$1/config).\n>   # Call this function before test_submodule_content as the latter might\n>   # write the index file leading to false positive index differences.\n>   #\n> @@ -170,23 +170,23 @@ replace_gitfile_with_git_dir () {\n>   test_git_directory_is_unchanged () {\n>   \t# does core.worktree point at the right place?\n>   \techo \"../../../$1\" >expect &&\n> -\tgit -C \".git/modules/$1\" config core.worktree >actual &&\n> +\tgit -C \".git/submodules/$1\" config core.worktree >actual &&\n>   \ttest_cmp expect actual &&\n>   \t# remove it temporarily before comparing, as\n>   \t# \"$1/.git/config\" lacks it...\n> -\tgit -C \".git/modules/$1\" config --unset core.worktree &&\n> -\tdiff -r \".git/modules/$1\" \"$1/.git\" &&\n> +\tgit -C \".git/submodules/$1\" config --unset core.worktree &&\n> +\tdiff -r \".git/submodules/$1\" \"$1/.git\" &&\n>   \t# ... and then restore.\n> -\tgit -C \".git/modules/$1\" config core.worktree \"../../../$1\"\n> +\tgit -C \".git/submodules/$1\" config core.worktree \"../../../$1\"\n>   }\n>   \n>   test_git_directory_exists () {\n> -\ttest -e \".git/modules/$1\" &&\n> +\ttest -e \".git/submodules/$1\" &&\n>   \tif test -f sub1/.git\n>   \tthen\n>   \t\t# does core.worktree point at the right place?\n>   \t\techo \"../../../$1\" >expect &&\n> -\t\tgit -C \".git/modules/$1\" config core.worktree >actual &&\n> +\t\tgit -C \".git/submodules/$1\" config core.worktree >actual &&\n>   \t\ttest_cmp expect actual\n>   \tfi\n>   }\n> @@ -225,22 +225,22 @@ reset_work_tree_to () {\n>   reset_work_tree_to_interested () {\n>   \treset_work_tree_to $1 &&\n>   \t# make the submodule git dirs available\n> -\tif ! test -d submodule_update/.git/modules/sub1\n> +\tif ! test -d submodule_update/.git/submodules/sub1\n>   \tthen\n> -\t\tmkdir -p submodule_update/.git/modules &&\n> -\t\tcp -r submodule_update_repo/.git/modules/sub1 submodule_update/.git/modules/sub1\n> -\t\tGIT_WORK_TREE=. git -C submodule_update/.git/modules/sub1 config --unset core.worktree\n> +\t\tmkdir -p submodule_update/.git/submodules &&\n> +\t\tcp -r submodule_update_repo/.git/submodules/sub1 submodule_update/.git/submodules/sub1\n> +\t\tGIT_WORK_TREE=. git -C submodule_update/.git/submodules/sub1 config --unset core.worktree\n>   \tfi &&\n> -\tif ! test -d submodule_update/.git/modules/sub1/modules/sub2\n> +\tif ! test -d submodule_update/.git/submodules/sub1/submodules/sub2\n>   \tthen\n> -\t\tmkdir -p submodule_update/.git/modules/sub1/modules &&\n> -\t\tcp -r submodule_update_repo/.git/modules/sub1/modules/sub2 submodule_update/.git/modules/sub1/modules/sub2\n> +\t\tmkdir -p submodule_update/.git/submodules/sub1/submodules &&\n> +\t\tcp -r submodule_update_repo/.git/submodules/sub1/submodules/sub2 submodule_update/.git/submodules/sub1/submodules/sub2\n>   \t\t# core.worktree is unset for sub2 as it is not checked out\n>   \tfi &&\n>   \t# indicate we are interested in the submodule:\n>   \tgit -C submodule_update config submodule.sub1.url \"bogus\" &&\n>   \t# sub1 might not be checked out, so use the git dir\n> -\tgit -C submodule_update/.git/modules/sub1 config submodule.sub2.url \"bogus\"\n> +\tgit -C submodule_update/.git/submodules/sub1 config submodule.sub2.url \"bogus\"\n>   }\n>   \n>   # Test that the superproject contains the content according to commit \"$1\"\n> @@ -742,7 +742,7 @@ test_submodule_recursing_with_args_common () {\n>   \t\t\t$command remove_sub1 &&\n>   \t\t\ttest_superproject_content origin/remove_sub1 &&\n>   \t\t\t! test -e sub1 &&\n> -\t\t\ttest_must_fail git config -f .git/modules/sub1/config core.worktree\n> +\t\t\ttest_must_fail git config -f .git/submodules/sub1/config core.worktree\n>   \t\t)\n>   \t'\n>   \t# ... absorbing a .git directory along the way.\n> @@ -753,7 +753,7 @@ test_submodule_recursing_with_args_common () {\n>   \t\t\tcd submodule_update &&\n>   \t\t\tgit branch -t remove_sub1 origin/remove_sub1 &&\n>   \t\t\treplace_gitfile_with_git_dir sub1 &&\n> -\t\t\trm -rf .git/modules &&\n> +\t\t\trm -rf .git/submodules &&\n>   \t\t\t$command remove_sub1 &&\n>   \t\t\ttest_superproject_content origin/remove_sub1 &&\n>   \t\t\t! test -e sub1 &&\n> @@ -803,8 +803,8 @@ test_submodule_recursing_with_args_common () {\n>   \t\t\t$command no_submodule &&\n>   \t\t\ttest_superproject_content origin/no_submodule &&\n>   \t\t\ttest_path_is_missing sub1 &&\n> -\t\t\ttest_must_fail git config -f .git/modules/sub1/config core.worktree &&\n> -\t\t\ttest_must_fail git config -f .git/modules/sub1/modules/sub2/config core.worktree\n> +\t\t\ttest_must_fail git config -f .git/submodules/sub1/config core.worktree &&\n> +\t\t\ttest_must_fail git config -f .git/submodules/sub1/submodules/sub2/config core.worktree\n>   \t\t)\n>   \t'\n>   \n> @@ -937,7 +937,7 @@ test_submodule_switch_recursing_with_args () {\n>   \t\t\tcd submodule_update &&\n>   \t\t\tgit branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&\n>   \t\t\treplace_gitfile_with_git_dir sub1 &&\n> -\t\t\trm -rf .git/modules &&\n> +\t\t\trm -rf .git/submodules &&\n>   \t\t\t$command replace_sub1_with_directory &&\n>   \t\t\ttest_superproject_content origin/replace_sub1_with_directory &&\n>   \t\t\ttest_git_directory_exists sub1\n> @@ -946,15 +946,15 @@ test_submodule_switch_recursing_with_args () {\n>   \n>   \t# ... and ignored files are ignored\n>   \ttest_expect_success \"$command: replace submodule with a file works ignores ignored files in submodule\" '\n> -\t\ttest_when_finished \"rm submodule_update/.git/modules/sub1/info/exclude\" &&\n> +\t\ttest_when_finished \"rm submodule_update/.git/submodules/sub1/info/exclude\" &&\n>   \t\tprolog &&\n>   \t\treset_work_tree_to_interested add_sub1 &&\n>   \t\t(\n>   \t\t\tcd submodule_update &&\n> -\t\t\trm -rf .git/modules/sub1/info &&\n> +\t\t\trm -rf .git/submodules/sub1/info &&\n>   \t\t\tgit branch -t replace_sub1_with_file origin/replace_sub1_with_file &&\n> -\t\t\tmkdir .git/modules/sub1/info &&\n> -\t\t\techo ignored >.git/modules/sub1/info/exclude &&\n> +\t\t\tmkdir .git/submodules/sub1/info &&\n> +\t\t\techo ignored >.git/submodules/sub1/info/exclude &&\n>   \t\t\t: >sub1/ignored &&\n>   \t\t\t$command replace_sub1_with_file &&\n>   \t\t\ttest_superproject_content origin/replace_sub1_with_file &&\n> @@ -1034,7 +1034,7 @@ test_submodule_forced_switch_recursing_with_args () {\n>   \t\t\tcd submodule_update &&\n>   \t\t\tgit branch -t replace_sub1_with_directory origin/replace_sub1_with_directory &&\n>   \t\t\treplace_gitfile_with_git_dir sub1 &&\n> -\t\t\trm -rf .git/modules/sub1 &&\n> +\t\t\trm -rf .git/submodules/sub1 &&\n>   \t\t\t$command replace_sub1_with_directory &&\n>   \t\t\ttest_superproject_content origin/replace_sub1_with_directory &&\n>   \t\t\ttest_git_directory_exists sub1\n> diff --git a/t/t0035-safe-bare-repository.sh b/t/t0035-safe-bare-repository.sh\n> index ae7ef092ab..a480ddf8d6 100755\n> --- a/t/t0035-safe-bare-repository.sh\n> +++ b/t/t0035-safe-bare-repository.sh\n> @@ -41,7 +41,7 @@ test_expect_success 'setup an embedded bare repo, secondary worktree and submodu\n>   \t\t\tsubmodule add --name subn -- ./bare-repo subd\n>   \t) &&\n>   \ttest_path_is_dir outer-repo/.git/worktrees/outer-secondary &&\n> -\ttest_path_is_dir outer-repo/.git/modules/subn\n> +\ttest_path_is_dir outer-repo/.git/submodules/subn\n>   '\n>   \n>   test_expect_success 'safe.bareRepository unset' '\n> @@ -100,7 +100,7 @@ test_expect_success 'no trace in $GIT_DIR of secondary worktree' '\n>   '\n>   \n>   test_expect_success 'no trace in $GIT_DIR of a submodule' '\n> -\texpect_accepted_implicit -C outer-repo/.git/modules/subn\n> +\texpect_accepted_implicit -C outer-repo/.git/submodules/subn\n>   '\n>   \n>   test_done\n> diff --git a/t/t1600-index.sh b/t/t1600-index.sh\n> index 03239e9faa..0e5e8efb20 100755\n> --- a/t/t1600-index.sh\n> +++ b/t/t1600-index.sh\n> @@ -87,10 +87,10 @@ test_expect_success 'index.skipHash config option' '\n>   \tgit -c protocol.file.allow=always submodule add ./ sub &&\n>   \tgit config index.skipHash false &&\n>   \tgit -C sub config index.skipHash true &&\n> -\trm -f .git/modules/sub/index &&\n> +\trm -f .git/submodules/sub/index &&\n>   \t>sub/file &&\n>   \tgit -C sub add a &&\n> -\ttest_trailing_hash .git/modules/sub/index >hash &&\n> +\ttest_trailing_hash .git/submodules/sub/index >hash &&\n>   \ttest_cmp expect hash &&\n>   \tgit -C sub fsck\n>   '\n> diff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh\n> index 11018f37c7..c18c2efca5 100755\n> --- a/t/t2405-worktree-submodule.sh\n> +++ b/t/t2405-worktree-submodule.sh\n> @@ -62,7 +62,7 @@ test_expect_success 'submodule is checked out after manually adding submodule wo\n>   test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules in a linked worktree' '\n>   \tgit -C main worktree add \"$base_path/checkout-recurse\" --detach  &&\n>   \tgit -C checkout-recurse submodule update --init &&\n> -\techo \"gitdir: ../../main/.git/worktrees/checkout-recurse/modules/sub\" >expect-gitfile &&\n> +\techo \"gitdir: ../../main/.git/worktrees/checkout-recurse/submodules/sub\" >expect-gitfile &&\n>   \tcat checkout-recurse/sub/.git >actual-gitfile &&\n>   \ttest_cmp expect-gitfile actual-gitfile &&\n>   \tgit -C main/sub rev-parse HEAD >expect-head-main &&\n> @@ -73,7 +73,7 @@ test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules\n>   \ttest_cmp expect-head-main actual-head-main\n>   '\n>   \n> -test_expect_success 'core.worktree is removed in $GIT_DIR/modules/<name>/config, not in $GIT_COMMON_DIR/modules/<name>/config' '\n> +test_expect_success 'core.worktree is removed in $GIT_DIR/submodules/<name>/config, not in $GIT_COMMON_DIR/submodules/<name>/config' '\n>   \techo \"../../../sub\" >expect-main &&\n>   \tgit -C main/sub config --get core.worktree >actual-main &&\n>   \ttest_cmp expect-main actual-main &&\n> @@ -81,14 +81,14 @@ test_expect_success 'core.worktree is removed in $GIT_DIR/modules/<name>/config,\n>   \tgit -C checkout-recurse/sub config --get core.worktree >actual-linked &&\n>   \ttest_cmp expect-linked actual-linked &&\n>   \tgit -C checkout-recurse checkout --recurse-submodules first &&\n> -\ttest_expect_code 1 git -C main/.git/worktrees/checkout-recurse/modules/sub config --get core.worktree >linked-config &&\n> +\ttest_expect_code 1 git -C main/.git/worktrees/checkout-recurse/submodules/sub config --get core.worktree >linked-config &&\n>   \ttest_must_be_empty linked-config &&\n>   \tgit -C main/sub config --get core.worktree >actual-main &&\n>   \ttest_cmp expect-main actual-main\n>   '\n>   \n>   test_expect_success 'unsetting core.worktree does not prevent running commands directly against the submodule repository' '\n> -\tgit -C main/.git/worktrees/checkout-recurse/modules/sub log\n> +\tgit -C main/.git/worktrees/checkout-recurse/submodules/sub log\n>   '\n>   \n>   test_done\n> diff --git a/t/t2501-cwd-empty.sh b/t/t2501-cwd-empty.sh\n> index be9140bbaa..bb8751433f 100755\n> --- a/t/t2501-cwd-empty.sh\n> +++ b/t/t2501-cwd-empty.sh\n> @@ -239,7 +239,7 @@ test_submodule_removal () {\n>   \ttest \"$path_status\" = dir && test_status=test_must_fail\n>   \n>   \ttest_when_finished \"git reset --hard HEAD~1\" &&\n> -\ttest_when_finished \"rm -rf .git/modules/my_submodule\" &&\n> +\ttest_when_finished \"rm -rf .git/submodules/my_submodule\" &&\n>   \n>   \tgit checkout foo/bar/baz &&\n>   \n> diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh\n> index 1f16e6b522..5b8ed57538 100755\n> --- a/t/t3600-rm.sh\n> +++ b/t/t3600-rm.sh\n> @@ -582,7 +582,7 @@ test_expect_success 'rm of a conflicted populated submodule with a .git director\n>   \t(\n>   \t\tcd submod &&\n>   \t\trm .git &&\n> -\t\tcp -R ../.git/modules/sub .git &&\n> +\t\tcp -R ../.git/submodules/sub .git &&\n>   \t\tGIT_WORK_TREE=. git config --unset core.worktree\n>   \t) &&\n>   \ttest_must_fail git merge conflict2 &&\n> @@ -617,9 +617,9 @@ test_expect_success 'rm of a populated submodule with a .git directory migrates\n>   \t(\n>   \t\tcd submod &&\n>   \t\trm .git &&\n> -\t\tcp -R ../.git/modules/sub .git &&\n> +\t\tcp -R ../.git/submodules/sub .git &&\n>   \t\tGIT_WORK_TREE=. git config --unset core.worktree &&\n> -\t\trm -r ../.git/modules/sub\n> +\t\trm -r ../.git/submodules/sub\n>   \t) &&\n>   \tgit rm submod 2>output.err &&\n>   \ttest_path_is_missing submod &&\n> @@ -709,7 +709,7 @@ test_expect_success \"rm absorbs submodule's nested .git directory\" '\n>   \t(\n>   \t\tcd submod/subsubmod &&\n>   \t\trm .git &&\n> -\t\tmv ../../.git/modules/sub/modules/sub .git &&\n> +\t\tmv ../../.git/submodules/sub/submodules/sub .git &&\n>   \t\tGIT_WORK_TREE=. git config --unset core.worktree\n>   \t) &&\n>   \tgit rm submod 2>output.err &&\n> diff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh\n> index 5e566205ba..b7385bc088 100755\n> --- a/t/t5526-fetch-submodules.sh\n> +++ b/t/t5526-fetch-submodules.sh\n> @@ -1143,7 +1143,7 @@ test_expect_success 'fetch --recurse-submodules updates name-conflicted, unpopul\n>   \thead1=$(git -C same-name-1/submodule rev-parse HEAD) &&\n>   \thead2=$(git -C same-name-2/submodule rev-parse HEAD) &&\n>   \t(\n> -\t\tcd same-name-downstream/.git/modules/submodule &&\n> +\t\tcd same-name-downstream/.git/submodules/submodule &&\n>   \t\t# The submodule has core.worktree pointing to the \"git\n>   \t\t# rm\"-ed directory, overwrite the invalid value. See\n>   \t\t# comment in get_fetch_task_from_changed() for more\n> diff --git a/t/t5619-clone-local-ambiguous-transport.sh b/t/t5619-clone-local-ambiguous-transport.sh\n> index cce62bf78d..cf2d5e7bfb 100755\n> --- a/t/t5619-clone-local-ambiguous-transport.sh\n> +++ b/t/t5619-clone-local-ambiguous-transport.sh\n> @@ -38,7 +38,7 @@ test_expect_success 'setup' '\n>   \t\tln -s \"$(cd .. && pwd)/sensitive\" repo/objects &&\n>   \n>   \t\tmkdir -p \"$HTTPD_URL/dumb\" &&\n> -\t\tln -s \"../../../.git/modules/sub/../../../repo/\" \"$URI\" &&\n> +\t\tln -s \"../../../.git/submodules/sub/../../../repo/\" \"$URI\" &&\n>   \n>   \t\tgit add . &&\n>   \t\tgit commit -m \"initial commit\"\n> @@ -57,7 +57,7 @@ test_expect_success 'ambiguous transport does not lead to arbitrary file-inclusi\n>   \tgit clone malicious clone &&\n>   \ttest_must_fail git -C clone submodule update --init 2>err &&\n>   \n> -\ttest_path_is_missing clone/.git/modules/sub/objects/secret &&\n> +\ttest_path_is_missing clone/.git/submodules/sub/objects/secret &&\n>   \t# We would actually expect \"transport .file. not allowed\" here,\n>   \t# but due to quirks of the URL detection in Git, we mis-parse\n>   \t# the absolute path as a bogus URL and die before that step.\n> diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh\n> index 256ccaefb7..56460ae8b5 100755\n> --- a/t/t6120-describe.sh\n> +++ b/t/t6120-describe.sh\n> @@ -357,7 +357,7 @@ test_expect_success 'setup and absorb a submodule' '\n>   '\n>   \n>   test_expect_success 'describe chokes on severely broken submodules' '\n> -\tmv .git/modules/sub1/ .git/modules/sub_moved &&\n> +\tmv .git/submodules/sub1/ .git/submodules/sub_moved &&\n>   \ttest_must_fail git describe --dirty\n>   '\n>   \n> @@ -371,7 +371,7 @@ test_expect_success 'describe with --work-tree ignoring a broken submodule' '\n>   \t\tcd \"$TEST_DIRECTORY\" &&\n>   \t\tgit --git-dir \"$TRASH_DIRECTORY/.git\" --work-tree \"$TRASH_DIRECTORY\" describe --broken >\"$TRASH_DIRECTORY/out\"\n>   \t) &&\n> -\ttest_when_finished \"mv .git/modules/sub_moved .git/modules/sub1\" &&\n> +\ttest_when_finished \"mv .git/submodules/sub_moved .git/submodules/sub1\" &&\n>   \tgrep broken out\n>   '\n>   \n> diff --git a/t/t7001-mv.sh b/t/t7001-mv.sh\n> index 920479e925..89b06ae3c1 100755\n> --- a/t/t7001-mv.sh\n> +++ b/t/t7001-mv.sh\n> @@ -360,7 +360,7 @@ test_expect_success 'git mv moves a submodule with a .git directory and no .gitm\n>   \t(\n>   \t\tcd sub &&\n>   \t\trm -f .git &&\n> -\t\tcp -R -P -p ../.git/modules/sub .git &&\n> +\t\tcp -R -P -p ../.git/submodules/sub .git &&\n>   \t\tGIT_WORK_TREE=. git config --unset core.worktree\n>   \t) &&\n>   \tmkdir mod &&\n> @@ -380,7 +380,7 @@ test_expect_success 'git mv moves a submodule with a .git directory and .gitmodu\n>   \t(\n>   \t\tcd sub &&\n>   \t\trm -f .git &&\n> -\t\tcp -R -P -p ../.git/modules/sub .git &&\n> +\t\tcp -R -P -p ../.git/submodules/sub .git &&\n>   \t\tGIT_WORK_TREE=. git config --unset core.worktree\n>   \t) &&\n>   \tmkdir mod &&\n> diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\n> index fd3e7e355e..178c386212 100755\n> --- a/t/t7400-submodule-basic.sh\n> +++ b/t/t7400-submodule-basic.sh\n> @@ -163,7 +163,7 @@ test_expect_success 'submodule add' '\n>   \t\tcd addtest &&\n>   \t\tgit submodule add -q \"$submodurl\" submod >actual &&\n>   \t\ttest_must_be_empty actual &&\n> -\t\techo \"gitdir: ../.git/modules/submod\" >expect &&\n> +\t\techo \"gitdir: ../.git/submodules/submod\" >expect &&\n>   \t\ttest_cmp expect submod/.git &&\n>   \t\t(\n>   \t\t\tcd submod &&\n> @@ -976,21 +976,21 @@ test_expect_success 'submodule add --name allows to replace a submodule with ano\n>   \t\t\techo \"$submodurl/repo\" >expect &&\n>   \t\t\tgit config remote.origin.url >actual &&\n>   \t\t\ttest_cmp expect actual &&\n> -\t\t\techo \"gitdir: ../.git/modules/repo\" >expect &&\n> +\t\t\techo \"gitdir: ../.git/submodules/repo\" >expect &&\n>   \t\t\ttest_cmp expect .git\n>   \t\t) &&\n>   \t\trm -rf repo &&\n>   \t\tgit rm repo &&\n>   \t\tgit submodule add -q --name repo_new \"$submodurl/bare.git\" repo >actual &&\n>   \t\ttest_must_be_empty actual &&\n> -\t\techo \"gitdir: ../.git/modules/submod\" >expect &&\n> +\t\techo \"gitdir: ../.git/submodules/submod\" >expect &&\n>   \t\ttest_cmp expect submod/.git &&\n>   \t\t(\n>   \t\t\tcd repo &&\n>   \t\t\techo \"$submodurl/bare.git\" >expect &&\n>   \t\t\tgit config remote.origin.url >actual &&\n>   \t\t\ttest_cmp expect actual &&\n> -\t\t\techo \"gitdir: ../.git/modules/repo_new\" >expect &&\n> +\t\t\techo \"gitdir: ../.git/submodules/repo_new\" >expect &&\n>   \t\t\ttest_cmp expect .git\n>   \t\t) &&\n>   \t\techo \"repo\" >expect &&\n> @@ -1045,8 +1045,8 @@ test_expect_success 'recursive relative submodules stay relative' '\n>   \t(\n>   \t\tcd clone2 &&\n>   \t\tgit submodule update --init --recursive &&\n> -\t\techo \"gitdir: ../.git/modules/sub3\" >./sub3/.git_expect &&\n> -\t\techo \"gitdir: ../../../.git/modules/sub3/modules/dirdir/subsub\" >./sub3/dirdir/subsub/.git_expect\n> +\t\techo \"gitdir: ../.git/submodules/sub3\" >./sub3/.git_expect &&\n> +\t\techo \"gitdir: ../../../.git/submodules/sub3/submodules/dirdir/subsub\" >./sub3/dirdir/subsub/.git_expect\n>   \t) &&\n>   \ttest_cmp clone2/sub3/.git_expect clone2/sub3/.git &&\n>   \ttest_cmp clone2/sub3/dirdir/subsub/.git_expect clone2/sub3/dirdir/subsub/.git\n> @@ -1108,8 +1108,8 @@ test_expect_success 'submodule deinit should remove the whole submodule section\n>   '\n>   \n>   test_expect_success 'submodule deinit should unset core.worktree' '\n> -\ttest_path_is_file .git/modules/example/config &&\n> -\ttest_must_fail git config -f .git/modules/example/config core.worktree\n> +\ttest_path_is_file .git/submodules/example/config &&\n> +\ttest_must_fail git config -f .git/submodules/example/config core.worktree\n>   '\n>   \n>   test_expect_success 'submodule deinit from subdirectory' '\n> @@ -1231,7 +1231,7 @@ test_expect_success 'submodule deinit absorbs .git directory if .git is a direct\n>   \t(\n>   \t\tcd init &&\n>   \t\trm .git &&\n> -\t\tmv ../.git/modules/example .git &&\n> +\t\tmv ../.git/submodules/example .git &&\n>   \t\tGIT_WORK_TREE=. git config --unset core.worktree\n>   \t) &&\n>   \tgit submodule deinit init &&\n> diff --git a/t/t7406-submodule-update.sh b/t/t7406-submodule-update.sh\n> index 3adab12091..f0c4da1ffa 100755\n> --- a/t/t7406-submodule-update.sh\n> +++ b/t/t7406-submodule-update.sh\n> @@ -864,7 +864,7 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir' '\n>   \t (cd deeper/submodule &&\n>   \t  git log > ../../expected\n>   \t ) &&\n> -\t (cd .git/modules/deeper/submodule &&\n> +\t (cd .git/submodules/deeper/submodule &&\n>   \t  git log > ../../../../actual\n>   \t ) &&\n>   \t test_cmp expected actual\n> @@ -882,7 +882,7 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir' '\n>   \t (cd deeper/submodule &&\n>   \t  git log > ../../expected\n>   \t ) &&\n> -\t (cd .git/modules/deeper/submodule &&\n> +\t (cd .git/submodules/deeper/submodule &&\n>   \t  git log > ../../../../actual\n>   \t ) &&\n>   \t test_cmp expected actual\n> @@ -899,7 +899,7 @@ test_expect_success 'submodule add places git-dir in superprojects git-dir recur\n>   \t  git commit -m \"added subsubmodule\" &&\n>   \t  git push origin :\n>   \t ) &&\n> -\t (cd .git/modules/deeper/submodule/modules/subsubmodule &&\n> +\t (cd .git/submodules/deeper/submodule/submodules/subsubmodule &&\n>   \t  git log > ../../../../../actual\n>   \t ) &&\n>   \t git add deeper/submodule &&\n> @@ -949,7 +949,7 @@ test_expect_success 'submodule update places git-dir in superprojects git-dir re\n>   \t (cd submodule/subsubmodule &&\n>   \t  git log > ../../expected\n>   \t ) &&\n> -\t (cd .git/modules/submodule/modules/subsubmodule &&\n> +\t (cd .git/submodules/submodule/submodules/subsubmodule &&\n>   \t  git log > ../../../../../actual\n>   \t ) &&\n>   \t test_cmp expected actual\n> @@ -1298,7 +1298,7 @@ test_expect_success CASE_INSENSITIVE_FS,SYMLINKS \\\n>   \tgit init captain &&\n>   \t(\n>   \t\tcd captain &&\n> -\t\tgit submodule add --name x/y \"$hook_repo_path\" A/modules/x &&\n> +\t\tgit submodule add --name x/y \"$hook_repo_path\" A/submodules/x &&\n>   \t\ttest_tick &&\n>   \t\tgit commit -m add-submodule &&\n>   \n> diff --git a/t/t7407-submodule-foreach.sh b/t/t7407-submodule-foreach.sh\n> index 77b6d0040e..75ba826968 100755\n> --- a/t/t7407-submodule-foreach.sh\n> +++ b/t/t7407-submodule-foreach.sh\n> @@ -368,9 +368,9 @@ test_expect_success 'test \"update --recursive\" with a flag with spaces' '\n>   \t\tgit rev-parse --resolve-git-dir nested1/.git &&\n>   \t\tgit rev-parse --resolve-git-dir nested1/nested2/.git &&\n>   \t\tgit rev-parse --resolve-git-dir nested1/nested2/nested3/.git &&\n> -\t\ttest -f .git/modules/nested1/objects/info/alternates &&\n> -\t\ttest -f .git/modules/nested1/modules/nested2/objects/info/alternates &&\n> -\t\ttest -f .git/modules/nested1/modules/nested2/modules/nested3/objects/info/alternates\n> +\t\ttest -f .git/submodules/nested1/objects/info/alternates &&\n> +\t\ttest -f .git/submodules/nested1/submodules/nested2/objects/info/alternates &&\n> +\t\ttest -f .git/submodules/nested1/submodules/nested2/submodules/nested3/objects/info/alternates\n>   \t)\n>   '\n>   \n> diff --git a/t/t7408-submodule-reference.sh b/t/t7408-submodule-reference.sh\n> index f860e7bbf4..25f4aec57e 100755\n> --- a/t/t7408-submodule-reference.sh\n> +++ b/t/t7408-submodule-reference.sh\n> @@ -61,7 +61,7 @@ test_expect_success 'submodule add --reference uses alternates' '\n>   \t\tgit commit -m B-super-added &&\n>   \t\tgit repack -ad\n>   \t) &&\n> -\ttest_alternate_is_used super/.git/modules/sub/objects/info/alternates super/sub\n> +\ttest_alternate_is_used super/.git/submodules/sub/objects/info/alternates super/sub\n>   '\n>   \n>   test_expect_success 'submodule add --reference with --dissociate does not use alternates' '\n> @@ -71,7 +71,7 @@ test_expect_success 'submodule add --reference with --dissociate does not use al\n>   \t\tgit commit -m B-super-added &&\n>   \t\tgit repack -ad\n>   \t) &&\n> -\ttest_path_is_missing super/.git/modules/sub-dissociate/objects/info/alternates\n> +\ttest_path_is_missing super/.git/submodules/sub-dissociate/objects/info/alternates\n>   '\n>   \n>   test_expect_success 'that reference gets used with add' '\n> @@ -94,14 +94,14 @@ test_expect_success 'updating superproject keeps alternates' '\n>   \ttest_when_finished \"rm -rf super-clone\" &&\n>   \tgit clone super super-clone &&\n>   \tgit -C super-clone submodule update --init --reference ../B &&\n> -\ttest_alternate_is_used super-clone/.git/modules/sub/objects/info/alternates super-clone/sub\n> +\ttest_alternate_is_used super-clone/.git/submodules/sub/objects/info/alternates super-clone/sub\n>   '\n>   \n>   test_expect_success 'updating superproject with --dissociate does not keep alternates' '\n>   \ttest_when_finished \"rm -rf super-clone\" &&\n>   \tgit clone super super-clone &&\n>   \tgit -C super-clone submodule update --init --reference ../B --dissociate &&\n> -\ttest_path_is_missing super-clone/.git/modules/sub/objects/info/alternates\n> +\ttest_path_is_missing super-clone/.git/submodules/sub/objects/info/alternates\n>   '\n>   \n>   test_expect_success 'submodules use alternates when cloning a superproject' '\n> @@ -112,7 +112,7 @@ test_expect_success 'submodules use alternates when cloning a superproject' '\n>   \t\t# test superproject has alternates setup correctly\n>   \t\ttest_alternate_is_used .git/objects/info/alternates . &&\n>   \t\t# test submodule has correct setup\n> -\t\ttest_alternate_is_used .git/modules/sub/objects/info/alternates sub\n> +\t\ttest_alternate_is_used .git/submodules/sub/objects/info/alternates sub\n>   \t)\n>   '\n>   \n> @@ -127,7 +127,7 @@ test_expect_success 'missing submodule alternate fails clone and submodule updat\n>   \t\t# update of the submodule succeeds\n>   \t\ttest_must_fail git submodule update --init &&\n>   \t\t# and we have no alternates:\n> -\t\ttest_path_is_missing .git/modules/sub/objects/info/alternates &&\n> +\t\ttest_path_is_missing .git/submodules/sub/objects/info/alternates &&\n>   \t\ttest_path_is_missing sub/file1\n>   \t)\n>   '\n> @@ -142,7 +142,7 @@ test_expect_success 'ignoring missing submodule alternates passes clone and subm\n>   \t\t# update of the submodule succeeds\n>   \t\tgit submodule update --init &&\n>   \t\t# and we have no alternates:\n> -\t\ttest_path_is_missing .git/modules/sub/objects/info/alternates &&\n> +\t\ttest_path_is_missing .git/submodules/sub/objects/info/alternates &&\n>   \t\ttest_path_is_file sub/file1\n>   \t)\n>   '\n> @@ -176,18 +176,18 @@ test_expect_success 'nested submodule alternate in works and is actually used' '\n>   \t\t# test superproject has alternates setup correctly\n>   \t\ttest_alternate_is_used .git/objects/info/alternates . &&\n>   \t\t# immediate submodule has alternate:\n> -\t\ttest_alternate_is_used .git/modules/subwithsub/objects/info/alternates subwithsub &&\n> +\t\ttest_alternate_is_used .git/submodules/subwithsub/objects/info/alternates subwithsub &&\n>   \t\t# nested submodule also has alternate:\n> -\t\ttest_alternate_is_used .git/modules/subwithsub/modules/sub/objects/info/alternates subwithsub/sub\n> +\t\ttest_alternate_is_used .git/submodules/subwithsub/submodules/sub/objects/info/alternates subwithsub/sub\n>   \t)\n>   '\n>   \n>   check_that_two_of_three_alternates_are_used() {\n>   \ttest_alternate_is_used .git/objects/info/alternates . &&\n>   \t# immediate submodule has alternate:\n> -\ttest_alternate_is_used .git/modules/subwithsub/objects/info/alternates subwithsub &&\n> +\ttest_alternate_is_used .git/submodules/subwithsub/objects/info/alternates subwithsub &&\n>   \t# but nested submodule has no alternate:\n> -\ttest_path_is_missing .git/modules/subwithsub/modules/sub/objects/info/alternates\n> +\ttest_path_is_missing .git/submodules/subwithsub/submodules/sub/objects/info/alternates\n>   }\n>   \n>   \n> diff --git a/t/t7412-submodule-absorbgitdirs.sh b/t/t7412-submodule-absorbgitdirs.sh\n> index 0490499573..dbaca9c69f 100755\n> --- a/t/t7412-submodule-absorbgitdirs.sh\n> +++ b/t/t7412-submodule-absorbgitdirs.sh\n> @@ -29,13 +29,13 @@ test_expect_success 'absorb the git dir' '\n>   \tcat >expect <<-EOF &&\n>   \tMigrating git directory of '\\''sub1'\\'' from\n>   \t'\\''$cwd/sub1/.git'\\'' to\n> -\t'\\''$cwd/.git/modules/sub1'\\''\n> +\t'\\''$cwd/.git/submodules/sub1'\\''\n>   \tEOF\n>   \tgit submodule absorbgitdirs 2>actual &&\n>   \ttest_cmp expect actual &&\n>   \tgit fsck &&\n>   \ttest -f sub1/.git &&\n> -\ttest -d .git/modules/sub1 &&\n> +\ttest -d .git/submodules/sub1 &&\n>   \tgit status >actual.1 &&\n>   \tgit -C sub1 rev-parse HEAD >actual.2 &&\n>   \ttest_cmp expect.1 actual.1 &&\n> @@ -47,7 +47,7 @@ test_expect_success 'absorbing does not fail for deinitialized submodules' '\n>   \tgit submodule deinit --all &&\n>   \tgit submodule absorbgitdirs 2>err &&\n>   \ttest_must_be_empty err &&\n> -\ttest -d .git/modules/sub1 &&\n> +\ttest -d .git/submodules/sub1 &&\n>   \ttest -d sub1 &&\n>   \t! test -e sub1/.git\n>   '\n> @@ -68,12 +68,12 @@ test_expect_success 'absorb the git dir in a nested submodule' '\n>   \tcat >expect <<-EOF &&\n>   \tMigrating git directory of '\\''sub1/nested'\\'' from\n>   \t'\\''$cwd/sub1/nested/.git'\\'' to\n> -\t'\\''$cwd/.git/modules/sub1/modules/nested'\\''\n> +\t'\\''$cwd/.git/submodules/sub1/submodules/nested'\\''\n>   \tEOF\n>   \tgit submodule absorbgitdirs 2>actual &&\n>   \ttest_cmp expect actual &&\n>   \ttest -f sub1/nested/.git &&\n> -\ttest -d .git/modules/sub1/modules/nested &&\n> +\ttest -d .git/submodules/sub1/submodules/nested &&\n>   \tgit status >actual.1 &&\n>   \tgit -C sub1/nested rev-parse HEAD >actual.2 &&\n>   \ttest_cmp expect.1 actual.1 &&\n> @@ -84,11 +84,11 @@ test_expect_success 're-setup nested submodule' '\n>   \t# un-absorb the direct submodule, to test if the nested submodule\n>   \t# is still correct (needs a rewrite of the gitfile only)\n>   \trm -rf sub1/.git &&\n> -\tmv .git/modules/sub1 sub1/.git &&\n> +\tmv .git/submodules/sub1 sub1/.git &&\n>   \tGIT_WORK_TREE=. git -C sub1 config --unset core.worktree &&\n>   \t# fixup the nested submodule\n> -\techo \"gitdir: ../.git/modules/nested\" >sub1/nested/.git &&\n> -\tGIT_WORK_TREE=../../../nested git -C sub1/.git/modules/nested config \\\n> +\techo \"gitdir: ../.git/submodules/nested\" >sub1/nested/.git &&\n> +\tGIT_WORK_TREE=../../../nested git -C sub1/.git/submodules/nested config \\\n>   \t\tcore.worktree \"../../../nested\" &&\n>   \t# make sure this re-setup is correct\n>   \tgit status --ignore-submodules=none &&\n> @@ -105,13 +105,13 @@ test_expect_success 'absorb the git dir in a nested submodule' '\n>   \tcat >expect <<-EOF &&\n>   \tMigrating git directory of '\\''sub1'\\'' from\n>   \t'\\''$cwd/sub1/.git'\\'' to\n> -\t'\\''$cwd/.git/modules/sub1'\\''\n> +\t'\\''$cwd/.git/submodules/sub1'\\''\n>   \tEOF\n>   \tgit submodule absorbgitdirs 2>actual &&\n>   \ttest_cmp expect actual &&\n>   \ttest -f sub1/.git &&\n>   \ttest -f sub1/nested/.git &&\n> -\ttest -d .git/modules/sub1/modules/nested &&\n> +\ttest -d .git/submodules/sub1/submodules/nested &&\n>   \tgit status >actual.1 &&\n>   \tgit -C sub1/nested rev-parse HEAD >actual.2 &&\n>   \ttest_cmp expect.1 actual.1 &&\n> @@ -133,7 +133,7 @@ test_expect_success 'absorb the git dir outside of primary worktree' '\n>   \tcat >expect <<-EOF &&\n>   \tMigrating git directory of '\\''sub2'\\'' from\n>   \t'\\''$cwd/repo-wt/sub2/.git'\\'' to\n> -\t'\\''$cwd/repo-bare.git/worktrees/repo-wt/modules/sub2'\\''\n> +\t'\\''$cwd/repo-bare.git/worktrees/repo-wt/submodules/sub2'\\''\n>   \tEOF\n>   \tgit -C repo-wt submodule absorbgitdirs 2>actual &&\n>   \ttest_cmp expect actual\n> diff --git a/t/t7423-submodule-symlinks.sh b/t/t7423-submodule-symlinks.sh\n> index 3d3c7af3ce..a51235136d 100755\n> --- a/t/t7423-submodule-symlinks.sh\n> +++ b/t/t7423-submodule-symlinks.sh\n> @@ -49,19 +49,19 @@ test_expect_success SYMLINKS 'git restore --recurse-submodules must not be confu\n>   \n>   test_expect_success SYMLINKS 'git restore --recurse-submodules must not migrate git dir of symlinked repo' '\n>   \tprepare_symlink_to_repo &&\n> -\trm -rf .git/modules &&\n> +\trm -rf .git/submodules &&\n>   \ttest_must_fail git restore --recurse-submodules a/sm &&\n>   \ttest_path_is_dir a/target/.git &&\n> -\ttest_path_is_missing .git/modules/a/sm &&\n> +\ttest_path_is_missing .git/submodules/a/sm &&\n>   \ttest_path_is_missing a/target/submodule_file\n>   '\n>   \n>   test_expect_success SYMLINKS 'git checkout -f --recurse-submodules must not migrate git dir of symlinked repo when removing submodule' '\n>   \tprepare_symlink_to_repo &&\n> -\trm -rf .git/modules &&\n> +\trm -rf .git/submodules &&\n>   \ttest_must_fail git checkout -f --recurse-submodules initial &&\n>   \ttest_path_is_dir a/target/.git &&\n> -\ttest_path_is_missing .git/modules/a/sm\n> +\ttest_path_is_missing .git/submodules/a/sm\n>   '\n>   \n>   test_done\n> diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\n> index f512eed278..4e2ced3636 100755\n> --- a/t/t7450-bad-git-dotfiles.sh\n> +++ b/t/t7450-bad-git-dotfiles.sh\n> @@ -77,28 +77,28 @@ test_expect_success 'create innocent subrepo' '\n>   \n>   test_expect_success 'submodule add refuses invalid names' '\n>   \ttest_must_fail \\\n> -\t\tgit submodule add --name ../../modules/evil \"$PWD/innocent\" evil\n> +\t\tgit submodule add --name ../../submodules/evil \"$PWD/innocent\" evil\n>   '\n>   \n>   test_expect_success 'add evil submodule' '\n>   \tgit submodule add \"$PWD/innocent\" evil &&\n>   \n> -\tmkdir modules &&\n> -\tcp -r .git/modules/evil modules &&\n> -\twrite_script modules/evil/hooks/post-checkout <<-\\EOF &&\n> +\tmkdir submodules &&\n> +\tcp -r .git/submodules/evil submodules &&\n> +\twrite_script submodules/evil/hooks/post-checkout <<-\\EOF &&\n>   \techo >&2 \"RUNNING POST CHECKOUT\"\n>   \tEOF\n>   \n>   \tgit config -f .gitmodules submodule.evil.update checkout &&\n>   \tgit config -f .gitmodules --rename-section \\\n> -\t\tsubmodule.evil submodule.../../modules/evil &&\n> -\tgit add modules &&\n> +\t\tsubmodule.evil submodule.../../submodules/evil &&\n> +\tgit add submodules &&\n>   \tgit commit -am evil\n>   '\n>   \n>   # This step seems like it shouldn't be necessary, since the payload is\n>   # contained entirely in the evil submodule. But due to the vagaries of the\n> -# submodule code, checking out the evil module will fail unless \".git/modules\"\n> +# submodule code, checking out the evil module will fail unless \".git/submodules\"\n>   # exists. Adding another submodule (with a name that sorts before \"evil\") is an\n>   # easy way to make sure this is the case in the victim clone.\n>   test_expect_success 'add other submodule' '\n> @@ -350,8 +350,8 @@ test_expect_success 'submodule git dir nesting detection must work with parallel\n>   \tcat err &&\n>   \tgrep -E \"(already exists|is inside git dir|not a git repository)\" err &&\n>   \t{\n> -\t\ttest_path_is_missing .git/modules/hippo/HEAD ||\n> -\t\ttest_path_is_missing .git/modules/hippo/hooks/HEAD\n> +\t\ttest_path_is_missing .git/submodules/hippo/HEAD ||\n> +\t\ttest_path_is_missing .git/submodules/hippo/hooks/HEAD\n>   \t}\n>   '\n>   \n> @@ -361,10 +361,10 @@ test_expect_success 'checkout -f --recurse-submodules must not use a nested gitd\n>   \t\tcd nested_checkout &&\n>   \t\tgit submodule init &&\n>   \t\tgit submodule update thing1 &&\n> -\t\tmkdir -p .git/modules/hippo/hooks/refs &&\n> -\t\tmkdir -p .git/modules/hippo/hooks/objects/info &&\n> -\t\techo \"../../../../objects\" >.git/modules/hippo/hooks/objects/info/alternates &&\n> -\t\techo \"ref: refs/heads/master\" >.git/modules/hippo/hooks/HEAD\n> +\t\tmkdir -p .git/submodules/hippo/hooks/refs &&\n> +\t\tmkdir -p .git/submodules/hippo/hooks/objects/info &&\n> +\t\techo \"../../../../objects\" >.git/submodules/hippo/hooks/objects/info/alternates &&\n> +\t\techo \"ref: refs/heads/master\" >.git/submodules/hippo/hooks/HEAD\n>   \t) &&\n>   \ttest_must_fail git -C nested_checkout checkout -f --recurse-submodules HEAD 2>err &&\n>   \tcat err &&\n> @@ -390,13 +390,13 @@ test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into d\n>   \tgit config unset -f repo/.gitmodules submodule.sub.path &&\n>   \tprintf \"\\tpath = \\\"sub\\r\\\"\\n\" >>repo/.gitmodules &&\n>   \n> -\tgit config unset -f repo/.git/modules/sub/config core.worktree &&\n> +\tgit config unset -f repo/.git/submodules/sub/config core.worktree &&\n>   \t{\n>   \t\tprintf \"[core]\\n\" &&\n>   \t\tprintf \"\\tworktree = \\\"../../../sub\\r\\\"\\n\"\n> -\t} >>repo/.git/modules/sub/config &&\n> +\t} >>repo/.git/submodules/sub/config &&\n>   \n> -\tln -s .git/modules/sub/hooks repo/sub &&\n> +\tln -s .git/submodules/sub/hooks repo/sub &&\n>   \tgit -C repo add -A &&\n>   \tgit -C repo commit -m submodule &&\n>   \n> diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh\n> index 409cd0cd12..ded482fdf2 100755\n> --- a/t/t7527-builtin-fsmonitor.sh\n> +++ b/t/t7527-builtin-fsmonitor.sh\n> @@ -866,7 +866,7 @@ test_expect_success 'submodule always visited' '\n>   '\n>   \n>   # If a submodule has a `sub/.git/` directory (rather than a file\n> -# pointing to the super's `.git/modules/sub`) and `core.fsmonitor`\n> +# pointing to the super's `.git/submodules/sub`) and `core.fsmonitor`\n>   # turned on in the submodule and the daemon is not yet started in\n>   # the submodule, and someone does a `git submodule absorbgitdirs`\n>   # in the super, Git will recursively invoke `git submodule--helper`\n> @@ -895,7 +895,7 @@ test_expect_success \"submodule absorbgitdirs implicitly starts daemon\" '\n>   \tcat >expect <<-EOF &&\n>   \tMigrating git directory of '\\''dir_1/dir_2/sub'\\'' from\n>   \t'\\''$cwd/dir_1/dir_2/sub/.git'\\'' to\n> -\t'\\''$cwd/.git/modules/dir_1/dir_2/sub'\\''\n> +\t'\\''$cwd/.git/submodules/dir_1/dir_2/sub'\\''\n>   \tEOF\n>   \tGIT_TRACE2_EVENT=\"$PWD/super-sub.trace\" \\\n>   \t\tgit -C super submodule absorbgitdirs >out 2>actual &&\n\n"},{"id":"525847","messageId":"877by9ndzt.fsf@ratioveremundo.com","threadId":"63967","inReplyTo":"fc69ee66-815f-48ec-a5fb-99cac5f4d58c@gmail.com","subject":"Re: [PATCH 2/9] submodule: create new gitdirs under submodules path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T15:46:46Z","receivedAt":"2025-09-08T15:47:15Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 08 Sep 2025, Phillip Wood <phillip.wood123@gmail.com> \nwrote:\n> Hi Adrian \n>\n\nHello Phillip and thanks for the feedback! :)\n\nI just sent v2 at the same time if you want to give that a look, \nthough the issues you raised are still valid for v2 as well.\n \n> On 16/08/2025 22:36, Adrian Ratiu wrote: \n>> This is in preparation for encoding the submodule names to \n>> avoid conflicts like submodules named foo and foo/bar together \n>> with case-insensitive file- system handling and other corner \n>> cases like reserved filenames on Windows.   Backward \n>> compatibility is kept with plain-name modules already existing \n>> at paths like .git/modules/<name>, however a clear separation \n>> between legacy (plain) and new (encoded) namespaces is \n>> desirable, to avoid situations like an existing plain-name \n>> module containing the encoding escape character/  Thus we split \n>> the new-style (encoded) gitdir name paths to .git/submodules, \n>> while legacy-style paths remain under .git/modules.   This is \n>> just a default directory change with the accompanying test \n>> updates, in preparation for the actual encoding additions in \n>> future commits. \n> \n> Does this need an extentions.submoduleEncoding (name suggestions \n> welcome) config key to stop older versions of git trying to read \n> the  repository as they wont be able to locate the gitdir of any \n> submodules  added under .git/submodules? \n \nVery good point. I'm a bit unsure we actually need it, likely we \ndo.\n\nOn the one hand, older versions of git can still initialize and \nwork on submodules under the legacy .git/modules/ path ignoring \nthe new one...\n\nOn the other hand, there is a non-zero risk users will get in \ntrouble by switching git versions or can lead to \ninconsistent/corrupted states, so I'm inclined to say the answer \nis yes: better safe than sorry.\n\nSo if there are no objections or better ideas, I'll add this in v3.\n"},{"id":"525848","messageId":"20250908155146.GA1308482@coredump.intra.peff.net","threadId":"63967","inReplyTo":"20250908140117.262205-8-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 07/10] submodule: error out if gitdir name is too long","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2025-09-08T15:51:46Z","receivedAt":"2025-09-08T15:51:51Z","isPatch":true,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Mon, Sep 08, 2025 at 05:01:14PM +0300, Adrian Ratiu wrote:\n\n> Encoding submodule names increases their name size, so there is an\n> increased risk to hit the max filename length in the gitdir path.\n> (the likelihood is still rather small, so it's an acceptable risk)\n> \n> This gitdir file-name-too-long corner case can be be addressed in\n> multiple ways, including sharding or trimming, however for now, just\n> add the portable logic (suggested by Peff) to detect the corner case\n> then error out to avoid comitting to a specific policy (or policies).\n\nThanks, the compat logic here looks reasonable to me.\n\nAs somebody who has not really been looking into or thought about the\ntopic at all, though, I wondered how necessary pathconf() is here. That\nis, I can imagine two alternatives:\n\n - just try to use the path, and we either get an error from\n   open()/mkdir() or we don't. This would end up with roughly the same\n   outcome as the current code which calls die(), though it would not\n   help with eventually fulfilling your TODO.\n\n - set some arbitrary but sane limit (say, 255?). That would make the\n   behavior consistent across platforms, though it does mean you might\n   be prevented from using very long submodule names on systems that\n   could support it.\n\nI dunno. Like I said, this is not a problem I thought a lot about, so\nfeel free to ignore. Mostly I just notice that we have lived for 20+\nyears without pathconf, I think mostly by following the philosophy of\nthe first bullet point above.\n\n-Peff\n"},{"id":"525865","messageId":"874itcoofp.fsf@collabora.com","threadId":"63967","inReplyTo":"20250908155146.GA1308482@coredump.intra.peff.net","subject":"Re: [PATCH v2 07/10] submodule: error out if gitdir name is too long","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-08T17:15:54Z","receivedAt":"2025-09-08T17:16:18Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 08 Sep 2025, Jeff King <peff@peff.net> wrote:\n> On Mon, Sep 08, 2025 at 05:01:14PM +0300, Adrian Ratiu wrote: \n> \n>> Encoding submodule names increases their name size, so there is \n>> an increased risk to hit the max filename length in the gitdir \n>> path.  (the likelihood is still rather small, so it's an \n>> acceptable risk)  This gitdir file-name-too-long corner case \n>> can be be addressed in multiple ways, including sharding or \n>> trimming, however for now, just add the portable logic \n>> (suggested by Peff) to detect the corner case then error out to \n>> avoid comitting to a specific policy (or policies). \n> \n> Thanks, the compat logic here looks reasonable to me. \n> \n> As somebody who has not really been looking into or thought \n> about the topic at all, though, I wondered how necessary \n> pathconf() is here. That is, I can imagine two alternatives: \n> \n>  - just try to use the path, and we either get an error from \n>    open()/mkdir() or we don't. This would end up with roughly \n>    the same outcome as the current code which calls die(), \n>    though it would not help with eventually fulfilling your \n>    TODO. \n> \n>  - set some arbitrary but sane limit (say, 255?). That would \n>  make the \n>    behavior consistent across platforms, though it does mean you \n>    might be prevented from using very long submodule names on \n>    systems that could support it. \n> \n> I dunno. Like I said, this is not a problem I thought a lot \n> about, so feel free to ignore. Mostly I just notice that we have \n> lived for 20+ years without pathconf, I think mostly by \n> following the philosophy of the first bullet point above. \n> \n> -Peff \n\nI can go either (thrice?) way with this. :) No strong opinion.\n\n1. Just let the OS/filesystem default do its own thing: not doing \nanything is the easiest and perhaps the best approach sometimes.\n\n2. Benefit of hardcoding: we already hardcode NAME_MAX in \ncompat/posix.h for platforms which don't have it and fallback to \nit. Might as well use it and print a nice error.\n\n3. The compat layer allows us to at least try and detect the \ncorner-case because we slightly increase the risk of hitting it \n(say you have a module name with length >200, encoding it might \npush beyond the limit).\n\nRight now it's worth it mostly for the error msg telling the user \nwhy the command fails because a default -ENAMETOOLONG might be \nconfusing (the plain-text name could be just 200 < NAME_MAX), \nwhile also opening the door to avoid this situation entirely by \naddressing the TODO.\n\nAgain, I'm fine either way.\n\nSince I split this logic into a separate commit, we could even \ndrop it now and bring it back later when (if?) we decide to tackle \nthe TODO (series is big enough already).\n\nThanks,\nAdrian\n"},{"id":"525916","messageId":"aL_Z6z1XZBEbNGV1@pks.im","threadId":"63967","inReplyTo":"20250908140117.262205-3-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 02/10] submodule: create new gitdirs under submodules path","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-09T07:40:27Z","receivedAt":"2025-09-09T07:40:34Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 08, 2025 at 05:01:09PM +0300, Adrian Ratiu wrote:\n> This is in preparation for encoding the submodule names to avoid conflicts\n> like submodules named foo and foo/bar together with case-insensitive file-\n> system handling and other corner cases like reserved filenames on Windows.\n> \n> Backward compatibility is kept with plain-name modules already existing at\n> paths like .git/modules/<name>, however a clear separation between legacy\n> (plain) and new (encoded) namespaces is desirable, to avoid situations like\n> an existing plain-name module containing the encoding escape character/\n> \n> Thus we split the new-style (encoded) gitdir name paths to .git/submodules,\n> while legacy-style paths remain under .git/modules.\n> \n> This is just a default directory change with the accompanying test updates,\n> in preparation for the actual encoding additions in future commits.\n\nOne of the questions here is how this move will affect alternate\nimplementations of Git, like libgit2, JGit or Gitoxide. There's two\nangles to this:\n\n  - Git needs to handle that those implementations continue to write\n    submodules into \".git/modules\".\n\n  - These implementations need to be able to handle the new-style paths.\n\nThe first item should work just fine, as we make sure that we handle\nboth paths. But do the other implementations need any adjustment? I\nguess the answer is \"yes\", so we need to treat this as a backwards\nincompatible change as they wouldn't be able to find the submodule\nrepositories anymore, right?\n\nIdeally, the way that submodules were populated was less fragile. For\nexample, we could have a \"submodule.*.repoPath\" config key that gets\npopulated whenever we clone a submodule. If Git clients knew to use that\nfield they wouldn't have to second-guess where a previous Git client\nstored a specific submodule, but they could just read that path and then\nuse whatever is stored therein. This would even allow for changes like\nusing a hash to encode the submodule name.\n\nBut to the best of my knowledge such a key does not currently exist,\nwhich is too bad (please correct me if I'm wrong, I'm definitely not an\nexpert when it comes to submodules).\n\nPatrick\n"},{"id":"525917","messageId":"aL_Z8DusXdyIr4Ru@pks.im","threadId":"63967","inReplyTo":"20250908140117.262205-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 03/10] submodule: add gitdir path config override","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-09T07:40:32Z","receivedAt":"2025-09-09T07:40:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 08, 2025 at 05:01:10PM +0300, Adrian Ratiu wrote:\n> This adds an ability to override gitdir paths via config files\n> (not .gitmodules), such that any encoding scheme can be changed\n> and JGit & co don't need to exactly match the default encoding.\n> \n> A new test and a helper are added. The helper will be used by\n> further tests exercising gitdir paths & encodings.\n\nAha, so you already do what I'm lamenting about in the preceding commit.\nIt still raises the question around how to handle this whole migration\nnow, as alternative implementations don't yet know about this specific\nconfig key. Doing all of this in a single patch series will most likely\nresult in breakage.\n\nIn theory, we should probably first introduce the configuration, then\nwait a couple releases for alternative implementations to catch up, and\nthen switch over. But we still cannot be sure that implementations know\nto handle this key alright.\n\nA heavy-handed solution to this would be to introduce a repository\nextension. This would ensure that any well-behaved Git client will\nrefuse to open the repository if it doesn't know about that specific\nextension. With the remaining ones we can be sure that they know to\nhandle the \"submodule.*.gitdir\" configuration.\n\nAs mentioned, extensions are rather heavy-handed. Furthermore, this\nwhole infra is only really needed under very specific circumstances. So\nmaybe it could be a viable approach to make this extension opt-in:\n\n  - We provide a new configuration \"init.useSubmoduleGitdirExtension\"\n    that tells git-init(1) and git-clone(1) to use the new extension for\n    newly initialized repositories.\n\n  - We detect the situation where a submodule cannot be cloned due to a\n    path conflict. If detected, we print a user-facing hint that tells\n    them to enable the extension.\n\nOnce the user enabled the extension we'll know to use new, encoded\nsubmodule paths from thereon and thus re-initializing the conflicting\nsubmodule should work now.\n\nWith all of this I wonder whether we even need a new \".git/submodules\"\ndirectory. Couldn't we just continue to create submodules in the old\npath and for example create a random suffix as required?\n\nIn any case, please stop me if I'm going overboard with my backwards\ncompatibility concerns :)\n\nPatrick\n"},{"id":"525926","messageId":"5290c591-fd3d-4737-bfcb-fc091751af1a@gmail.com","threadId":"63967","inReplyTo":"877by9ndzt.fsf@ratioveremundo.com","subject":"Re: [PATCH 2/9] submodule: create new gitdirs under submodules path","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-09T08:53:46Z","receivedAt":"2025-09-09T08:53:52Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Adrian\n\nOn 08/09/2025 16:46, Adrian Ratiu wrote:\n> On Mon, 08 Sep 2025, Phillip Wood <phillip.wood123@gmail.com> wrote:\n>>\n>> Does this need an extentions.submoduleEncoding (name suggestions \n>> welcome) config key to stop older versions of git trying to read the  \n>> repository as they wont be able to locate the gitdir of any \n>> submodules  added under .git/submodules? \n> \n> Very good point. I'm a bit unsure we actually need it, likely we do.\n> \n> On the one hand, older versions of git can still initialize and work on \n> submodules under the legacy .git/modules/ path ignoring the new one...\n> \n> On the other hand, there is a non-zero risk users will get in trouble by \n> switching git versions or can lead to inconsistent/corrupted states, so \n> I'm inclined to say the answer is yes: better safe than sorry.\n\nIf we only needed to convert the submodule name to a gitdir when the \nsubmodule was initialized and all other access went through the .git \nfile of the submodule in the working tree then I think old clients would \nbe fine because they'd find the right gitdir by reading the .git file. \nI'm not familiar with the submodule code but I don't think that's the \ncase in which case I agree it would be safer to add an \"extestions\" \nconfig key.\n\nThanks\n\nPhillip\n\n"},{"id":"525942","messageId":"87jz277v14.fsf@gentoo.mail-host-address-is-not-set","threadId":"63967","inReplyTo":"5290c591-fd3d-4737-bfcb-fc091751af1a@gmail.com","subject":"Re: [PATCH 2/9] submodule: create new gitdirs under submodules path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-09T10:57:43Z","receivedAt":"2025-09-09T10:58:15Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 09 Sep 2025, Phillip Wood <phillip.wood123@gmail.com> \nwrote:\n> Hi Adrian \n> \n> On 08/09/2025 16:46, Adrian Ratiu wrote: \n>> On Mon, 08 Sep 2025, Phillip Wood <phillip.wood123@gmail.com> \n>> wrote: \n>>> \n>>> Does this need an extentions.submoduleEncoding (name \n>>> suggestions  welcome) config key to stop older versions of git \n>>> trying to read the   repository as they wont be able to locate \n>>> the gitdir of any  submodules  added under .git/submodules?  \n>>  Very good point. I'm a bit unsure we actually need it, likely \n>> we do.   On the one hand, older versions of git can still \n>> initialize and work on  submodules under the legacy \n>> .git/modules/ path ignoring the new one...   On the other hand, \n>> there is a non-zero risk users will get in trouble by \n>> switching git versions or can lead to inconsistent/corrupted \n>> states, so  I'm inclined to say the answer is yes: better safe \n>> than sorry. \n> \n> If we only needed to convert the submodule name to a gitdir when \n> the  submodule was initialized and all other access went through \n> the .git  file of the submodule in the working tree then I think \n> old clients would  be fine because they'd find the right gitdir \n> by reading the .git file.  I'm not familiar with the submodule \n> code but I don't think that's the  case in which case I agree it \n> would be safer to add an \"extestions\"  config key. \n\nYes, your understanding is correct and it goes beyond just the git core\n(where we at least have a unified API to compute the gitdir path, so\nmaking the subsequent accesses follow .git file contents would be easy),\nit also affects JGit, libgit2 and other implementations, so it's the\nmost prudent approach to put this behind an extension key.\n"},{"id":"525961","messageId":"87h5xb7s0v.fsf@collabora.com","threadId":"63967","inReplyTo":"ee61c97a-63a5-4c81-8859-09b896bdcf42@gmail.com","subject":"Re: [PATCH 3/9] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-09T12:02:40Z","receivedAt":"2025-09-09T12:03:15Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 08 Sep 2025, Phillip Wood <phillip.wood123@gmail.com> \nwrote:\n> Hi Adrian \n> \n> On 16/08/2025 22:36, Adrian Ratiu wrote: \n>> This adds an ability to override gitdir paths via config files \n>> (not .gitmodules), such that any encoding scheme can be changed \n>> and JGit & co don't need to exactly match the default encoding. \n> \n> Reading the old email thread you linked to in the cover letter, \n> my  understanding is that this was suggested as an alternative \n> to changing  the gitdir for submodules from \"modules\" to \n> \"submodules\". Do the later  patches set this key when encoding \n> the gitdir so that JGit can find the  gitdir by reading the \n> config? \n\nI read it more like a complementary / good-to-have feature than a \nrequirement for getting JGit & co to work or as a path to keep \n\"legacy\" and \"new/encoded\" modules in a unified dir/namespace. :)\n\nThe key is not set automatically, which also avoids unnecessary \nconfig clutter and is mostly useful to change the encoding and \nsimplify tests.\n\nI see your point though: it's not a necessary requirement for the \ngit core encoding itself, both due to the separate dir/namespace \nand because we'll add an extension in v3, so the other \nimplementations will have to explicitely add support for it.\n\nWill reword the commit msg to make it more clear in v3. Let me \nknow if you have more thoughts or any concerns.\n\nThanks,\nAdrian\n"},{"id":"525967","messageId":"87ecsf7g84.fsf@collabora.com","threadId":"63967","inReplyTo":"aL_Z6z1XZBEbNGV1@pks.im","subject":"Re: [PATCH v2 02/10] submodule: create new gitdirs under submodules path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-09T16:17:31Z","receivedAt":"2025-09-09T16:17:55Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hi Patrick and thank you for the feedback! o/\n\nOn Tue, 09 Sep 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Sep 08, 2025 at 05:01:09PM +0300, Adrian Ratiu wrote: \n>> This is in preparation for encoding the submodule names to \n>> avoid conflicts like submodules named foo and foo/bar together \n>> with case-insensitive file- system handling and other corner \n>> cases like reserved filenames on Windows.   Backward \n>> compatibility is kept with plain-name modules already existing \n>> at paths like .git/modules/<name>, however a clear separation \n>> between legacy (plain) and new (encoded) namespaces is \n>> desirable, to avoid situations like an existing plain-name \n>> module containing the encoding escape character/  Thus we split \n>> the new-style (encoded) gitdir name paths to .git/submodules, \n>> while legacy-style paths remain under .git/modules.   This is \n>> just a default directory change with the accompanying test \n>> updates, in preparation for the actual encoding additions in \n>> future commits. \n> \n> One of the questions here is how this move will affect alternate \n> implementations of Git, like libgit2, JGit or Gitoxide. There's \n> two angles to this: \n> \n>   - Git needs to handle that those implementations continue to \n>   write \n>     submodules into \".git/modules\". \n> \n>   - These implementations need to be able to handle the \n>   new-style paths. \n> \n> The first item should work just fine, as we make sure that we \n> handle both paths. But do the other implementations need any \n> adjustment? I guess the answer is \"yes\", so we need to treat \n> this as a backwards incompatible change as they wouldn't be able \n> to find the submodule repositories anymore, right?\n\nThat is correct and also applies to older versions git itself \nwhich do not have this mechanism. Phillip Wood suggested we add an \nextension like \"extensions.submoduleEncoding\" (name suggestions \nwelcome).\n\nI'll do that in v3 of this series.\n \n> \n> Ideally, the way that submodules were populated was less \n> fragile. For example, we could have a \"submodule.*.repoPath\" \n> config key that gets populated whenever we clone a submodule. If \n> Git clients knew to use that field they wouldn't have to \n> second-guess where a previous Git client stored a specific \n> submodule, but they could just read that path and then use \n> whatever is stored therein. This would even allow for changes \n> like using a hash to encode the submodule name.\n\nSlight tangent (I'll respond to your point after this):\n\nJunio asked to please keep the name human-readable and that's why \nwe use url-encoding which is also widely known and well \nunderstood.\n\nI guess we could add a config to change the name encoding or \nhashing mechanism while keeping url-encoding as the \ndefault. Likely in a later series because this one is big enough \nnow at 10 patches and keeps growing. \n\nOne of my Collabora collegues even suggested they would like to \nuse a pattern like \"hash_name\" to get the best of both worlds.\n \n> \n> But to the best of my knowledge such a key does not currently \n> exist, which is too bad (please correct me if I'm wrong, I'm \n> definitely not an expert when it comes to submodules). \n\nNo, it does not exist. I've added something a little bit similar \nwith the gitdir path config option in this series, however it is \nonly used to override default paths computed by git-submodule, \nwhen necessary. \n\nThere is also a config clutter problem, if such a key were to be \nadded by default, since most submodules use default paths.\n\nPhillip had the idea to only compute the path once, during the \ninitial submodule clone, then reuse it from the .git file inside \nthe submodule workdir in later actions, however that is not enough \nfor compatibility with other implementations or older versions.\n\nSo yes, to avoid user confusion, multi-implementation \ninter-operability problems or risk any repo inconsistency, I'll \nmake it a breaking change.\n"},{"id":"525969","messageId":"87bjnj7c3y.fsf@collabora.com","threadId":"63967","inReplyTo":"aL_Z8DusXdyIr4Ru@pks.im","subject":"Re: [PATCH v2 03/10] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-09T17:46:25Z","receivedAt":"2025-09-09T17:46:51Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 09 Sep 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Sep 08, 2025 at 05:01:10PM +0300, Adrian Ratiu wrote: \n>> This adds an ability to override gitdir paths via config files \n>> (not .gitmodules), such that any encoding scheme can be changed \n>> and JGit & co don't need to exactly match the default encoding. \n>> A new test and a helper are added. The helper will be used by \n>> further tests exercising gitdir paths & encodings. \n> \n> Aha, so you already do what I'm lamenting about in the preceding \n> commit. \n\nNot quite. I tried to explain in my previous reply that this only \nlooks similar and perhaps I should have worded the commit message \ndifferently (I promised Phillip I'll reword it), however it's \nintended just to allow simple overrides, without being a unified \nway for all implementations to work with gitdirs. :)\n\n> It still raises the question around how to handle this whole \n> migration now, as alternative implementations don't yet know \n> about this specific config key. Doing all of this in a single \n> patch series will most likely result in breakage. \n> \n> In theory, we should probably first introduce the configuration, \n> then wait a couple releases for alternative implementations to \n> catch up, and then switch over. But we still cannot be sure that \n> implementations know to handle this key alright.\n\nExactly, they won't. :)\n \n> \n> A heavy-handed solution to this would be to introduce a \n> repository extension. This would ensure that any well-behaved \n> Git client will refuse to open the repository if it doesn't know \n> about that specific extension. With the remaining ones we can be \n> sure that they know to handle the \"submodule.*.gitdir\" \n> configuration.\n> \n> As mentioned, extensions are rather heavy-handed. Furthermore, \n> this whole infra is only really needed under very specific \n> circumstances. So maybe it could be a viable approach to make \n> this extension opt-in: \n> \n>   - We provide a new configuration \n>   \"init.useSubmoduleGitdirExtension\" \n>     that tells git-init(1) and git-clone(1) to use the new \n>     extension for newly initialized repositories. \n> \n>   - We detect the situation where a submodule cannot be cloned \n>   due to a \n>     path conflict. If detected, we print a user-facing hint that \n>     tells them to enable the extension. \n\nThis is actually a very good idea, to detect path conflicts and \nprint a nice message to instruct the user to enable the extension, \ninstead of the default \"error: git dir is inside git dir\" or \nsomesuch confusing msg.\n\nI'll work on it for patch v3 and add some tests.\n\n> \n> Once the user enabled the extension we'll know to use new, \n> encoded submodule paths from thereon and thus re-initializing \n> the conflicting submodule should work now. \n> \n> With all of this I wonder whether we even need a new \n> \".git/submodules\" directory. Couldn't we just continue to create \n> submodules in the old path and for example create a random \n> suffix as required? \n\nNow that we decided to put this logic behind an extension, we \nmight get away with having a unified directory, yes. It's \nsomething I'll explore for v3. I would really like to avoid having \nboth \"modules\" and \"submodules\" alongside eachother.\n\n> \n> In any case, please stop me if I'm going overboard with my \n> backwards compatibility concerns :) \n\nYou have good ideas and your feedback is much appreciated. Kudos and\nthanks!\n"},{"id":"526065","messageId":"aMHATMvn4Sdcz7mJ@szeder.dev","threadId":"63967","inReplyTo":"20250908140117.262205-7-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 06/10] submodule: encode gitdir paths to avoid conflicts","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2025-09-10T18:15:40Z","receivedAt":"2025-09-10T18:15:58Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Sep 08, 2025 at 05:01:13PM +0300, Adrian Ratiu wrote:\n> diff --git a/submodule.c b/submodule.c\n> index bf78636195..8e0fd077db 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n\n> @@ -2588,30 +2593,26 @@ int submodule_to_gitdir(struct repository *repo,\n>  \treturn ret;\n>  }\n>  \n> +static void strbuf_addstr_case_encode(struct strbuf *dst, const char *src)\n> +{\n> +\tfor (; *src; src++) {\n> +\t\tunsigned char c = *src;\n> +\t\tif (c >= 'A' && c <= 'Z') {\n> +\t\t\tstrbuf_addch(dst, '_');\n> +\t\t\tstrbuf_addch(dst, c - 'A' + 'a');\n> +\t\t} else {\n> +\t\t\tstrbuf_addch(dst, c);\n> +\t\t}\n> +\t}\n> +}\n> +\n>  void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \t\t\t      const char *submodule_name)\n>  {\n> -\t/*\n> -\t * NEEDSWORK: The current way of mapping a submodule's name to\n> -\t * its location in .git/modules/ has problems with some naming\n> -\t * schemes. For example, if a submodule is named \"foo\" and\n> -\t * another is named \"foo/bar\" (whether present in the same\n> -\t * superproject commit or not - the problem will arise if both\n> -\t * superproject commits have been checked out at any point in\n> -\t * time), or if two submodule names only have different cases in\n> -\t * a case-insensitive filesystem.\n> -\t *\n> -\t * There are several solutions, including encoding the path in\n> -\t * some way, introducing a submodule.<name>.gitdir config in\n> -\t * .git/config (not .gitmodules) that allows overriding what the\n> -\t * gitdir of a submodule would be (and teach Git, upon noticing\n> -\t * a clash, to automatically determine a non-clashing name and\n> -\t * to write such a config), or introducing a\n> -\t * submodule.<name>.gitdir config in .gitmodules that repo\n> -\t * administrators can explicitly set. Nothing has been decided,\n> -\t * so for now, just append the name at the end of the path.\n> -\t */\n> +\tstruct strbuf encoded_sub_name = STRBUF_INIT, tmp = STRBUF_INIT;\n> +\tsize_t base_len, encoded_len;\n>  \tchar *gitdir_path, *key;\n> +\tlong name_max;\n\nSome of these new variables are not used or are only written:\n\n  submodule.c: In function ‘submodule_name_to_gitdir’:\n  submodule.c:2615:14: error: unused variable ‘name_max’ [-Werror=unused-variable]\n   2615 |         long name_max;\n        |              ^~~~~~~~\n  submodule.c:2613:26: error: unused variable ‘encoded_len’ [-Werror=unused-variable]\n   2613 |         size_t base_len, encoded_len;\n        |                          ^~~~~~~~~~~\n  submodule.c:2613:16: error: variable ‘base_len’ set but not used [-Werror=unused-but-set-variable]\n   2613 |         size_t base_len, encoded_len;\n        |                ^~~~~~~~\n  cc1: all warnings being treated as errors\n  make: *** [Makefile:2815: submodule.o] Error 1\n\n"},{"id":"526076","messageId":"878qim6r6h.fsf@collabora.com","threadId":"63967","inReplyTo":"aMHATMvn4Sdcz7mJ@szeder.dev","subject":"Re: [PATCH v2 06/10] submodule: encode gitdir paths to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-09-10T19:30:46Z","receivedAt":"2025-09-10T19:31:19Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Wed, 10 Sep 2025, SZEDER Gábor <szeder.dev@gmail.com> wrote:\n> On Mon, Sep 08, 2025 at 05:01:13PM +0300, Adrian Ratiu wrote: \n>> +\tstruct strbuf encoded_sub_name = STRBUF_INIT, tmp = \n>> STRBUF_INIT; +\tsize_t base_len, encoded_len; \n>>  \tchar *gitdir_path, *key; \n>> +\tlong name_max; \n> \n> Some of these new variables are not used or are only written: \n \nI moved the logic to a new commit but forgot to also move the \nvariable definitions to the new commit. :)\n\nWill fix this in v3, alongside running more tests & diagnostics on \neach commit to ensure they are also bisectable and so on.\n\nThanks for spotting this!\n"},{"id":"526079","messageId":"867e9a54-9b49-41c5-bc65-7e50c21a939a@app.fastmail.com","threadId":"63967","inReplyTo":"20250908140117.262205-7-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 06/10] submodule: encode gitdir paths to avoid conflicts","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-09-10T20:18:11Z","receivedAt":"2025-09-10T20:21:11Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Sep 8, 2025, at 16:01, Adrian Ratiu wrote:\n> Based on previous work by Brandon & all [1].\n\nnit: “& al”/“et al”?\n\n> Based on previous work by Brandon & all [1].\n> ...\n> Link: https://lore.kernel.org/git/20180807230637.247200-1-bmwill@google.com/ [1]\n\nnit: This practice of appending or I guess prepending[1] the footnote\nreference to a Link trailer isn’t commonly done.  It’s usually\nthat regular\n\n    [1]: <link>\n\nThing.\n\nTrailers are almost always pointing to people identities.  Not links or\ncommits or other inanimate things.\n\n† 1: f62dcc7f30d (remote: remove branch->merge_name and fix\n    branch_release(), 2025-06-23)\n\n>[snip]\n\n-- \nKristoffer Haugsbakk\n"},{"id":"527631","messageId":"9c313e46-805e-4b10-b8f9-6e2d5bf0ccf1@app.fastmail.com","threadId":"63967","inReplyTo":"20250908140117.262205-9-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 08/10] submodule: remove validate_submodule_git_dir()","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-09-30T13:35:06Z","receivedAt":"2025-09-30T13:35:29Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Sep 8, 2025, at 16:01, Adrian Ratiu wrote:\n> The validate_submodule_git_dir test is not very useful anymore, after\n> submodule names are encoded to resolve gitdir path conflicts.\n>\n> In other words, the purpouse of gitdir path encoding is precisely to\n\ns/purpouse/purpose/\n\n> avoid such conflicts as this function tries to also prevent.\n>\n>[snip]\n"},{"id":"527632","messageId":"b723d8b7-5c17-48fc-90da-ad2173d9ce79@app.fastmail.com","threadId":"63967","inReplyTo":"20250908140117.262205-8-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 07/10] submodule: error out if gitdir name is too long","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-09-30T13:35:31Z","receivedAt":"2025-09-30T13:35:52Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Sep 8, 2025, at 16:01, Adrian Ratiu wrote:\n> Encoding submodule names increases their name size, so there is an\n> increased risk to hit the max filename length in the gitdir path.\n> (the likelihood is still rather small, so it's an acceptable risk)\n>\n> This gitdir file-name-too-long corner case can be be addressed in\n> multiple ways, including sharding or trimming, however for now, just\n> add the portable logic (suggested by Peff) to detect the corner case\n> then error out to avoid comitting to a specific policy (or policies).\n>\n> In the future, instead of throwing an error (which we do now anyway\n> without submodule encoding), we could maybe let the user specify via\n> configs how to address this case, eg pick trimming or sharding.\n\ns/eg/e.g./\n\n>\n> Suggested-by: Jeff King <peff@peff.net>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n"},{"id":"527633","messageId":"03d3cd99-b319-4e9f-8ad8-2c1174efea28@app.fastmail.com","threadId":"63967","inReplyTo":"20250908140117.262205-7-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 06/10] submodule: encode gitdir paths to avoid conflicts","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-09-30T13:36:21Z","receivedAt":"2025-09-30T13:36:43Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Sep 8, 2025, at 16:01, Adrian Ratiu wrote:\n> Based on previous work by Brandon & all [1].\n>\n> This encodes submodule gitdir names to avoid colisions like nested gitdirs\n\ns/colisions/collisions/\n\n> due to names like \"foo\" and \"foo/bar\".\n>\n> A custom encoding can become unnecesarily complex, while url-encoding is\n\ns/unnecesarily/unnecessarily/\n\n> relatively well-known, however it needs some extending to support case\n> insensitive filesystems and quirks like Windows reserving \"COM1\" names.\n> Hence why I opted to encode A as _a, B as _b and so on.\n>\n>[snip]\n"},{"id":"527634","messageId":"6a7bb99b-68f7-4120-9ee7-d4efbea99243@app.fastmail.com","threadId":"63967","inReplyTo":"20250908140117.262205-2-adrian.ratiu@collabora.com","subject":"Re: [PATCH v2 01/10] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-09-30T13:37:42Z","receivedAt":"2025-09-30T13:38:04Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Mon, Sep 8, 2025, at 16:01, Adrian Ratiu wrote:\n> While testing submodule gitdir path encoding, I noticed submodule--helper\n> is still using a hardcoded name-based path leading to test failures, so\n> convert it to the common helper function introduced by commit ce125d431a\n> (submodule: extract path to submodule gitdir func, 2021-09-15)  and used\n> in other locations accross the source tree.\n\ns/accross/across/\n\n>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n"},{"id":"527868","messageId":"87wm5cbeqj.fsf@collabora.com","threadId":"63967","inReplyTo":"9c313e46-805e-4b10-b8f9-6e2d5bf0ccf1@app.fastmail.com","subject":"Re: [PATCH v2 08/10] submodule: remove validate_submodule_git_dir()","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-03T07:56:20Z","receivedAt":"2025-10-03T07:56:47Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 30 Sep 2025, \"Kristoffer Haugsbakk\" \n<kristofferhaugsbakk@fastmail.com> wrote:\n> On Mon, Sep 8, 2025, at 16:01, Adrian Ratiu wrote: \n>> The validate_submodule_git_dir test is not very useful anymore, \n>> after submodule names are encoded to resolve gitdir path \n>> conflicts. \n>> \n>> In other words, the purpouse of gitdir path encoding is \n>> precisely to \n> \n> s/purpouse/purpose/ \n\nThanks, I'll send v3 with these typos fixed very soon.\n"},{"id":"527968","messageId":"20251006112518.3764240-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 0/5] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-06T11:25:13Z","receivedAt":"2025-10-06T11:26:01Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello everyone,\n\nv3 is much simplified from v2, starting from the design idea that submodule gitdir\nname encoding is to be put behind an extensions.submoduleEncoding.\n\nThis allowed removal of the modules vs submodules directories split and simplified\nour logic quite a lot. Tests have been been squashed in the smaller commits as well.\n\nMany thanks to all who provided feedback, especially Patrick and Phillip who\nsuggested the extension idea.\n\nThis is based on the latest master branch and I've also merged and tested against next.\n\nI pushed the patches to github [1] and also did a CI run [2] which passed (the lone\nWin+Meson CI failure seems to be unrelated because it reproduces without the patches).\n\n[1] https://github.com/10ne1/git/tree/dev/aratiu/encoding-v3\n[2] https://github.com/10ne1/git/actions/runs/18276914867\n\nChanges between v2 -> v3:\n* Put submodule encoding behind an extension (Phillip & Patrick).\n* Removed the submodules vs modules directory split (Patrick).\n* Undeleted validate_submodule_git_dir() because it still needs to check the default.\n* Undeleted tests from t7450-bad-git-dotfiles.sh because they are still required.\n* Moved new tests to a new file which enables the extension.\n* Moved unused variables to commit which uses them (Szeder Gabor).\n* Squashed commits to reduce their number (e.g. tests are together with new logic).\n* Fixed a small bug passing the module repo instead of the_repo to the gitdir helper.\n* Small commit msg rewording improvements, typos (Kristoffer & Phillip).\n\nAdrian Ratiu (5):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  submodule: add gitdir path config override\n  strbuf: bring back is_rfc3986_unreserved\n  submodule: encode gitdir paths to avoid conflicts\n  submodule: error out if gitdir name is too long\n\n Documentation/config/extensions.adoc  |   9 ++\n Documentation/config/submodule.adoc   |   7 ++\n Makefile                              |   5 +\n builtin/credential-store.c            |   6 -\n builtin/submodule--helper.c           |  30 ++++-\n compat/pathconf.c                     |  10 ++\n compat/posix.h                        |   8 ++\n config.mak.uname                      |   2 +\n meson.build                           |   1 +\n repository.h                          |   1 +\n setup.c                               |   7 ++\n setup.h                               |   1 +\n strbuf.c                              |   6 +\n strbuf.h                              |   2 +\n submodule.c                           |  84 +++++++++----\n t/lib-verify-submodule-gitdir-path.sh |  20 ++++\n t/meson.build                         |   1 +\n t/t7400-submodule-basic.sh            |   9 ++\n t/t7425-submodule-encoding.sh         | 162 ++++++++++++++++++++++++++\n t/t9902-completion.sh                 |   1 +\n 20 files changed, 340 insertions(+), 32 deletions(-)\n create mode 100644 compat/pathconf.c\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-encoding.sh\n\n-- \n2.49.1\n\n"},{"id":"527969","messageId":"20251006112518.3764240-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251006112518.3764240-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 2/5] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-06T11:25:15Z","receivedAt":"2025-10-06T11:26:07Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This adds the ability to override gitdir paths via config files\n(not .gitmodules) such that the encoding scheme (or plain text\nname if the encoding extension is disabled) can be changed via\nconfig entries.\n\nThese entries are not added by default for all submodules: they\nshould be used on an as-needed basis.\n\nA new test and a helper are added. The helper will also be used\nin further tests exercising gitdir encoding functionality.\n\nBased-on-patch-by: Brandon Williams <bmwill@google.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/submodule.adoc   |  4 ++++\n builtin/submodule--helper.c           | 17 +++++++++++++++++\n submodule.c                           | 12 ++++++++++++\n t/lib-verify-submodule-gitdir-path.sh | 20 ++++++++++++++++++++\n t/t7400-submodule-basic.sh            |  9 +++++++++\n t/t9902-completion.sh                 |  1 +\n 6 files changed, 63 insertions(+)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n\ndiff --git a/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 0672d99117..8f64adfbe3 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -52,6 +52,10 @@ submodule.<name>.active::\n \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n \tdetails.\n \n+submodule.<name>.gitdir::\n+\tThis option sets the gitdir path for submodule <name>, allowing users\n+\tto override the default path or change the default path name encoding.\n+\n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\n \tsubmodule's path to determine if the submodule is of interest to git\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 2873b2780e..abd20eee53 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1208,6 +1208,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3591,6 +3607,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\ndiff --git a/submodule.c b/submodule.c\nindex 35c55155f7..7a2d7cd592 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2604,6 +2604,18 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t * administrators can explicitly set. Nothing has been decided,\n \t * so for now, just append the name at the end of the path.\n \t */\n+\tchar *gitdir_path, *key;\n+\n+\t/* Allow config override. */\n+\tkey = xstrfmt(\"submodule.%s.gitdirpath\", submodule_name);\n+\tif (!repo_config_get_string(r, key, &gitdir_path)) {\n+\t\tstrbuf_addstr(buf, gitdir_path);\n+\t\tfree(key);\n+\t\tfree(gitdir_path);\n+\t\treturn;\n+\t}\n+\tfree(key);\n+\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n }\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..3a83f2d975\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,20 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n+\n+# This does not check filesystem existence. That is done in submodule.c via the\n+# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n+# might or might not exist (e.g. when adding a new submodule), so this only\n+# checks the expected configuration path, which might be overridden by the user.\n+\n+verify_submodule_gitdir_path() {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\tcat >expect <<-EOF &&\n+\t\t\t$(git rev-parse --git-common-dir)/$path\n+\t\tEOF\n+\t\tgit submodule--helper gitdir \"$name\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\nindex fd3e7e355e..11c84a7bdf 100755\n--- a/t/t7400-submodule-basic.sh\n+++ b/t/t7400-submodule-basic.sh\n@@ -13,6 +13,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n \n test_expect_success 'setup - enable local submodules' '\n \tgit config --global protocol.file.allow always\n@@ -1505,4 +1506,12 @@ test_expect_success 'submodule add fails when name is reused' '\n \t)\n '\n \n+test_expect_success 'submodule helper gitdir config overrides' '\n+\tverify_submodule_gitdir_path test-submodule child modules/child &&\n+\ttest_config -C test-submodule submodule.child.gitdirpath \".git/modules/custom-child\" &&\n+\tverify_submodule_gitdir_path test-submodule child modules/custom-child &&\n+\ttest_unconfig -C test-submodule submodule.child.gitdirpath &&\n+\tverify_submodule_gitdir_path test-submodule child modules/child\n+'\n+\n test_done\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 964e1f1569..ffb9c8b522 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n \tsubmodule.sub.fetchRecurseSubmodules Z\n \tsubmodule.sub.ignore Z\n \tsubmodule.sub.active Z\n+\tsubmodule.sub.gitdir Z\n \tEOF\n '\n \n-- \n2.49.1\n\n"},{"id":"527970","messageId":"20251006112518.3764240-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251006112518.3764240-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 1/5] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-06T11:25:14Z","receivedAt":"2025-10-06T11:26:08Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded name-based path leading to test failures, so\nconvert it to the common helper function introduced by commit ce125d431a\n(submodule: extract path to submodule gitdir func, 2021-09-15)  and used\nin other locations across the source tree.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 13 +++++++------\n 1 file changed, 7 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex fcd73abe53..2873b2780e 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3187,13 +3187,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n \n static int add_submodule(const struct add_data *add_data)\n {\n-\tchar *submod_gitdir_path;\n \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n \tint ret = -1;\n \n \t/* perhaps the path already exists and is already a git repo, else clone it */\n \tif (is_directory(add_data->sm_path)) {\n+\t\tchar *submod_gitdir_path;\n \t\tstruct strbuf sm_path = STRBUF_INIT;\n \t\tstrbuf_addstr(&sm_path, add_data->sm_path);\n \t\tsubmod_gitdir_path = xstrfmt(\"%s/.git\", add_data->sm_path);\n@@ -3207,10 +3207,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3219,8 +3220,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3238,7 +3239,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.49.1\n\n"},{"id":"527971","messageId":"20251006112518.3764240-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251006112518.3764240-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 4/5] submodule: encode gitdir paths to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-06T11:25:17Z","receivedAt":"2025-10-06T11:26:14Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This adds a new submoduleEncoding extension which encodes gitdir names\nto avoid collisions due to nested gitdirs or case insensitive filesystems.\n\nA custom encoding can become unnecessarily complex, while url-encoding is\nrelatively well-known, however it needs some extending to support case\ninsensitive filesystems, hence why A is encoded as _a, B as _b and so on.\n\nUnfortunately encoding A -> _a (...) is not enough to fix the reserved\nWindows file names (e.g. COM1) because worktrees still use names like COM1\neven if the gitdirs paths are encoded, so future work is needed to fully\naddress Windows reserved names.\n\nFor now url-encoding is the only option, however in the future we may\nadd alternatives (other encodings, hashes or even hash_name).\n\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc |   9 ++\n Documentation/config/submodule.adoc  |   3 +\n repository.h                         |   1 +\n setup.c                              |   7 ++\n setup.h                              |   1 +\n submodule.c                          |  56 ++++++----\n t/meson.build                        |   1 +\n t/t7425-submodule-encoding.sh        | 146 +++++++++++++++++++++++++++\n 8 files changed, 204 insertions(+), 20 deletions(-)\n create mode 100755 t/t7425-submodule-encoding.sh\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 829f2523fc..c6ab268328 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -73,6 +73,15 @@ relativeWorktrees::\n \trepaired with either the `--relative-paths` option or with the\n \t`worktree.useRelativePaths` config set to `true`.\n \n+submoduleEncoding::\n+\tIf enabled, submodule gitdir paths are encoded to avoid filesystem\n+\tconflicts due to nested gitdirs or case insensitivity. For now, only\n+\turl-encoding (rfc3986) is available, with a small addition to encode\n+\tuppercase to lowercase letters (`A  -> _a`, `B -> _b` and so on).\n+\tOther encoding or hashing methods may be added in the future.\n+\tAny preexisting non-encoded submodule gitdirs are used as-is, to\n+\tease migration and reduce risk of gitdirs not being recognized.\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/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 8f64adfbe3..cd7fd5da5b 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -55,6 +55,9 @@ submodule.<name>.active::\n submodule.<name>.gitdir::\n \tThis option sets the gitdir path for submodule <name>, allowing users\n \tto override the default path or change the default path name encoding.\n+\tSubmodule gitdir encoding is enabled via `extensions.submoduleEncoding`\n+\t(see linkgit:git-config[1]). This config works both with the extension\n+\tenabled or disabled.\n \n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\ndiff --git a/repository.h b/repository.h\nindex 5808a5d610..7e39b2acf7 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -158,6 +158,7 @@ struct repository {\n \tint repository_format_worktree_config;\n \tint repository_format_relative_worktrees;\n \tint repository_format_precious_objects;\n+\tint repository_format_submodule_encoding;\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 7086741e6c..bf6e815105 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -687,6 +687,9 @@ static enum extension_result handle_extension(const char *var,\n \t} else if (!strcmp(ext, \"relativeworktrees\")) {\n \t\tdata->relative_worktrees = git_config_bool(var, value);\n \t\treturn EXTENSION_OK;\n+\t} else if (!strcmp(ext, \"submoduleencoding\")) {\n+\t\tdata->submodule_encoding = git_config_bool(var, value);\n+\t\treturn EXTENSION_OK;\n \t}\n \treturn EXTENSION_UNKNOWN;\n }\n@@ -1865,6 +1868,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\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\tthe_repository->repository_format_submodule_encoding =\n+\t\t\t\trepo_fmt.submodule_encoding;\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@@ -1963,6 +1968,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_submodule_encoding =\n+\t\tfmt->submodule_encoding;\n \tthe_repository->repository_format_relative_worktrees =\n \t\tfmt->relative_worktrees;\n \tthe_repository->repository_format_partial_clone =\ndiff --git a/setup.h b/setup.h\nindex 8522fa8575..66ec1ceba5 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -130,6 +130,7 @@ struct repository_format {\n \tchar *partial_clone; /* value of extensions.partialclone */\n \tint worktree_config;\n \tint relative_worktrees;\n+\tint submodule_encoding;\n \tint is_bare;\n \tint hash_algo;\n \tint compat_hash_algo;\ndiff --git a/submodule.c b/submodule.c\nindex 7a2d7cd592..23b79c9192 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2262,6 +2262,13 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \tchar *p;\n \tint ret = 0;\n \n+\t/*\n+\t * Skip these checks when extensions.submoduleEncoding is enabled because\n+\t * it fixes the nesting issues and the suffixes will not match by design.\n+\t */\n+\tif (the_repository->repository_format_submodule_encoding)\n+\t\treturn 0;\n+\n \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n \t    strcmp(p, submodule_name))\n \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n@@ -2581,29 +2588,22 @@ int submodule_to_gitdir(struct repository *repo,\n \treturn ret;\n }\n \n+static void strbuf_addstr_case_encode(struct strbuf *dst, const char *src)\n+{\n+\tfor (; *src; src++) {\n+\t\tunsigned char c = *src;\n+\t\tif (c >= 'A' && c <= 'Z') {\n+\t\t\tstrbuf_addch(dst, '_');\n+\t\t\tstrbuf_addch(dst, c - 'A' + 'a');\n+\t\t} else {\n+\t\t\tstrbuf_addch(dst, c);\n+\t\t}\n+\t}\n+}\n+\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n-\t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n-\t */\n \tchar *gitdir_path, *key;\n \n \t/* Allow config override. */\n@@ -2618,4 +2618,20 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n+\n+\t/* Existing legacy non-encoded names are used as-is */\n+\tif (is_git_directory(buf->buf))\n+\t\treturn;\n+\n+\tif (the_repository->repository_format_submodule_encoding) {\n+\t\tstruct strbuf tmp = STRBUF_INIT;\n+\n+\t\tstrbuf_reset(buf);\n+\t\trepo_git_path_append(r, buf, \"modules/\");\n+\n+\t\tstrbuf_addstr_urlencode(&tmp, submodule_name, is_rfc3986_unreserved);\n+\t\tstrbuf_addstr_case_encode(buf, tmp.buf);\n+\n+\t\tstrbuf_release(&tmp);\n+\t}\n }\ndiff --git a/t/meson.build b/t/meson.build\nindex 11376b9e25..de277227a2 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -882,6 +882,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-encoding.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-encoding.sh b/t/t7425-submodule-encoding.sh\nnew file mode 100755\nindex 0000000000..4ea385d882\n--- /dev/null\n+++ b/t/t7425-submodule-encoding.sh\n@@ -0,0 +1,146 @@\n+#!/bin/sh\n+\n+test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+\tgit config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed encoded and non-encoded submodules' '\n+\tgit init -b main legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init -b main new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init -b main main &&\n+\t(\n+\t\tcd main &&\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submoduleEncoding true &&\n+\n+\t\tgit submodule add ../new-sub \"New Sub\" &&\n+\t\ttest_commit new\n+\t)\n+'\n+\n+test_expect_success 'verify submodule name is properly encoded' '\n+\tverify_submodule_gitdir_path main legacy modules/legacy &&\n+\tverify_submodule_gitdir_path main \"New Sub\" modules/_new%20_sub\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned-non-encoding &&\n+\t(\n+\t\tcd cloned-non-encoding &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/\"New Sub\" &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t) &&\n+\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules main cloned-encoding &&\n+\t(\n+\t\tcd cloned-encoding &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/_new%20_sub &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to encoded submodules' '\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C main config receive.denyCurrentBranch updateInstead &&\n+\t(\n+\t\tcd cloned-encoding &&\n+\n+\t\tgit -C legacy switch --track -C main origin/main  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C \"New Sub\" switch --track -C main origin/main &&\n+\t\ttest_commit -C \"New Sub\" second-commit &&\n+\t\tgit -C \"New Sub\" push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C main origin/main  &&\n+\t\tgit add legacy \"New Sub\" &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/_new%20_sub &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of encoded sibling submodules must not be nested' '\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules nested clone_nested &&\n+\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules --jobs=2 nested clone_parallel &&\n+\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n+\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks\n+'\n+\n+test_done\n-- \n2.49.1\n\n"},{"id":"527972","messageId":"20251006112518.3764240-6-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251006112518.3764240-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 5/5] submodule: error out if gitdir name is too long","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-06T11:25:18Z","receivedAt":"2025-10-06T11:26:17Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Encoding submodule names increases their name size, so there is an\nincreased risk to hit the max filename length in the gitdir path.\n(the likelihood is still rather small, so it's an acceptable risk)\n\nThis gitdir file-name-too-long corner case can be be addressed in\nmultiple ways, including sharding or trimming, however for now, just\nadd the portable logic (suggested by Peff) to detect the corner case\nthen error out to avoid committing to a specific policy (or policies).\n\nIn the future, instead of throwing an error (which we do now anyway\nwithout submodule encoding), we could maybe let the user specify via\nconfigs how to address this case, e.g. pick trimming or sharding.\n\nAt least now we print a nice error instead of the OS defaults which\ncan be rather cryptic for users.\n\nSuggested-by: Jeff King <peff@peff.net>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Makefile                      |  5 +++++\n compat/pathconf.c             | 10 ++++++++++\n compat/posix.h                |  8 ++++++++\n config.mak.uname              |  2 ++\n meson.build                   |  1 +\n submodule.c                   | 16 ++++++++++++++++\n t/t7425-submodule-encoding.sh | 16 ++++++++++++++++\n 7 files changed, 58 insertions(+)\n create mode 100644 compat/pathconf.c\n\ndiff --git a/Makefile b/Makefile\nindex 92fd8d86d8..9f76a67d4b 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2215,6 +2215,11 @@ ifndef HAVE_PLATFORM_PROCINFO\n \tCOMPAT_OBJS += compat/stub/procinfo.o\n endif\n \n+ifdef NO_PATHCONF\n+\tCOMPAT_CFLAGS += -DNO_PATHCONF\n+\tCOMPAT_OBJS += compat/pathconf.o\n+endif\n+\n ifdef RUNTIME_PREFIX\n \n         ifdef HAVE_BSD_KERN_PROC_SYSCTL\ndiff --git a/compat/pathconf.c b/compat/pathconf.c\nnew file mode 100644\nindex 0000000000..37500cfa0d\n--- /dev/null\n+++ b/compat/pathconf.c\n@@ -0,0 +1,10 @@\n+#include \"git-compat-util.h\"\n+\n+/*\n+ * Minimal stub for platforms without pathconf() (e.g. Windows),\n+ * to fall back to NAME_MAX from limits.h or compat/posix.h.\n+ */\n+long git_pathconf(const char *path UNUSED, int name UNUSED)\n+{\n+\treturn -1;\n+}\ndiff --git a/compat/posix.h b/compat/posix.h\nindex 067a00f33b..aa050fd58c 100644\n--- a/compat/posix.h\n+++ b/compat/posix.h\n@@ -250,6 +250,14 @@ char *gitdirname(char *);\n #define NAME_MAX 255\n #endif\n \n+#ifdef NO_PATHCONF\n+#ifndef _PC_NAME_MAX\n+#define _PC_NAME_MAX 1 /* dummy value, only used for git_pathconf */\n+#endif\n+#define pathconf(a,b) git_pathconf(a,b)\n+long git_pathconf(const char *path, int name);\n+#endif\n+\n typedef uintmax_t timestamp_t;\n #define PRItime PRIuMAX\n #define parse_timestamp strtoumax\ndiff --git a/config.mak.uname b/config.mak.uname\nindex 1691c6ae6e..49ba3de39d 100644\n--- a/config.mak.uname\n+++ b/config.mak.uname\n@@ -473,6 +473,7 @@ ifeq ($(uname_S),Windows)\n \tNEEDS_CRYPTO_WITH_SSL = YesPlease\n \tNO_LIBGEN_H = YesPlease\n \tNO_POLL = YesPlease\n+\tNO_PATHCONF = YesPlease\n \tNO_SYMLINK_HEAD = YesPlease\n \tNO_IPV6 = YesPlease\n \tNO_SETENV = YesPlease\n@@ -688,6 +689,7 @@ ifeq ($(uname_S),MINGW)\n \tNEEDS_CRYPTO_WITH_SSL = YesPlease\n \tNO_LIBGEN_H = YesPlease\n \tNO_POLL = YesPlease\n+\tNO_PATHCONF = YesPlease\n \tNO_SYMLINK_HEAD = YesPlease\n \tNO_SETENV = YesPlease\n \tNO_STRCASESTR = YesPlease\ndiff --git a/meson.build b/meson.build\nindex db1710e229..b65b5e0911 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -1394,6 +1394,7 @@ checkfuncs = {\n   'initgroups' : [],\n   'strtoumax' : ['strtoumax.c', 'strtoimax.c'],\n   'pread' : ['pread.c'],\n+  'pathconf' : ['pathconf.c'],\n }\n \n if host_machine.system() == 'windows'\ndiff --git a/submodule.c b/submodule.c\nindex 23b79c9192..201e8a8fe5 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2625,13 +2625,29 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \n \tif (the_repository->repository_format_submodule_encoding) {\n \t\tstruct strbuf tmp = STRBUF_INIT;\n+\t\tsize_t base_len;\n+\t\tlong name_max;\n \n \t\tstrbuf_reset(buf);\n \t\trepo_git_path_append(r, buf, \"modules/\");\n+\t\tbase_len = buf->len;\n \n \t\tstrbuf_addstr_urlencode(&tmp, submodule_name, is_rfc3986_unreserved);\n \t\tstrbuf_addstr_case_encode(buf, tmp.buf);\n \n+\t\t/* Ensure final path length is below NAME_MAX after encoding */\n+\t\tname_max = pathconf(buf->buf, _PC_NAME_MAX);\n+\t\tif (name_max == -1)\n+\t\t\tname_max = NAME_MAX;\n+\n+\t\tif (buf->len - base_len > name_max)\n+\t\t\t/*\n+\t\t\t * TODO: make this smarter; instead of erroring out, maybe we could trim or\n+\t\t\t * shard the gitdir names to make them fit under NAME_MAX.\n+\t\t\t */\n+\t\t\tdie(_(\"submodule name %s is too long (%\"PRIuMAX\" bytes, limit %\"PRIuMAX\")\"),\n+\t\t\t    buf->buf, (uintmax_t)buf->len - base_len, (uintmax_t)name_max);\n+\n \t\tstrbuf_release(&tmp);\n \t}\n }\ndiff --git a/t/t7425-submodule-encoding.sh b/t/t7425-submodule-encoding.sh\nindex 4ea385d882..8041781491 100755\n--- a/t/t7425-submodule-encoding.sh\n+++ b/t/t7425-submodule-encoding.sh\n@@ -143,4 +143,20 @@ test_expect_success 'submodule git dir nesting detection must work with parallel\n \tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks\n '\n \n+test_expect_success 'submodule encoded name exceeds max name limit' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# find the system NAME_MAX (fall back to 255 if unknown)\n+\t\tname_max=$(getconf NAME_MAX . 2>/dev/null || echo 255) &&\n+\n+\t\t# each \"%\" char encodes to \"%25\" (3 chars), ensure we exceed NAME_MAX\n+\t\tcount=$((name_max + 10)) &&\n+\t\tlongname=$(test_seq -f \"%%%0.s\" 1 $count | tr -d \"\\n\") &&\n+\n+\t\ttest_must_fail git submodule add ../new-sub \"$longname\" 2>err &&\n+\t\ttest_grep \"fatal: submodule name .* is too long\" err\n+\t)\n+'\n+\n test_done\n-- \n2.49.1\n\n"},{"id":"527973","messageId":"20251006112518.3764240-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251006112518.3764240-1-adrian.ratiu@collabora.com","subject":"[PATCH v3 3/5] strbuf: bring back is_rfc3986_unreserved","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-06T11:25:16Z","receivedAt":"2025-10-06T11:26:24Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"is_rfc3986_unreserved() was moved to credential-store.c and was made\nstatic by f89854362c (credential-store: move related functions to\ncredential-store file, 2023-06-06) under a correct assumption, at the\ntime, that it was the only place using it.\n\nHowever now we need it to apply URL-encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c | 6 ------\n strbuf.c                   | 6 ++++++\n strbuf.h                   | 2 ++\n 3 files changed, 8 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..0acaf1cc82 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -76,12 +76,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/strbuf.c b/strbuf.c\nindex 6c3851a7f8..e8d84cbb6d 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -817,6 +817,12 @@ void strbuf_addstr_xml_quoted(struct strbuf *buf, const char *s)\n \t}\n }\n \n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n static void strbuf_add_urlencode(struct strbuf *sb, const char *s, size_t len,\n \t\t\t\t char_predicate allow_unencoded_fn)\n {\ndiff --git a/strbuf.h b/strbuf.h\nindex a580ac6084..5139269039 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -640,6 +640,8 @@ static inline void strbuf_complete_line(struct strbuf *sb)\n \n typedef int (*char_predicate)(char ch);\n \n+int is_rfc3986_unreserved(char ch);\n+\n void strbuf_addstr_urlencode(struct strbuf *sb, const char *name,\n \t\t\t     char_predicate allow_unencoded_fn);\n \n-- \n2.49.1\n\n"},{"id":"527991","messageId":"xmqqo6qkq9vm.fsf@gitster.g","threadId":"63967","inReplyTo":"20251006112518.3764240-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 0/5] Encode submodule gitdir names to avoid conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-06T16:21:17Z","receivedAt":"2025-10-06T16:21:20Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Hello everyone,\n>\n> v3 is much simplified from v2, starting from the design idea that submodule gitdir\n> name encoding is to be put behind an extensions.submoduleEncoding.\n\nThis design decision to make it an extension makes a repository with\na new-style submodule incompatible with older Git, which may not matter\nall that much unless you use third-party tools that come with their own\nversion of Git embedded (which by definition can become stale).\n\nIf you already have submodules creted under the original scheme,\nthen add a new submodule that needs this extension, do you enable\nthis new extension and write the new submodule under encoded name,\nand move the existing submodules under their encoded names?\n\n> This allowed removal of the modules vs submodules directories split and simplified\n> our logic quite a lot. Tests have been been squashed in the smaller commits as well.\n\nBy this statement, I am guessing that the answer is yes?  That\nwould make it consistent.  The last thing we want here is the code\nthat needs to guess which ones are encoded and which ones are not.\n\nThanks.\n"},{"id":"527992","messageId":"xmqqh5wcq94v.fsf@gitster.g","threadId":"63967","inReplyTo":"20251006112518.3764240-2-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 1/5] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-06T16:37:20Z","receivedAt":"2025-10-06T16:37:23Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> While testing submodule gitdir path encoding, I noticed submodule--helper\n> is still using a hardcoded name-based path leading to test failures, so\n> convert it to the common helper function introduced by commit ce125d431a\n> (submodule: extract path to submodule gitdir func, 2021-09-15)  and used\n> in other locations across the source tree.\n\nOK.  To me during my first reading, the above read as if you found\nan open coded logic here in add_submodule(), made it into a new\ncommon helper function, and made this part as well as other locations\ncall that new common helper function.  Of course that is not the\ncase.\n\nPerhaps replacing everything after \", so convert it\" with something\nsimpler like\n\n    ... to test failures.  Call submodule_name_to_gitdir() helper\n    instead, which was invented exactly for this purpose and\n    everybody else uses.\n    \nmight have helped me to avoid such a confusion.  I dunno.\n\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index fcd73abe53..2873b2780e 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -3187,13 +3187,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n>  \n>  static int add_submodule(const struct add_data *add_data)\n>  {\n> -\tchar *submod_gitdir_path;\n>  \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n>  \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n>  \tint ret = -1;\n>  \n>  \t/* perhaps the path already exists and is already a git repo, else clone it */\n>  \tif (is_directory(add_data->sm_path)) {\n> +\t\tchar *submod_gitdir_path;\n\nThis hunk is not related to the theme of the change and not\nexplained?  I think the variable becomes used only within this block\nafter the patch that loses the use of it on the \"else\" side, so in\nthat sense it is not strictly unrelated, but is a fallout of this\nchange.  If we were to mention the change in the log message,\nsomething like \"Also narrow the scope of a variable that is no\nlonger used in the updated code\" would suffice.\n\n> @@ -3207,10 +3207,11 @@ static int add_submodule(const struct add_data *add_data)\n>  \t\tfree(submod_gitdir_path);\n>  \t} else {\n>  \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n> +\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n>  \n> -\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n> +\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n>  \n> -\t\tif (is_directory(submod_gitdir_path)) {\n> +\t\tif (is_directory(submod_gitdir.buf)) {\n>  \t\t\tif (!add_data->force) {\n>  \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n>  \t\t\t\tchar *die_msg;\n\nSo this is the crux of the change, which makes sense.  Where do we\nrelease the resource aquired here?  Let's see...\n\n> @@ -3219,8 +3220,8 @@ static int add_submodule(const struct add_data *add_data)\n>  \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n>  \t\t\t\t\t    add_data->sm_name);\n>  \n> -\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n> -\t\t\t\tfree(submod_gitdir_path);\n> +\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n> +\t\t\t\tstrbuf_release(&submod_gitdir);\n\n... OK, where the variable was freed in the original, so if this is\nnot the right place to release the resources, then the original was\nalready buggy.\n\n>  \n>  \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n>  \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n> @@ -3238,7 +3239,7 @@ static int add_submodule(const struct add_data *add_data)\n>  \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n>  \t\t\t}\n>  \t\t}\n> -\t\tfree(submod_gitdir_path);\n> +\t\tstrbuf_release(&submod_gitdir);\n\nDitto.\n\nAnd the only nonlocal exit other than die() inside this \"else\"\nblock appears _after_ this part, so we are OK.\n\nLooks good to me.  Thanks.\n"},{"id":"527993","messageId":"xmqqcy70q8n7.fsf@gitster.g","threadId":"63967","inReplyTo":"20251006112518.3764240-3-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 2/5] submodule: add gitdir path config override","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-06T16:47:56Z","receivedAt":"2025-10-06T16:47:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n[jc: brandon removed from CC list as the address would bounce]\n\n> This adds the ability to override gitdir paths via config files\n> (not .gitmodules) such that the encoding scheme (or plain text\n> name if the encoding extension is disabled) can be changed via\n> config entries.\n>\n> These entries are not added by default for all submodules: they\n> should be used on an as-needed basis.\n>\n> A new test and a helper are added. The helper will also be used\n> in further tests exercising gitdir encoding functionality.\n\nWhat is the use case of this?  The only reasonable use case I can\nsee is to set this to all the existing submodules when you are\nswitching the extension on before adding a new submodule, in which\ncase the old ones will keep using unencoded names, while the new\nones will use encoded ones.  But is that a sensible thing to do?\nHow would we guarantee that existing submodules' vanilla names would\nnot collide with encoded submodules' names?  We haven't seen the\nencoded names yet, but I think I saw some mention of URL encoding.\n\nSo if I had a submodule whose name is \"%41%42%43\", set this\nconfiguration because I do not want it treated as URL-encoded, then\nenable the extension, and then later add a separate submodule whose\nname is \"ABC\", which may be encoded (remember use of \"ABC\" here is\nonly for illustration; replace it with something that do need\nencoding if you want a more realistic example) to the same\n\"%41%42%43\".\n\nIf that kind of situation is what this new configuration allows, I\ndo not quite see why it is a good idea to have such a thing.\n\n> Based-on-patch-by: Brandon Williams <bmwill@google.com>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>  Documentation/config/submodule.adoc   |  4 ++++\n>  builtin/submodule--helper.c           | 17 +++++++++++++++++\n>  submodule.c                           | 12 ++++++++++++\n>  t/lib-verify-submodule-gitdir-path.sh | 20 ++++++++++++++++++++\n>  t/t7400-submodule-basic.sh            |  9 +++++++++\n>  t/t9902-completion.sh                 |  1 +\n>  6 files changed, 63 insertions(+)\n>  create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n>\n> diff --git a/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\n> index 0672d99117..8f64adfbe3 100644\n> --- a/Documentation/config/submodule.adoc\n> +++ b/Documentation/config/submodule.adoc\n> @@ -52,6 +52,10 @@ submodule.<name>.active::\n>  \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n>  \tdetails.\n>  \n> +submodule.<name>.gitdir::\n> +\tThis option sets the gitdir path for submodule <name>, allowing users\n> +\tto override the default path or change the default path name encoding.\n> +\n>  submodule.active::\n>  \tA repeated field which contains a pathspec used to match against a\n>  \tsubmodule's path to determine if the submodule is of interest to git\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index 2873b2780e..abd20eee53 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -1208,6 +1208,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n>  \treturn ret;\n>  }\n>  \n> +static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n> +\t\t\t struct repository *repo)\n> +{\n> +\tstruct strbuf gitdir = STRBUF_INIT;\n> +\n> +\tif (argc != 2)\n> +\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n> +\n> +\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n> +\n> +\tprintf(\"%s\\n\", gitdir.buf);\n> +\n> +\tstrbuf_release(&gitdir);\n> +\treturn 0;\n> +}\n> +\n>  struct sync_cb {\n>  \tconst char *prefix;\n>  \tconst char *super_prefix;\n> @@ -3591,6 +3607,7 @@ int cmd_submodule__helper(int argc,\n>  \t\tNULL\n>  \t};\n>  \tstruct option options[] = {\n> +\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n>  \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n>  \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n>  \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\n> diff --git a/submodule.c b/submodule.c\n> index 35c55155f7..7a2d7cd592 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2604,6 +2604,18 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \t * administrators can explicitly set. Nothing has been decided,\n>  \t * so for now, just append the name at the end of the path.\n>  \t */\n> +\tchar *gitdir_path, *key;\n> +\n> +\t/* Allow config override. */\n> +\tkey = xstrfmt(\"submodule.%s.gitdirpath\", submodule_name);\n> +\tif (!repo_config_get_string(r, key, &gitdir_path)) {\n> +\t\tstrbuf_addstr(buf, gitdir_path);\n> +\t\tfree(key);\n> +\t\tfree(gitdir_path);\n> +\t\treturn;\n> +\t}\n> +\tfree(key);\n> +\n>  \trepo_git_path_append(r, buf, \"modules/\");\n>  \tstrbuf_addstr(buf, submodule_name);\n>  }\n> diff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\n> new file mode 100644\n> index 0000000000..3a83f2d975\n> --- /dev/null\n> +++ b/t/lib-verify-submodule-gitdir-path.sh\n> @@ -0,0 +1,20 @@\n> +# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n> +\n> +# This does not check filesystem existence. That is done in submodule.c via the\n> +# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n> +# might or might not exist (e.g. when adding a new submodule), so this only\n> +# checks the expected configuration path, which might be overridden by the user.\n> +\n> +verify_submodule_gitdir_path() {\n> +\trepo=\"$1\" &&\n> +\tname=\"$2\" &&\n> +\tpath=\"$3\" &&\n> +\t(\n> +\t\tcd \"$repo\" &&\n> +\t\tcat >expect <<-EOF &&\n> +\t\t\t$(git rev-parse --git-common-dir)/$path\n> +\t\tEOF\n> +\t\tgit submodule--helper gitdir \"$name\" >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +}\n> diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\n> index fd3e7e355e..11c84a7bdf 100755\n> --- a/t/t7400-submodule-basic.sh\n> +++ b/t/t7400-submodule-basic.sh\n> @@ -13,6 +13,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n>  export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>  \n>  . ./test-lib.sh\n> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n>  \n>  test_expect_success 'setup - enable local submodules' '\n>  \tgit config --global protocol.file.allow always\n> @@ -1505,4 +1506,12 @@ test_expect_success 'submodule add fails when name is reused' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'submodule helper gitdir config overrides' '\n> +\tverify_submodule_gitdir_path test-submodule child modules/child &&\n> +\ttest_config -C test-submodule submodule.child.gitdirpath \".git/modules/custom-child\" &&\n> +\tverify_submodule_gitdir_path test-submodule child modules/custom-child &&\n> +\ttest_unconfig -C test-submodule submodule.child.gitdirpath &&\n> +\tverify_submodule_gitdir_path test-submodule child modules/child\n> +'\n> +\n>  test_done\n> diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\n> index 964e1f1569..ffb9c8b522 100755\n> --- a/t/t9902-completion.sh\n> +++ b/t/t9902-completion.sh\n> @@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n>  \tsubmodule.sub.fetchRecurseSubmodules Z\n>  \tsubmodule.sub.ignore Z\n>  \tsubmodule.sub.active Z\n> +\tsubmodule.sub.gitdir Z\n>  \tEOF\n>  '\n"},{"id":"527994","messageId":"xmqq8qhoq8hg.fsf@gitster.g","threadId":"63967","inReplyTo":"20251006112518.3764240-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 3/5] strbuf: bring back is_rfc3986_unreserved","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-06T16:51:23Z","receivedAt":"2025-10-06T16:51:26Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> is_rfc3986_unreserved() was moved to credential-store.c and was made\n> static by f89854362c (credential-store: move related functions to\n> credential-store file, 2023-06-06) under a correct assumption, at the\n> time, that it was the only place using it.\n>\n> However now we need it to apply URL-encoding to submodule names when\n> constructing gitdir paths, to avoid conflicts, so bring it back.\n\nWhy to strbuf, though?\n\nThis does not have anything to do with what strbuf does.  I could be\npossible that strbuf.c had some function that encodes/decodes 3986\nin a strbuf and this may have been a useful helper for that feature,\nbut it is apparent that this helper function is needed by\nstrbuf.[ch] in today's code, so moving it to strbuf.[ch] makes no\nsense to me.\n\n"},{"id":"527995","messageId":"xmqq347wq87s.fsf@gitster.g","threadId":"63967","inReplyTo":"20251006112518.3764240-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 4/5] submodule: encode gitdir paths to avoid conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-06T16:57:11Z","receivedAt":"2025-10-06T16:57:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> A custom encoding can become unnecessarily complex, while url-encoding is\n> relatively well-known, however it needs some extending to support case\n> insensitive filesystems, hence why A is encoded as _a, B as _b and so on.\n\nWhy 'A' cannot be encoded as %41 while encodign 'a' as %61?  Are\nthere case insensitive filesystems that cannot see the difference\nbetween %41 and %61?  And they would not collide with COM: and other\nanomalies, would they?\n\n> For now url-encoding is the only option, however in the future we may\n> add alternatives (other encodings, hashes or even hash_name).\n\nLet's not say \"For now\".\n\nChoose a single encoding that we can use forever so that we do not\nhave to upgrade extensions.encodeSubmoduleName with suffixes like\nextensions.encodeSubmoduleNamev2, extensions.encodeSubmoduleNamev3,\netc. to cover our earlier mistakes and force renaming on users.\n\n> Suggested-by: Phillip Wood <phillip.wood123@gmail.com>\n> Suggested-by: Patrick Steinhardt <ps@pks.im>\n> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n> ---\n>  Documentation/config/extensions.adoc |   9 ++\n>  Documentation/config/submodule.adoc  |   3 +\n>  repository.h                         |   1 +\n>  setup.c                              |   7 ++\n>  setup.h                              |   1 +\n>  submodule.c                          |  56 ++++++----\n>  t/meson.build                        |   1 +\n>  t/t7425-submodule-encoding.sh        | 146 +++++++++++++++++++++++++++\n>  8 files changed, 204 insertions(+), 20 deletions(-)\n>  create mode 100755 t/t7425-submodule-encoding.sh\n>\n> diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\n> index 829f2523fc..c6ab268328 100644\n> --- a/Documentation/config/extensions.adoc\n> +++ b/Documentation/config/extensions.adoc\n> @@ -73,6 +73,15 @@ relativeWorktrees::\n>  \trepaired with either the `--relative-paths` option or with the\n>  \t`worktree.useRelativePaths` config set to `true`.\n>  \n> +submoduleEncoding::\n> +\tIf enabled, submodule gitdir paths are encoded to avoid filesystem\n> +\tconflicts due to nested gitdirs or case insensitivity. For now, only\n> +\turl-encoding (rfc3986) is available, with a small addition to encode\n> +\tuppercase to lowercase letters (`A  -> _a`, `B -> _b` and so on).\n> +\tOther encoding or hashing methods may be added in the future.\n> +\tAny preexisting non-encoded submodule gitdirs are used as-is, to\n> +\tease migration and reduce risk of gitdirs not being recognized.\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\n> diff --git a/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\n> index 8f64adfbe3..cd7fd5da5b 100644\n> --- a/Documentation/config/submodule.adoc\n> +++ b/Documentation/config/submodule.adoc\n> @@ -55,6 +55,9 @@ submodule.<name>.active::\n>  submodule.<name>.gitdir::\n>  \tThis option sets the gitdir path for submodule <name>, allowing users\n>  \tto override the default path or change the default path name encoding.\n> +\tSubmodule gitdir encoding is enabled via `extensions.submoduleEncoding`\n> +\t(see linkgit:git-config[1]). This config works both with the extension\n> +\tenabled or disabled.\n>  \n>  submodule.active::\n>  \tA repeated field which contains a pathspec used to match against a\n> diff --git a/repository.h b/repository.h\n> index 5808a5d610..7e39b2acf7 100644\n> --- a/repository.h\n> +++ b/repository.h\n> @@ -158,6 +158,7 @@ struct repository {\n>  \tint repository_format_worktree_config;\n>  \tint repository_format_relative_worktrees;\n>  \tint repository_format_precious_objects;\n> +\tint repository_format_submodule_encoding;\n>  \n>  \t/* Indicate if a repository has a different 'commondir' from 'gitdir' */\n>  \tunsigned different_commondir:1;\n> diff --git a/setup.c b/setup.c\n> index 7086741e6c..bf6e815105 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -687,6 +687,9 @@ static enum extension_result handle_extension(const char *var,\n>  \t} else if (!strcmp(ext, \"relativeworktrees\")) {\n>  \t\tdata->relative_worktrees = git_config_bool(var, value);\n>  \t\treturn EXTENSION_OK;\n> +\t} else if (!strcmp(ext, \"submoduleencoding\")) {\n> +\t\tdata->submodule_encoding = git_config_bool(var, value);\n> +\t\treturn EXTENSION_OK;\n>  \t}\n>  \treturn EXTENSION_UNKNOWN;\n>  }\n> @@ -1865,6 +1868,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\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\tthe_repository->repository_format_submodule_encoding =\n> +\t\t\t\trepo_fmt.submodule_encoding;\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> @@ -1963,6 +1968,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_submodule_encoding =\n> +\t\tfmt->submodule_encoding;\n>  \tthe_repository->repository_format_relative_worktrees =\n>  \t\tfmt->relative_worktrees;\n>  \tthe_repository->repository_format_partial_clone =\n> diff --git a/setup.h b/setup.h\n> index 8522fa8575..66ec1ceba5 100644\n> --- a/setup.h\n> +++ b/setup.h\n> @@ -130,6 +130,7 @@ struct repository_format {\n>  \tchar *partial_clone; /* value of extensions.partialclone */\n>  \tint worktree_config;\n>  \tint relative_worktrees;\n> +\tint submodule_encoding;\n>  \tint is_bare;\n>  \tint hash_algo;\n>  \tint compat_hash_algo;\n> diff --git a/submodule.c b/submodule.c\n> index 7a2d7cd592..23b79c9192 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2262,6 +2262,13 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n>  \tchar *p;\n>  \tint ret = 0;\n>  \n> +\t/*\n> +\t * Skip these checks when extensions.submoduleEncoding is enabled because\n> +\t * it fixes the nesting issues and the suffixes will not match by design.\n> +\t */\n> +\tif (the_repository->repository_format_submodule_encoding)\n> +\t\treturn 0;\n> +\n>  \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n>  \t    strcmp(p, submodule_name))\n>  \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n> @@ -2581,29 +2588,22 @@ int submodule_to_gitdir(struct repository *repo,\n>  \treturn ret;\n>  }\n>  \n> +static void strbuf_addstr_case_encode(struct strbuf *dst, const char *src)\n> +{\n> +\tfor (; *src; src++) {\n> +\t\tunsigned char c = *src;\n> +\t\tif (c >= 'A' && c <= 'Z') {\n> +\t\t\tstrbuf_addch(dst, '_');\n> +\t\t\tstrbuf_addch(dst, c - 'A' + 'a');\n> +\t\t} else {\n> +\t\t\tstrbuf_addch(dst, c);\n> +\t\t}\n> +\t}\n> +}\n> +\n>  void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \t\t\t      const char *submodule_name)\n>  {\n> -\t/*\n> -\t * NEEDSWORK: The current way of mapping a submodule's name to\n> -\t * its location in .git/modules/ has problems with some naming\n> -\t * schemes. For example, if a submodule is named \"foo\" and\n> -\t * another is named \"foo/bar\" (whether present in the same\n> -\t * superproject commit or not - the problem will arise if both\n> -\t * superproject commits have been checked out at any point in\n> -\t * time), or if two submodule names only have different cases in\n> -\t * a case-insensitive filesystem.\n> -\t *\n> -\t * There are several solutions, including encoding the path in\n> -\t * some way, introducing a submodule.<name>.gitdir config in\n> -\t * .git/config (not .gitmodules) that allows overriding what the\n> -\t * gitdir of a submodule would be (and teach Git, upon noticing\n> -\t * a clash, to automatically determine a non-clashing name and\n> -\t * to write such a config), or introducing a\n> -\t * submodule.<name>.gitdir config in .gitmodules that repo\n> -\t * administrators can explicitly set. Nothing has been decided,\n> -\t * so for now, just append the name at the end of the path.\n> -\t */\n>  \tchar *gitdir_path, *key;\n>  \n>  \t/* Allow config override. */\n> @@ -2618,4 +2618,20 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \n>  \trepo_git_path_append(r, buf, \"modules/\");\n>  \tstrbuf_addstr(buf, submodule_name);\n> +\n> +\t/* Existing legacy non-encoded names are used as-is */\n> +\tif (is_git_directory(buf->buf))\n> +\t\treturn;\n> +\n> +\tif (the_repository->repository_format_submodule_encoding) {\n> +\t\tstruct strbuf tmp = STRBUF_INIT;\n> +\n> +\t\tstrbuf_reset(buf);\n> +\t\trepo_git_path_append(r, buf, \"modules/\");\n> +\n> +\t\tstrbuf_addstr_urlencode(&tmp, submodule_name, is_rfc3986_unreserved);\n> +\t\tstrbuf_addstr_case_encode(buf, tmp.buf);\n> +\n> +\t\tstrbuf_release(&tmp);\n> +\t}\n>  }\n> diff --git a/t/meson.build b/t/meson.build\n> index 11376b9e25..de277227a2 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -882,6 +882,7 @@ integration_tests = [\n>    't7422-submodule-output.sh',\n>    't7423-submodule-symlinks.sh',\n>    't7424-submodule-mixed-ref-formats.sh',\n> +  't7425-submodule-encoding.sh',\n>    't7450-bad-git-dotfiles.sh',\n>    't7500-commit-template-squash-signoff.sh',\n>    't7501-commit-basic-functionality.sh',\n> diff --git a/t/t7425-submodule-encoding.sh b/t/t7425-submodule-encoding.sh\n> new file mode 100755\n> index 0000000000..4ea385d882\n> --- /dev/null\n> +++ b/t/t7425-submodule-encoding.sh\n> @@ -0,0 +1,146 @@\n> +#!/bin/sh\n> +\n> +test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n> +\n> +. ./test-lib.sh\n> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n> +\n> +test_expect_success 'setup: allow file protocol' '\n> +\tgit config --global protocol.file.allow always\n> +'\n> +\n> +test_expect_success 'create repo with mixed encoded and non-encoded submodules' '\n> +\tgit init -b main legacy-sub &&\n> +\ttest_commit -C legacy-sub legacy-initial &&\n> +\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n> +\n> +\tgit init -b main new-sub &&\n> +\ttest_commit -C new-sub new-initial &&\n> +\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n> +\n> +\tgit init -b main main &&\n> +\t(\n> +\t\tcd main &&\n> +\t\tgit submodule add ../legacy-sub legacy &&\n> +\t\ttest_commit legacy-sub &&\n> +\n> +\t\tgit config core.repositoryformatversion 1 &&\n> +\t\tgit config extensions.submoduleEncoding true &&\n> +\n> +\t\tgit submodule add ../new-sub \"New Sub\" &&\n> +\t\ttest_commit new\n> +\t)\n> +'\n> +\n> +test_expect_success 'verify submodule name is properly encoded' '\n> +\tverify_submodule_gitdir_path main legacy modules/legacy &&\n> +\tverify_submodule_gitdir_path main \"New Sub\" modules/_new%20_sub\n> +'\n> +\n> +test_expect_success 'clone from repo with both legacy and new-style submodules' '\n> +\tgit clone --recurse-submodules main cloned-non-encoding &&\n> +\t(\n> +\t\tcd cloned-non-encoding &&\n> +\n> +\t\ttest_path_is_dir .git/modules/legacy &&\n> +\t\ttest_path_is_dir .git/modules/\"New Sub\" &&\n> +\n> +\t\tgit submodule status >list &&\n> +\t\ttest_grep \"$legacy_rev legacy\" list &&\n> +\t\ttest_grep \"$new_rev New Sub\" list\n> +\t) &&\n> +\n> +\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules main cloned-encoding &&\n> +\t(\n> +\t\tcd cloned-encoding &&\n> +\n> +\t\ttest_path_is_dir .git/modules/legacy &&\n> +\t\ttest_path_is_dir .git/modules/_new%20_sub &&\n> +\n> +\t\tgit submodule status >list &&\n> +\t\ttest_grep \"$legacy_rev legacy\" list &&\n> +\t\ttest_grep \"$new_rev New Sub\" list\n> +\t)\n> +'\n> +\n> +test_expect_success 'commit and push changes to encoded submodules' '\n> +\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n> +\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n> +\tgit -C main config receive.denyCurrentBranch updateInstead &&\n> +\t(\n> +\t\tcd cloned-encoding &&\n> +\n> +\t\tgit -C legacy switch --track -C main origin/main  &&\n> +\t\ttest_commit -C legacy second-commit &&\n> +\t\tgit -C legacy push &&\n> +\n> +\t\tgit -C \"New Sub\" switch --track -C main origin/main &&\n> +\t\ttest_commit -C \"New Sub\" second-commit &&\n> +\t\tgit -C \"New Sub\" push &&\n> +\n> +\t\t# Stage and commit submodule changes in superproject\n> +\t\tgit switch --track -C main origin/main  &&\n> +\t\tgit add legacy \"New Sub\" &&\n> +\t\tgit commit -m \"update submodules\" &&\n> +\n> +\t\t# push superproject commit to main repo\n> +\t\tgit push\n> +\t) &&\n> +\n> +\t# update expected legacy & new submodule checksums\n> +\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n> +\tnew_rev=$(git -C new-sub rev-parse HEAD)\n> +'\n> +\n> +test_expect_success 'fetch mixed submodule changes and verify updates' '\n> +\t(\n> +\t\tcd main &&\n> +\n> +\t\t# only update submodules because superproject was\n> +\t\t# pushed into at the end of last test\n> +\t\tgit submodule update --init --recursive &&\n> +\n> +\t\ttest_path_is_dir .git/modules/legacy &&\n> +\t\ttest_path_is_dir .git/modules/_new%20_sub &&\n> +\n> +\t\t# Verify both submodules are at the expected commits\n> +\t\tgit submodule status >list &&\n> +\t\ttest_grep \"$legacy_rev legacy\" list &&\n> +\t\ttest_grep \"$new_rev New Sub\" list\n> +\t)\n> +'\n> +\n> +test_expect_success 'setup submodules with nested git dirs' '\n> +\tgit init nested &&\n> +\ttest_commit -C nested nested &&\n> +\t(\n> +\t\tcd nested &&\n> +\t\tcat >.gitmodules <<-EOF &&\n> +\t\t[submodule \"hippo\"]\n> +\t\t\turl = .\n> +\t\t\tpath = thing1\n> +\t\t[submodule \"hippo/hooks\"]\n> +\t\t\turl = .\n> +\t\t\tpath = thing2\n> +\t\tEOF\n> +\t\tgit clone . thing1 &&\n> +\t\tgit clone . thing2 &&\n> +\t\tgit add .gitmodules thing1 thing2 &&\n> +\t\ttest_tick &&\n> +\t\tgit commit -m nested\n> +\t)\n> +'\n> +\n> +test_expect_success 'git dirs of encoded sibling submodules must not be nested' '\n> +\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules nested clone_nested &&\n> +\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n> +\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks\n> +'\n> +\n> +test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n> +\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules --jobs=2 nested clone_parallel &&\n> +\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n> +\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks\n> +'\n> +\n> +test_done\n"},{"id":"527996","messageId":"xmqqy0poot7g.fsf@gitster.g","threadId":"63967","inReplyTo":"20251006112518.3764240-6-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 5/5] submodule: error out if gitdir name is too long","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-06T17:06:43Z","receivedAt":"2025-10-06T17:06:46Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Encoding submodule names increases their name size, so there is an\n> increased risk to hit the max filename length in the gitdir path.\n> (the likelihood is still rather small, so it's an acceptable risk)\n\nIf it is acceptable, can we ignore it?\n\nJust stepping back a bit, how are we keeping track of the mapping\nbetween submodule names vs locations in .git/modules/?  Don't we\nalways go through that mapping and would a half-clever code that\nsays \"heh, that is url encoded and I know how to decode it\" and\nbypass the mapping a bug?\n\nIf we keep track of the mapping ourselves, then the names under\n.git/modules/ do not have to be \"decodable\" by themselves.  They can\neven be sequence numbers and that would not hit any maximum filename\nlength before you fill your disk.\n\nNo, no I am not suggesting to use sequence numbers; something\nremotely readable by humans is better.  But my point is that just\nlike you have to make sure that the encoded name you give to a new\nthing does not collide with existing names (you know with \"ls\n.git/modules/\" what names are taken), you can notice your mkdir()\nwould not error with name-too-long, truncate and twiddle with suffix\nto make it unique and retry, without giving a failure to the end\nuser.\n\n\n\n"},{"id":"528009","messageId":"xmqqcy6zq5wp.fsf@gitster.g","threadId":"63967","inReplyTo":"xmqq8qhoq8hg.fsf@gitster.g","subject":"Re: [PATCH v3 3/5] strbuf: bring back is_rfc3986_unreserved","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-06T17:47:02Z","receivedAt":"2025-10-06T17:47:05Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n>> is_rfc3986_unreserved() was moved to credential-store.c and was made\n>> static by f89854362c (credential-store: move related functions to\n>> credential-store file, 2023-06-06) under a correct assumption, at the\n>> time, that it was the only place using it.\n>>\n>> However now we need it to apply URL-encoding to submodule names when\n>> constructing gitdir paths, to avoid conflicts, so bring it back.\n>\n> Why to strbuf, though?\n>\n> This does not have anything to do with what strbuf does.  I could be\n> possible that strbuf.c had some function that encodes/decodes 3986\n> in a strbuf and this may have been a useful helper for that feature,\n> but it is apparent that this helper function is needed by\n\n\"is needed\" -> \"is not needed\", of course.  Sorry for a typo.\n\n> strbuf.[ch] in today's code, so moving it to strbuf.[ch] makes no\n> sense to me.\n"},{"id":"528058","messageId":"87plaz3w0w.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqh5wcq94v.fsf@gitster.g","subject":"Re: [PATCH v3 1/5] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-07T09:23:43Z","receivedAt":"2025-10-07T09:24:14Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 06 Oct 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> While testing submodule gitdir path encoding, I noticed \n>> submodule--helper is still using a hardcoded name-based path \n>> leading to test failures, so convert it to the common helper \n>> function introduced by commit ce125d431a (submodule: extract \n>> path to submodule gitdir func, 2021-09-15)  and used in other \n>> locations across the source tree. \n> \n> OK.  To me during my first reading, the above read as if you \n> found an open coded logic here in add_submodule(), made it into \n> a new common helper function, and made this part as well as \n> other locations call that new common helper function.  Of course \n> that is not the case. \n> \n> Perhaps replacing everything after \", so convert it\" with \n> something simpler like \n> \n>     ... to test failures.  Call submodule_name_to_gitdir() \n>     helper instead, which was invented exactly for this purpose \n>     and everybody else uses.  \n> might have helped me to avoid such a confusion.  I dunno. \n\nAck, I'll reword as you suggested to make it clearer.\n\n \n>> diff --git a/builtin/submodule--helper.c \n>> b/builtin/submodule--helper.c index fcd73abe53..2873b2780e \n>> 100644 --- a/builtin/submodule--helper.c +++ \n>> b/builtin/submodule--helper.c @@ -3187,13 +3187,13 @@ static \n>> void append_fetch_remotes(struct strbuf *msg, const char \n>> *git_dir_path) \n>>   static int add_submodule(const struct add_data *add_data) { \n>> -\tchar *submod_gitdir_path; \n>>  \tstruct module_clone_data clone_data = \n>>  MODULE_CLONE_DATA_INIT; struct string_list reference = \n>>  STRING_LIST_INIT_NODUP; int ret = -1;  /* perhaps the path \n>>  already exists and is already a git repo, else clone it */ if \n>>  (is_directory(add_data->sm_path)) { \n>> +\t\tchar *submod_gitdir_path; \n> \n> This hunk is not related to the theme of the change and not \n> explained?  I think the variable becomes used only within this \n> block after the patch that loses the use of it on the \"else\" \n> side, so in that sense it is not strictly unrelated, but is a \n> fallout of this change.  If we were to mention the change in the \n> log message, something like \"Also narrow the scope of a variable \n> that is no longer used in the updated code\" would suffice.\n\nYour understanding is correct, yes. I'll add it to the log msg.\n"},{"id":"528059","messageId":"87ms633v3g.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqq8qhoq8hg.fsf@gitster.g","subject":"Re: [PATCH v3 3/5] strbuf: bring back is_rfc3986_unreserved","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-07T09:43:47Z","receivedAt":"2025-10-07T09:44:17Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 06 Oct 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> is_rfc3986_unreserved() was moved to credential-store.c and was \n>> made static by f89854362c (credential-store: move related \n>> functions to credential-store file, 2023-06-06) under a correct \n>> assumption, at the time, that it was the only place using it. \n>> \n>> However now we need it to apply URL-encoding to submodule names \n>> when constructing gitdir paths, to avoid conflicts, so bring it \n>> back. \n> \n> Why to strbuf, though? \n> \n> This does not have anything to do with what strbuf does.  I \n> could be possible that strbuf.c had some function that \n> encodes/decodes 3986 in a strbuf and this may have been a useful \n> helper for that feature, but it is apparent that this helper \n> function is needed by strbuf.[ch] in today's code, so moving it \n> to strbuf.[ch] makes no sense to me. \n\nAgreed. The only reason I moved it back to strbuf is because it \nwas there in the past, however we can move it to anywhere else.\n\nPerhaps url.[ch] is the best place. Would that be ok?\n\nOther location suggestions are welcome btw. :)\n"},{"id":"528061","messageId":"87jz173tih.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqy0poot7g.fsf@gitster.g","subject":"Re: [PATCH v3 5/5] submodule: error out if gitdir name is too long","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-07T10:17:58Z","receivedAt":"2025-10-07T10:18:36Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 06 Oct 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> Encoding submodule names increases their name size, so there is \n>> an increased risk to hit the max filename length in the gitdir \n>> path.  (the likelihood is still rather small, so it's an \n>> acceptable risk) \n> \n> If it is acceptable, can we ignore it? \n\nYes, we can.\n\nPeff actually asked if it's worth pursuing this path. We can \ncertainly ignore and let the OS fail like it does now, without the \ncompat wrapper.\n\nThe only usefulness of this commit as-is is to to print a nicer \nerror msg and allow us to eventually address the patch TODO \n(sharding/trimming).\n\nPlease let me know if you wish to drop this commit in v4. I am \nperfectly fine with removing it. :)\n\n> \n> Just stepping back a bit, how are we keeping track of the \n> mapping between submodule names vs locations in .git/modules/? \n> Don't we always go through that mapping and would a half-clever \n> code that says \"heh, that is url encoded and I know how to \n> decode it\" and bypass the mapping a bug?\n\nThe short answer is no. :) The slightly longer answer is:\n\nBefore v3, we used to have a clear filesystem separation:\n.git/modules were unencoded\n.git/submodules were encoded\n\nAfter v3, when the extension is enabled, we encode all submodule \nnames with one exception: if they already exist, at which point \nthey are treated as unencoded.\n \n> \n> If we keep track of the mapping ourselves, then the names under \n> .git/modules/ do not have to be \"decodable\" by themselves.  They \n> can even be sequence numbers and that would not hit any maximum \n> filename length before you fill your disk.\n\nIIRC the consensus was to avoid keeping such a mapping due to \ncomplexity, risk of desynchronizing with the filesystem layout and \nso on, so simple rules like the above, or even a simple hash_name \nmight be better (see below).\n \n> \n> No, no I am not suggesting to use sequence numbers; something \n> remotely readable by humans is better.  But my point is that \n> just like you have to make sure that the encoded name you give \n> to a new thing does not collide with existing names (you know \n> with \"ls .git/modules/\" what names are taken), you can notice \n> your mkdir() would not error with name-too-long, truncate and \n> twiddle with suffix to make it unique and retry, without giving \n> a failure to the end user. \n\nEverything is on the table and I have no strong opinions. :)\n\nSomeone suggested we use something like .git/modules/<hash>_<name> \nwhen the extension is enabled, so we have a simple, unique and \nrecognizable pattern.\n\nWhat do you think of that?\n"},{"id":"528079","messageId":"87frbv3qyr.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqo6qkq9vm.fsf@gitster.g","subject":"Re: [PATCH v3 0/5] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-07T11:13:00Z","receivedAt":"2025-10-07T11:13:32Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 06 Oct 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> Hello everyone, \n>> \n>> v3 is much simplified from v2, starting from the design idea \n>> that submodule gitdir name encoding is to be put behind an \n>> extensions.submoduleEncoding. \n> \n> This design decision to make it an extension makes a repository \n> with a new-style submodule incompatible with older Git, which \n> may not matter all that much unless you use third-party tools \n> that come with their own version of Git embedded (which by \n> definition can become stale). \n> \n> If you already have submodules creted under the original scheme, \n> then add a new submodule that needs this extension, do you \n> enable this new extension and write the new submodule under \n> encoded name, and move the existing submodules under their \n> encoded names? \n\nExcellent observation! We could do that (it's not being done in \nv3).\n\nCurrently any existing submodule gitdir names are left untouched \nand are used as-is (unencoded) after the extension is enabled.\n\nIt's been done like this for backwards compatibility, to eliminate \nany potential risk of breakage by having to move/update gitdirs, \nhowever maybe we've been overly cautious and we can attempt a \n\"migration\" once the extension is enabled, to move the submodule \ngitdirs.\n\n> \n>> This allowed removal of the modules vs submodules directories \n>> split and simplified our logic quite a lot. Tests have been \n>> been squashed in the smaller commits as well. \n> \n> By this statement, I am guessing that the answer is yes?  That \n> would make it consistent.  The last thing we want here is the \n> code that needs to guess which ones are encoded and which ones \n> are not. \n\nNo, not yet, though you do raise a fair point.\n\nWe could do a migration of existing gitdirs to the new encoding to \nensure consistency when the extension is enabled.\n\nThis will simplify our logic and assumptions a lot, at the cost of \nthe initial up-front migration.\n\nWill do this in v4 if nobody has any objections.\n"},{"id":"528119","messageId":"87cy6y4xb4.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqq347wq87s.fsf@gitster.g","subject":"Re: [PATCH v3 4/5] submodule: encode gitdir paths to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-07T14:10:39Z","receivedAt":"2025-10-07T14:11:05Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 06 Oct 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> A custom encoding can become unnecessarily complex, while \n>> url-encoding is relatively well-known, however it needs some \n>> extending to support case insensitive filesystems, hence why A \n>> is encoded as _a, B as _b and so on. \n> \n> Why 'A' cannot be encoded as %41 while encodign 'a' as %61?  Are \n> there case insensitive filesystems that cannot see the \n> difference between %41 and %61?  And they would not collide with \n> COM: and other anomalies, would they? \n\nThat is correct.\n\nThe only reason I chose A -> _a and so on is because it was \nsuggested in the initial thread from 8+ years ago when people \ndiscussed creating a custom encoding, but we can use anything \nelse.\n\nI will percent encode the upper case by modifying \nis_rfc3986_unreserved().\n \n>> For now url-encoding is the only option, however in the future \n>> we may add alternatives (other encodings, hashes or even \n>> hash_name). \n> \n> Let's not say \"For now\". \n> \n> Choose a single encoding that we can use forever so that we do \n> not have to upgrade extensions.encodeSubmoduleName with suffixes \n> like extensions.encodeSubmoduleNamev2, \n> extensions.encodeSubmoduleNamev3, etc. to cover our earlier \n> mistakes and force renaming on users. \n\nUnderstood. Will drop that idea.\n"},{"id":"528126","messageId":"xmqq5xcqn2pe.fsf@gitster.g","threadId":"63967","inReplyTo":"87frbv3qyr.fsf@collabora.com","subject":"Re: [PATCH v3 0/5] Encode submodule gitdir names to avoid conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-07T15:36:45Z","receivedAt":"2025-10-07T15:36:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Currently any existing submodule gitdir names are left untouched \n> and are used as-is (unencoded) after the extension is enabled.\n\nAnd in order to make sure that a funny names and paths in existing\nsubmodules that can be misinterpreted as encoded would be registered\nwith the new submodule.<name>.gitdirpath variable?\n\nThat would be a robust way to transition.  It also means that you\nhave a mapping from submodule name to path, and you will have to\nmake an effort to maintain that mapping and the reality on the\nfilesystem in sync.\n\nSo why not take advantage of the fact that you are making that\neffort anyway?  It can simplify things quite a bit.  Imagine what\nwould happen if we did this:\n\n - You officially declare that submodule.<name>.gitdirpath is _the_\n   mapping mechanism, not a mere \"override\".\n\n - When enabling the extension, you register all submodules that\n   already has their gitdirs on the filesystem to the mapping\n   mechanism under their \"historical and natural\" names.\n\n - When you add a new submodule, you \"munge\" its name to be used as\n   a subdirectory name under .git/modules/.  You only specify for\n   the end users the purpose and nature of this munging, perhaps\n   like\n\n   - (purpose) We give each submodule a place in .git/modules\n     directory of the superproject to store its repository data, but\n     the names of submodules we find in .gitmodules may not\n     necessarily be friendly to the filesystem (e.g., \"CON:\", or\n     longer than the filesystem allows for a single path component),\n     or may introduce a subdirectory (e.g., slashes and\n     backslashes), or two directories whose names only differ in\n     case may not coexist on your filesystem.  That is why we do not\n     use the name found in .gitmodules as-is.\n\n   - (nature) This \"munging\" would remove problematic bytes or\n     replace them with safe ones in the name, or truncate overly\n     long string, or insert sequence numbers to make the result\n     unique and representable on the filesystem.  Hopefully the\n     result of the munging may still be recognisable as derived from\n     the original name, but it is *not* designed to be reversible by\n     itself.  The submodule.<name>.gitdirpath is the only and\n     authoritative place to learn how <name> got munged to produce a\n     path.\n\n   without having to go into the details to avoid tempting users to\n   write scripts that guess and decode bypassing the mapping.\n\nHmm?\n"},{"id":"528127","messageId":"xmqqzfa2lnxi.fsf@gitster.g","threadId":"63967","inReplyTo":"xmqqcy70q8n7.fsf@gitster.g","subject":"Re: [PATCH v3 2/5] submodule: add gitdir path config override","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-07T15:41:13Z","receivedAt":"2025-10-07T15:41:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n> [jc: brandon removed from CC list as the address would bounce]\n>\n>> This adds the ability to override gitdir paths via config files\n>> (not .gitmodules) such that the encoding scheme (or plain text\n>> name if the encoding extension is disabled) can be changed via\n>> config entries.\n>>\n>> These entries are not added by default for all submodules: they\n>> should be used on an as-needed basis.\n>>\n>> A new test and a helper are added. The helper will also be used\n>> in further tests exercising gitdir encoding functionality.\n>\n> What is the use case of this?  The only reasonable use case I can\n> see is to set this to all the existing submodules when you are\n> switching the extension on before adding a new submodule, in which\n> case the old ones will keep using unencoded names, while the new\n> ones will use encoded ones.\n\nTwo things.\n\n * I no longer mind this setting existing, but I think it should not\n   be a mere \"override\", but the authoritative source of truth for\n   all submodules (see my other response on 0/5).\n\n * The documentation part of this patch says submodule.<name>.gitdir,\n   but what the code implements is submodule.<name>.gitdirpath.\n\nThanks.\n"},{"id":"528128","messageId":"xmqqqzveln5j.fsf@gitster.g","threadId":"63967","inReplyTo":"87jz173tih.fsf@collabora.com","subject":"Re: [PATCH v3 5/5] submodule: error out if gitdir name is too long","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-07T15:58:00Z","receivedAt":"2025-10-07T15:58:03Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> IIRC the consensus was to avoid keeping such a mapping due to \n> complexity, risk of desynchronizing with the filesystem layout and \n> so on, so simple rules like the above, or even a simple hash_name \n> might be better (see below).\n\nI do not quite see a merit in the above argument.\n\nYou have submodule.<name>.gitdir that you need to make sure your\n\"risk of desynchronizing\" is managed anyway.  By making sure\nname-to-directory always goes through the single mapping, you reduce\nthe complexity and concentrate the choice of the path to a single\nplace, i.e. where it is assigned by munging the name and registered\nto the mapping.\n\n> Someone suggested we use something like .git/modules/<hash>_<name> \n> when the extension is enabled, so we have a simple, unique and \n> recognizable pattern.\n>\n> What do you think of that?\n\nIf there are letters in <name> that you cannot use there (e.g.,\nslash, or perhaps <name> is overly long), you have to munge the\n<name> part in <hash>_<name> in order to fit the filesystem\nconstraints.  And prefixing random string \"<hash>_\" would be one way\nto make sure two <name>s that happen to collide after munging can\nstill be differentiated.  So I have no problem with prefixing or\nsuffixing, but I do not think that syntax alone is sufficient to\nsolve other issues that come from the fact that some names are not\nfilesystem friendly and can collide with names of other submodules.\n\nThanks.\n"},{"id":"528130","messageId":"xmqqldlmlm1n.fsf@gitster.g","threadId":"63967","inReplyTo":"87frbv3qyr.fsf@collabora.com","subject":"Re: [PATCH v3 0/5] Encode submodule gitdir names to avoid conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-07T16:21:56Z","receivedAt":"2025-10-07T16:22:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n>> If you already have submodules creted under the original scheme, \n>> then add a new submodule that needs this extension, do you \n>> enable this new extension and write the new submodule under \n>> encoded name, and move the existing submodules under their \n>> encoded names? \n> ...\n> We could do a migration of existing gitdirs to the new encoding to \n> ensure consistency when the extension is enabled.\n>\n> This will simplify our logic and assumptions a lot, at the cost of \n> the initial up-front migration.\n>\n> Will do this in v4 if nobody has any objections.\n\nLet's not.\n\nYou have support for submodule.<name>.gitdirpath already, so it is\nfar safer to use that mechanism to etch-in-stone-fix the existing\nsubmodules and their gitdirs without touching the directories for\nmigration.\n\nOne case you might want to really move directories when migrating is\nwhen two existing submodules' gitdirs are already overlapping, e.g.,\n.git/modules/A and .git/modules/A/B are used for submodule A and\nsubmodule A/B.  Depending on what \"B\" is, a project with such a\nlayout may not be able to upgrade to versions of Git that newly\nstarts using .git/B directory for a new feature.  Introduction of\na new directory or a new file directly underneath $GIT_DIR is rare\nbut does happen (.git/reftable/ is a relatively recent addition, for\nexample).\n\nThanks.\n"},{"id":"528133","messageId":"878qhm4pk2.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqq5xcqn2pe.fsf@gitster.g","subject":"Re: [PATCH v3 0/5] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-07T16:58:05Z","receivedAt":"2025-10-07T16:58:32Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 07 Oct 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> Currently any existing submodule gitdir names are left \n>> untouched  and are used as-is (unencoded) after the extension \n>> is enabled. \n> \n> And in order to make sure that a funny names and paths in \n> existing submodules that can be misinterpreted as encoded would \n> be registered with the new submodule.<name>.gitdirpath variable? \n> \n> That would be a robust way to transition.  It also means that \n> you have a mapping from submodule name to path, and you will \n> have to make an effort to maintain that mapping and the reality \n> on the filesystem in sync. \n> \n> So why not take advantage of the fact that you are making that \n> effort anyway?  It can simplify things quite a bit.  Imagine \n> what would happen if we did this: \n> \n>  - You officially declare that submodule.<name>.gitdirpath is \n>  _the_ \n>    mapping mechanism, not a mere \"override\". \n> \n>  - When enabling the extension, you register all submodules that \n>    already has their gitdirs on the filesystem to the mapping \n>    mechanism under their \"historical and natural\" names. \n> \n>  - When you add a new submodule, you \"munge\" its name to be used \n>  as \n>    a subdirectory name under .git/modules/.  You only specify \n>    for the end users the purpose and nature of this munging, \n>    perhaps like \n> \n>    - (purpose) We give each submodule a place in .git/modules \n>      directory of the superproject to store its repository data, \n>      but the names of submodules we find in .gitmodules may not \n>      necessarily be friendly to the filesystem (e.g., \"CON:\", or \n>      longer than the filesystem allows for a single path \n>      component), or may introduce a subdirectory (e.g., slashes \n>      and backslashes), or two directories whose names only \n>      differ in case may not coexist on your filesystem.  That is \n>      why we do not use the name found in .gitmodules as-is. \n> \n>    - (nature) This \"munging\" would remove problematic bytes or \n>      replace them with safe ones in the name, or truncate overly \n>      long string, or insert sequence numbers to make the result \n>      unique and representable on the filesystem.  Hopefully the \n>      result of the munging may still be recognisable as derived \n>      from the original name, but it is *not* designed to be \n>      reversible by itself.  The submodule.<name>.gitdirpath is \n>      the only and authoritative place to learn how <name> got \n>      munged to produce a path. \n> \n>    without having to go into the details to avoid tempting users \n>    to write scripts that guess and decode bypassing the mapping. \n> \n> Hmm? \n\nYes, I think that can work. Will do in v4. Thanks!\n"},{"id":"528139","messageId":"xmqqo6qik4r9.fsf@gitster.g","threadId":"63967","inReplyTo":"87cy6y4xb4.fsf@collabora.com","subject":"Re: [PATCH v3 4/5] submodule: encode gitdir paths to avoid conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-07T17:20:42Z","receivedAt":"2025-10-07T17:20:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> I will percent encode the upper case by modifying \n> is_rfc3986_unreserved().\n\nAgain, do we really need to encode everything?  We need to only when\n(1) there are other submodules that would collide with us (e.g., by\nbeing only case-different, or by having our name plus a slash as the\nprefix of their name, make us overlap with them) or (2) our name is\nnot filesystem friendly and needs munging.  A letter being in an upper\ncase is not a crime.\n\n>>> For now url-encoding is the only option, however in the future \n>>> we may add alternatives (other encodings, hashes or even \n>>> hash_name). \n>> \n>> Let's not say \"For now\". \n>> \n>> Choose a single encoding that we can use forever so that we do \n>> not have to upgrade extensions.encodeSubmoduleName with suffixes \n>> like extensions.encodeSubmoduleNamev2, \n>> extensions.encodeSubmoduleNamev3, etc. to cover our earlier \n>> mistakes and force renaming on users. \n>\n> Understood. Will drop that idea.\n\nIf we are to consistently use submodule.<name>.gitdirpath as the\nauthoritative collection of name-path mapping, then the exact\nalgorithm to derive path from name can be improved over time without\nhaving to worry about an old submodule whose path was computed by\nolder iteration of the algorithm colliding with a new submodule\nwhose path is derived by the more modern algorithm.  So I do not\nmind to make ourselves aware of the possibility that we can tweak\nand improve.  But as I said elsewhere, I do not think we need to\neven say what exactly algorithm is used to the end-users.\n\nThanks.\n"},{"id":"528140","messageId":"875xcq4oha.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqldlmlm1n.fsf@gitster.g","subject":"Re: [PATCH v3 0/5] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-07T17:21:21Z","receivedAt":"2025-10-07T17:21:54Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 07 Oct 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>>> If you already have submodules creted under the original \n>>> scheme,  then add a new submodule that needs this extension, \n>>> do you  enable this new extension and write the new submodule \n>>> under  encoded name, and move the existing submodules under \n>>> their  encoded names?  \n>> ...  We could do a migration of existing gitdirs to the new \n>> encoding to  ensure consistency when the extension is enabled. \n>> \n>> This will simplify our logic and assumptions a lot, at the cost \n>> of  the initial up-front migration. \n>> \n>> Will do this in v4 if nobody has any objections. \n> \n> Let's not. \n> \n> You have support for submodule.<name>.gitdirpath already, so it \n> is far safer to use that mechanism to etch-in-stone-fix the \n> existing submodules and their gitdirs without touching the \n> directories for migration. \n\nAck. I'll follow this design path in v4.\n\nI was actually preparing to do the opposite :) i.e. drop the \n\"override\" and do the migration, thanks for clarifying before I \ndid a re-roll. :)\n\nWill leave this a week or two on the ML in case others want to \nreview or have more feedback.\n\n> \n> One case you might want to really move directories when \n> migrating is when two existing submodules' gitdirs are already \n> overlapping, e.g., .git/modules/A and .git/modules/A/B are used \n> for submodule A and submodule A/B.  Depending on what \"B\" is, a \n> project with such a layout may not be able to upgrade to \n> versions of Git that newly starts using .git/B directory for a \n> new feature.  Introduction of a new directory or a new file \n> directly underneath $GIT_DIR is rare but does happen \n> (.git/reftable/ is a relatively recent addition, for example). \n\nYes, though I don't think this is a big concern because \nsubmodule.c already has the validate_submodule_git_dir() check \nwhich prevents users from creating overlapping / nested dirs.\n\nWhen the rare, deliberate clash does happen (like with \n.git/reftable) it can be treated like before: do nothing, let the \nuser fix the repo, or we could add a small one-time migration at \nthat point in time.\n\nI really like avoiding any kind of automated blanket migration btw,\nthank you so much for your feedback Junio, it's really appreciated.\n"},{"id":"528141","messageId":"xmqqjz16k4gr.fsf@gitster.g","threadId":"63967","inReplyTo":"xmqq5xcqn2pe.fsf@gitster.g","subject":"Re: [PATCH v3 0/5] Encode submodule gitdir names to avoid conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-07T17:27:00Z","receivedAt":"2025-10-07T17:27:03Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n>> Currently any existing submodule gitdir names are left untouched \n>> and are used as-is (unencoded) after the extension is enabled.\n>\n> And in order to make sure that a funny names and paths in existing\n> submodules that can be misinterpreted as encoded would be registered\n> with the new submodule.<name>.gitdirpath variable?\n\nSorry but this is -ECANNOTPARSE; let me try again.\n\n    An existing submodule's gitdir may look like an encoded one (it may\n    be littered with %xx if urlencode is what you chose), but we do not\n    want it to be misinterpreted as such.  One way to ensure that the\n    gitdir of the submodule <name> is literally that path is to use\n    submodule.<name>.gitdirpath to point at the directory, right?\n\n> That would be a robust way to transition.\n"},{"id":"528146","messageId":"87347u4nkh.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqo6qik4r9.fsf@gitster.g","subject":"Re: [PATCH v3 4/5] submodule: encode gitdir paths to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-07T17:41:02Z","receivedAt":"2025-10-07T17:41:25Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 07 Oct 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> I will percent encode the upper case by modifying \n>> is_rfc3986_unreserved(). \n> \n> Again, do we really need to encode everything?  We need to only \n> when (1) there are other submodules that would collide with us \n> (e.g., by being only case-different, or by having our name plus \n> a slash as the prefix of their name, make us overlap with them) \n> or (2) our name is not filesystem friendly and needs munging.  A \n> letter being in an upper case is not a crime. \n\nRight, I see what you mean now.\n\nThe problem becomes how to detect (1) and (2), for example: a \nspecific name is filesystem-friendly or is case-folding happening \nor not?\n\n(ext4 for example can be mounted with casefolding on/off)\n\nThis is not a trivial problem, however I think it is solvable. We \ncan write a few runtime tests to detect things like case-folding, \nto decide if encoding is required or not (better ideas welcome).\n\nOn the other hand, by always encoding everything we avoid the (1) \n(2) detection problems. :)\n\n> \n>>>> For now url-encoding is the only option, however in the \n>>>> future  we may add alternatives (other encodings, hashes or \n>>>> even  hash_name).  \n>>>  Let's not say \"For now\".    Choose a single encoding that we \n>>> can use forever so that we do  not have to upgrade \n>>> extensions.encodeSubmoduleName with suffixes  like \n>>> extensions.encodeSubmoduleNamev2, \n>>> extensions.encodeSubmoduleNamev3, etc. to cover our earlier \n>>> mistakes and force renaming on users.  \n>> \n>> Understood. Will drop that idea. \n> \n> If we are to consistently use submodule.<name>.gitdirpath as the \n> authoritative collection of name-path mapping, then the exact \n> algorithm to derive path from name can be improved over time \n> without having to worry about an old submodule whose path was \n> computed by older iteration of the algorithm colliding with a \n> new submodule whose path is derived by the more modern \n> algorithm.  So I do not mind to make ourselves aware of the \n> possibility that we can tweak and improve.  But as I said \n> elsewhere, I do not think we need to even say what exactly \n> algorithm is used to the end-users. \n\nGot it. I'll do the submodule.<name>.gitdirpath centric design in \nv4 and will not mention the encoding algorithm to users.\n"},{"id":"528153","messageId":"xmqq347ujxld.fsf@gitster.g","threadId":"63967","inReplyTo":"87347u4nkh.fsf@collabora.com","subject":"Re: [PATCH v3 4/5] submodule: encode gitdir paths to avoid conflicts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-07T19:55:26Z","receivedAt":"2025-10-07T19:55:29Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> The problem becomes how to detect (1) and (2), for example: a \n> specific name is filesystem-friendly or is case-folding happening \n> or not?\n>\n> (ext4 for example can be mounted with casefolding on/off)\n\ncore.ignorecase is dynamically probed upon reposiory creation, if I\nam not mistaken.\n\nBut other things like \"I have this name, transformed to avoid\nslashes and other problematic letters and the result got this long,\nwould it fit or would I get ENAMETOOLONG?\", I think the code may\nhave to be prepared to try-fail-adjust-retry.  We come up with a\nproposed \"munged\" name, try to mkdir() with that name, see that it\nfails with ENAMETOOLONG, shorten the munged name and retry.  And we\nensure the name we propose is unique among submodule.<name>.gitdirpath\npaths in use, then hopefully we find a good name that fits, recognisable\nby human, acceptable by the filesystem and unique among submodules.\n\nEven without consulting core.ignorecase, I suspect that the code can\ndo a similar try-fail-adjust-retry for case-insensitive or NFC/NFD\nclashes.  We try to mkdir() with a munged name, and if we get\nEEXISTS and submodule.<name>.gitdirpath would not have the path we\ntried already registered, then we are seeing another path that is\nnot byte-for-byte identical to ours conflicting, so we can adjust\nours and retry, for example.\n"},{"id":"529234","messageId":"aPc-4FHCpHYgQ9hs@pks.im","threadId":"63967","inReplyTo":"20251006112518.3764240-3-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 2/5] submodule: add gitdir path config override","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T08:05:52Z","receivedAt":"2025-10-21T08:06:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Oct 06, 2025 at 02:25:15PM +0300, Adrian Ratiu wrote:\n> diff --git a/submodule.c b/submodule.c\n> index 35c55155f7..7a2d7cd592 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2604,6 +2604,18 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \t * administrators can explicitly set. Nothing has been decided,\n>  \t * so for now, just append the name at the end of the path.\n>  \t */\n> +\tchar *gitdir_path, *key;\n> +\n> +\t/* Allow config override. */\n> +\tkey = xstrfmt(\"submodule.%s.gitdirpath\", submodule_name);\n> +\tif (!repo_config_get_string(r, key, &gitdir_path)) {\n> +\t\tstrbuf_addstr(buf, gitdir_path);\n> +\t\tfree(key);\n> +\t\tfree(gitdir_path);\n> +\t\treturn;\n> +\t}\n> +\tfree(key);\n> +\n>  \trepo_git_path_append(r, buf, \"modules/\");\n>  \tstrbuf_addstr(buf, submodule_name);\n>  }\n\nYou can use `repo_config_get_string_tmp()` to avoid having to manage the\n`gitdir_path` lifetime.\n\n> diff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\n> new file mode 100644\n> index 0000000000..3a83f2d975\n> --- /dev/null\n> +++ b/t/lib-verify-submodule-gitdir-path.sh\n> @@ -0,0 +1,20 @@\n> +# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n> +\n> +# This does not check filesystem existence. That is done in submodule.c via the\n> +# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n> +# might or might not exist (e.g. when adding a new submodule), so this only\n> +# checks the expected configuration path, which might be overridden by the user.\n> +\n> +verify_submodule_gitdir_path() {\n> +\trepo=\"$1\" &&\n> +\tname=\"$2\" &&\n> +\tpath=\"$3\" &&\n> +\t(\n> +\t\tcd \"$repo\" &&\n> +\t\tcat >expect <<-EOF &&\n> +\t\t\t$(git rev-parse --git-common-dir)/$path\n> +\t\tEOF\n\nStyle nit: we typically don't indent the heredoc body.\n\n> +\t\tgit submodule--helper gitdir \"$name\" >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +}\n> diff --git a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh\n> index fd3e7e355e..11c84a7bdf 100755\n> --- a/t/t7400-submodule-basic.sh\n> +++ b/t/t7400-submodule-basic.sh\n> @@ -13,6 +13,7 @@ GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n>  export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n>  \n>  . ./test-lib.sh\n> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n>  \n>  test_expect_success 'setup - enable local submodules' '\n>  \tgit config --global protocol.file.allow always\n> @@ -1505,4 +1506,12 @@ test_expect_success 'submodule add fails when name is reused' '\n>  \t)\n>  '\n>  \n> +test_expect_success 'submodule helper gitdir config overrides' '\n> +\tverify_submodule_gitdir_path test-submodule child modules/child &&\n> +\ttest_config -C test-submodule submodule.child.gitdirpath \".git/modules/custom-child\" &&\n> +\tverify_submodule_gitdir_path test-submodule child modules/custom-child &&\n> +\ttest_unconfig -C test-submodule submodule.child.gitdirpath &&\n> +\tverify_submodule_gitdir_path test-submodule child modules/child\n> +'\n> +\n>  test_done\n\nI feel like it's a bit curious that we recognize the configuration even\nthough \"extensions.submodulePath\" hasn't been introduced yet. I would\nexpect that we ignore the config key if that extension is not set, as\nthe extension otherwise seems to not be doing its job, does it?\n\nPatrick\n"},{"id":"529235","messageId":"aPc-6OBaFzO_jkXd@pks.im","threadId":"63967","inReplyTo":"20251006112518.3764240-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 3/5] strbuf: bring back is_rfc3986_unreserved","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T08:06:00Z","receivedAt":"2025-10-21T08:06:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Oct 06, 2025 at 02:25:16PM +0300, Adrian Ratiu wrote:\n> diff --git a/strbuf.h b/strbuf.h\n> index a580ac6084..5139269039 100644\n> --- a/strbuf.h\n> +++ b/strbuf.h\n> @@ -640,6 +640,8 @@ static inline void strbuf_complete_line(struct strbuf *sb)\n>  \n>  typedef int (*char_predicate)(char ch);\n>  \n> +int is_rfc3986_unreserved(char ch);\n\nI think it would help if we had a short comment here explaining what it\ndoes. I doubt that most people immediately go \"Ah, RFC3986!\". So maybe\nexplaining in a sentence or two what this is roughly doing would help\nthem.\n\nPatrick\n"},{"id":"529236","messageId":"aPc-79_XLyTjA_w0@pks.im","threadId":"63967","inReplyTo":"20251006112518.3764240-6-adrian.ratiu@collabora.com","subject":"Re: [PATCH v3 5/5] submodule: error out if gitdir name is too long","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T08:06:07Z","receivedAt":"2025-10-21T08:06:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Oct 06, 2025 at 02:25:18PM +0300, Adrian Ratiu wrote:\n> diff --git a/compat/posix.h b/compat/posix.h\n> index 067a00f33b..aa050fd58c 100644\n> --- a/compat/posix.h\n> +++ b/compat/posix.h\n> @@ -250,6 +250,14 @@ char *gitdirname(char *);\n>  #define NAME_MAX 255\n>  #endif\n>  \n> +#ifdef NO_PATHCONF\n> +#ifndef _PC_NAME_MAX\n> +#define _PC_NAME_MAX 1 /* dummy value, only used for git_pathconf */\n\nAre there platforms that have pathconf(3) but not _PC_NAME_MAX?\n\n> +#endif\n> +#define pathconf(a,b) git_pathconf(a,b)\n> +long git_pathconf(const char *path, int name);\n> +#endif\n> +\n>  typedef uintmax_t timestamp_t;\n>  #define PRItime PRIuMAX\n>  #define parse_timestamp strtoumax\n\nLet's adapt this to our coding guidelines to make this easier to parse:\n\n - Nested C preprocessor directives are indented after the hash by one\n   space per nesting level.\n\n\t#if FOO\n\t# include <foo.h>\n\t# if BAR\n\t#  include <bar.h>\n\t# endif\n\t#endif\n\nPatrick\n"},{"id":"529237","messageId":"aPc-98ps84R45MBF@pks.im","threadId":"63967","inReplyTo":"xmqqzfa2lnxi.fsf@gitster.g","subject":"Re: [PATCH v3 2/5] submodule: add gitdir path config override","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T08:06:15Z","receivedAt":"2025-10-21T08:06:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 07, 2025 at 08:41:13AM -0700, Junio C Hamano wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n> \n> > Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n> >\n> > [jc: brandon removed from CC list as the address would bounce]\n> >\n> >> This adds the ability to override gitdir paths via config files\n> >> (not .gitmodules) such that the encoding scheme (or plain text\n> >> name if the encoding extension is disabled) can be changed via\n> >> config entries.\n> >>\n> >> These entries are not added by default for all submodules: they\n> >> should be used on an as-needed basis.\n> >>\n> >> A new test and a helper are added. The helper will also be used\n> >> in further tests exercising gitdir encoding functionality.\n> >\n> > What is the use case of this?  The only reasonable use case I can\n> > see is to set this to all the existing submodules when you are\n> > switching the extension on before adding a new submodule, in which\n> > case the old ones will keep using unencoded names, while the new\n> > ones will use encoded ones.\n> \n> Two things.\n> \n>  * I no longer mind this setting existing, but I think it should not\n>    be a mere \"override\", but the authoritative source of truth for\n>    all submodules (see my other response on 0/5).\n\nI think making this the authoritative source of truth for all submodules\nin the case where the new extension is enabled does make a ton of sense.\nIt makes things way easier for us to reason about:\n\n  - If the extension is set, we know to always use whatever is in the\n    gitconfig.\n\n  - If any submodule path is missing we know that we are in a broken\n    repository and can abort accordingly with directions for how to fix\n    things.\n\n  - If we need to enable the extension we can trivially migrate all\n    existing submodules by just writing their gitdir configuration.\n\n  - We can change the exact encoding going forward, as the extension now\n    only indicates whether or not submodule gitdirs are tracked via the\n    configuration or encoded \"live\".\n\nI think especially the last point is a big win, as we are not stuck with\nthe current encoding schema in case it proves insufficient.\n\nPatrick\n"},{"id":"529259","messageId":"878qh4qxsl.fsf@gentoo.mail-host-address-is-not-set","threadId":"63967","inReplyTo":"aPc-98ps84R45MBF@pks.im","subject":"Re: [PATCH v3 2/5] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-21T11:50:02Z","receivedAt":"2025-10-21T11:50:29Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 21 Oct 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Tue, Oct 07, 2025 at 08:41:13AM -0700, Junio C Hamano wrote: \n>> Junio C Hamano <gitster@pobox.com> writes:  \n>> > Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n>> > \n>> > [jc: brandon removed from CC list as the address would \n>> > bounce] \n>> > \n>> >> This adds the ability to override gitdir paths via config \n>> >> files (not .gitmodules) such that the encoding scheme (or \n>> >> plain text name if the encoding extension is disabled) can \n>> >> be changed via config entries. \n>> >> \n>> >> These entries are not added by default for all submodules: \n>> >> they should be used on an as-needed basis. \n>> >> \n>> >> A new test and a helper are added. The helper will also be \n>> >> used in further tests exercising gitdir encoding \n>> >> functionality. \n>> > \n>> > What is the use case of this?  The only reasonable use case I \n>> > can see is to set this to all the existing submodules when \n>> > you are switching the extension on before adding a new \n>> > submodule, in which case the old ones will keep using \n>> > unencoded names, while the new ones will use encoded ones. \n>>  Two things.  \n>>  * I no longer mind this setting existing, but I think it \n>>  should not \n>>    be a mere \"override\", but the authoritative source of truth \n>>    for all submodules (see my other response on 0/5). \n> \n> I think making this the authoritative source of truth for all \n> submodules in the case where the new extension is enabled does \n> make a ton of sense.  It makes things way easier for us to \n> reason about: \n> \n>   - If the extension is set, we know to always use whatever is \n>   in the \n>     gitconfig. \n> \n>   - If any submodule path is missing we know that we are in a \n>   broken \n>     repository and can abort accordingly with directions for how \n>     to fix things. \n> \n>   - If we need to enable the extension we can trivially migrate \n>   all \n>     existing submodules by just writing their gitdir \n>     configuration. \n> \n>   - We can change the exact encoding going forward, as the \n>   extension now \n>     only indicates whether or not submodule gitdirs are tracked \n>     via the configuration or encoded \"live\". \n> \n> I think especially the last point is a big win, as we are not \n> stuck with the current encoding schema in case it proves \n> insufficient. \n \nAgreed, as I also mentioned in replies to Junio, I will follow \nthis direction in v4. :)\n\nThanks,\nAdrian\n"},{"id":"529260","messageId":"875xc8qxfr.fsf@collabora.com","threadId":"63967","inReplyTo":"aPc-4FHCpHYgQ9hs@pks.im","subject":"Re: [PATCH v3 2/5] submodule: add gitdir path config override","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-21T11:57:44Z","receivedAt":"2025-10-21T11:58:18Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 21 Oct 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Oct 06, 2025 at 02:25:15PM +0300, Adrian Ratiu wrote: \n>> diff --git a/submodule.c b/submodule.c index \n>> 35c55155f7..7a2d7cd592 100644 --- a/submodule.c +++ \n>> b/submodule.c @@ -2604,6 +2604,18 @@ void \n>> submodule_name_to_gitdir(struct strbuf *buf, struct repository \n>> *r, \n>>  \t * administrators can explicitly set. Nothing has been \n>>  decided, * so for now, just append the name at the end of the \n>>  path.  */ \n>> +\tchar *gitdir_path, *key; + +\t/* Allow config \n>> override. */ +\tkey = xstrfmt(\"submodule.%s.gitdirpath\", \n>> submodule_name); +\tif (!repo_config_get_string(r, key, \n>> &gitdir_path)) { +\t\tstrbuf_addstr(buf, gitdir_path); + \n>> free(key); +\t\tfree(gitdir_path); +\t\treturn; + \n>> } +\tfree(key); + \n>>  \trepo_git_path_append(r, buf, \"modules/\"); \n>>  strbuf_addstr(buf, submodule_name); } \n> \n> You can use `repo_config_get_string_tmp()` to avoid having to \n> manage the `gitdir_path` lifetime. \n\nOh, sweet, I wasn't aware of that. Will do, thanks!\n \n>> diff --git a/t/lib-verify-submodule-gitdir-path.sh \n>> b/t/lib-verify-submodule-gitdir-path.sh new file mode 100644 \n>> index 0000000000..3a83f2d975 --- /dev/null +++ \n>> b/t/lib-verify-submodule-gitdir-path.sh @@ -0,0 +1,20 @@ +# \n>> Helper to verify if repo $1 contains a submodule named $2 with \n>> gitdir path $3 + +# This does not check filesystem \n>> existence. That is done in submodule.c via the +# \n>> submodule_name_to_gitdir() API which this helper ends up \n>> calling. The gitdirs +# might or might not exist (e.g. when \n>> adding a new submodule), so this only +# checks the expected \n>> configuration path, which might be overridden by the user.  + \n>> +verify_submodule_gitdir_path() { +\trepo=\"$1\" && +\tname=\"$2\" \n>> && +\tpath=\"$3\" && +\t( +\t\tcd \"$repo\" && + \n>> cat >expect <<-EOF && +\t\t\t$(git rev-parse \n>> --git-common-dir)/$path +\t\tEOF \n> \n> Style nit: we typically don't indent the heredoc body.\n\nAck, will fix in v4. \n \n>> +\t\tgit submodule--helper gitdir \"$name\" >actual && + \n>> test_cmp expect actual +\t) +} diff --git \n>> a/t/t7400-submodule-basic.sh b/t/t7400-submodule-basic.sh index \n>> fd3e7e355e..11c84a7bdf 100755 --- a/t/t7400-submodule-basic.sh \n>> +++ b/t/t7400-submodule-basic.sh @@ -13,6 +13,7 @@ \n>> GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main \n>>  export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME  . ./test-lib.sh \n>> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh \n>>   test_expect_success 'setup - enable local submodules' ' git \n>>  config --global protocol.file.allow always \n>> @@ -1505,4 +1506,12 @@ test_expect_success 'submodule add fails \n>> when name is reused' ' \n>>  \t) '  \n>> +test_expect_success 'submodule helper gitdir config overrides' \n>> ' +\tverify_submodule_gitdir_path test-submodule child \n>> modules/child && +\ttest_config -C test-submodule \n>> submodule.child.gitdirpath \".git/modules/custom-child\" && + \n>> verify_submodule_gitdir_path test-submodule child \n>> modules/custom-child && +\ttest_unconfig -C test-submodule \n>> submodule.child.gitdirpath && + \n>> verify_submodule_gitdir_path test-submodule child modules/child \n>> +' + \n>>  test_done \n> \n> I feel like it's a bit curious that we recognize the \n> configuration even though \"extensions.submodulePath\" hasn't been \n> introduced yet. I would expect that we ignore the config key if \n> that extension is not set, as the extension otherwise seems to \n> not be doing its job, does it? \n\nGood point.\n\nIn v3 the config wasn't as tied to the extension because it was \njust an optional override, however since we'll make it the \ncenterpiece of the design, this 100% makes sense.\n\nI will reorder the commits & rework this logic in v4 as you and \nJunio suggested.\n\nMany thanks,\nAdrian\n"},{"id":"529267","messageId":"871pmwqtxb.fsf@collabora.com","threadId":"63967","inReplyTo":"aPc-79_XLyTjA_w0@pks.im","subject":"Re: [PATCH v3 5/5] submodule: error out if gitdir name is too long","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-10-21T13:13:36Z","receivedAt":"2025-10-21T13:14:03Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 21 Oct 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Oct 06, 2025 at 02:25:18PM +0300, Adrian Ratiu wrote: \n>> diff --git a/compat/posix.h b/compat/posix.h index \n>> 067a00f33b..aa050fd58c 100644 --- a/compat/posix.h +++ \n>> b/compat/posix.h @@ -250,6 +250,14 @@ char *gitdirname(char *); \n>>  #define NAME_MAX 255 #endif  \n>> +#ifdef NO_PATHCONF +#ifndef _PC_NAME_MAX +#define _PC_NAME_MAX \n>> 1 /* dummy value, only used for git_pathconf */ \n> \n> Are there platforms that have pathconf(3) but not _PC_NAME_MAX? \n\nAFAIK no, because they're both part of POSIX and in all known \nimplementations they are both defined.\n\n> \n>> +#endif +#define pathconf(a,b) git_pathconf(a,b) +long \n>> git_pathconf(const char *path, int name); +#endif + \n>>  typedef uintmax_t timestamp_t; #define PRItime PRIuMAX #define \n>>  parse_timestamp strtoumax \n> \n> Let's adapt this to our coding guidelines to make this easier to \n> parse: \n> \n>  - Nested C preprocessor directives are indented after the hash \n>  by one \n>    space per nesting level. \n> \n> \t#if FOO # include <foo.h> # if BAR #  include <bar.h> # \n> endif #endif \n\nThanks, however I'm inclining towards dropping this commit in v4 \nas Junio and Peff suggested, since it doesn't add anything other \nthan the nice error message, so we can let each OS fail with its \nown message like they did before this patch. \n\nWe could bring it back when/if we decide to implement sharding /\ntrimming, i.e. to address the TODO in the patch, that way it's more\nuseful.\n"},{"id":"530371","messageId":"20251107150547.3272180-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH v4 0/4] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-07T15:05:43Z","receivedAt":"2025-11-07T15:06:36Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello everyone,\n\nFor those new to this series, we are adding an extension to encode submodule\ngitdir paths to avoid filesystem conflicts.\n\nv4 continues with the simplifications started in v3, based on reviewer feedback\n(many thanks to everyone, especially to Junio and Patrick).\n\nThe biggest design change in v4 is that config submodule.<name>.gitdir becomes\nmandatory when extensions.submoduleEncoding is enabled, so instead of being just\nan optional override like in previous iterations, in v4 it's the centerpiece of\ndesign, the single point of truth from which submodule gitdir paths are located.\n\nThis allows users to not care about the specific encoding being used and also\nallows git to improveme the encoding implementation over time: everyone just gets\nthe submodule gitdirs from the config after the extension is enabled.\n\nAnother important change is that we do not encode everything by default when the\nextension is enabled: submodule_name_to_gitdir() validates multiple candidates\nand picks the best one and encoding only when required.\n\nAs always this is based on latest master branch, I've merged into next/seen for\nany conflicts, pushed to GitHub [1] and ran the CI with all tests passing [2].\n\nI also added a rangediff between v3 and v4, which might help some reviewers,\nthough it's rather big because we changed the config/encoding design like I\nmentioned above so it might be easier to give it a full review.\n\nChanges between v3 -> v4:\n* Replaced bmwill@google.com -> bwilliams.eng@gmail.com (Kristoffer)\n* Made the gitdir config the authoritative source of truth for all submodule\n  git dirs when the extension is enabled (Junio, Patrick)\n* Replaced the \"encode everything by default\" design with a stepwise/retry\n  validation in submodule_name_to_gitdir() to pick the best canditate (Junio)\n* Moved is_rfc3986_unreserved() to url.[ch] instead of strbuf (Junio)\n* Fixed a parallel job execution bug I introduced with the extension in v3 (Adrian)\n* Improved lib-verify-submodule-gitdir-path.sh to handle relative/abs paths (Adrian)\n* Fixed submodule.<name>.gitdir documentation vs code mismatch (Junio)\n* Defined the config together with the extension by squashing commits (Patrick)\n* Removed the A -> _a, B -> _b custom encoding in favor of a simplified percent\n  encodin which is only done when case-folding to allow uppercase chars (Junio)\n* Dropped patch with pathconf wrapper, as it's not required for basic encoding,\n  it can be added back if we implement sharding (Junio, Peff, Patrick)\n* Used repo_config_get_string_tmp to avoid a char* free call (Patrick)\n* Added comment to document is_rfc3986_unreserved() (Patrick)\n* Fixed heredoc body indentation (Patrick)\n* Fixed trivial doc markup conflict with upstream commit (Adrian)\n* Reworded multiple commits for clarity (Junio)\n\nPlease let me know if you have any questions or other feedback.\n\nThank you,\nAdrian\n\n[1] https://github.com/10ne1/git/tree/dev/aratiu/encoding-v4\n[2] https://github.com/10ne1/git/actions/runs/19170390360\n\nRange-diff against v3:\n1:  b2e317a8f6 ! 1:  7d34507692 submodule--helper: use submodule_name_to_gitdir in add_submodule\n    @@ Commit message\n         submodule--helper: use submodule_name_to_gitdir in add_submodule\n     \n         While testing submodule gitdir path encoding, I noticed submodule--helper\n    -    is still using a hardcoded name-based path leading to test failures, so\n    -    convert it to the common helper function introduced by commit ce125d431a\n    -    (submodule: extract path to submodule gitdir func, 2021-09-15)  and used\n    -    in other locations across the source tree.\n    +    is still using a hardcoded modules gitdir path leading to test failures.\n    +\n    +    Call the submodule_name_to_gitdir() helper instead, which was invented\n    +    exactly for this purpose and is already used by all the other locations\n    +    which work on gitdirs.\n    +\n    +    Also narrow the scope of the submod_gitdir_path variable which is not\n    +    used anymore in the updated \"else\" branch.\n     \n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n2:  17838ad13f < -:  ---------- submodule: add gitdir path config override\n3:  4fc7020f26 < -:  ---------- strbuf: bring back is_rfc3986_unreserved\n-:  ---------- > 2:  1e609bdd1a builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n4:  dc6d5069ff ! 3:  c51fd58669 submodule: encode gitdir paths to avoid conflicts\n    @@ Metadata\n     Author: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n      ## Commit message ##\n    -    submodule: encode gitdir paths to avoid conflicts\n    +    submodule: add extension to encode gitdir paths\n     \n    -    This adds a new submoduleEncoding extension which encodes gitdir names\n    -    to avoid collisions due to nested gitdirs or case insensitive filesystems.\n    +    Add a submoduleEncoding extension which fixes filesystem collisions by\n    +    encoding gitdir paths. At a high level, this implements a mechanism to\n    +    encode -> validate -> retry until a working gitdir path is found.\n     \n    -    A custom encoding can become unnecessarily complex, while url-encoding is\n    -    relatively well-known, however it needs some extending to support case\n    -    insensitive filesystems, hence why A is encoded as _a, B as _b and so on.\n    +    Credit goes to Junio for coming up with this design: encoding is only\n    +    applied when necessary, e.g. uppercase characters are encoded only on\n    +    case-folding filesystems and only if a real conflict is detected.\n     \n    -    Unfortunately encoding A -> _a (...) is not enough to fix the reserved\n    -    Windows file names (e.g. COM1) because worktrees still use names like COM1\n    -    even if the gitdirs paths are encoded, so future work is needed to fully\n    -    address Windows reserved names.\n    +    To make this work, we rely on the submodule.<name>.gitdir config as the\n    +    single source of truth for gitidir paths: the config is always set when\n    +    the extension is enabled. Users who care about gitdir paths are expected\n    +    to get/set the config and not the underlying encoding implementation.\n     \n    -    For now url-encoding is the only option, however in the future we may\n    -    add alternatives (other encodings, hashes or even hash_name).\n    +    This commit adds the basic encoding logic which addresses nested gitdirs.\n    +    The next commit fixes case-folding, the next commit fixes names longer\n    +    than NAME_MAX. The idea is the encoding can be improved over time in a\n    +    way which is transparent to users.\n     \n    +    Suggested-by: Junio C Hamano <gitster@pobox.com>\n         Suggested-by: Phillip Wood <phillip.wood123@gmail.com>\n         Suggested-by: Patrick Steinhardt <ps@pks.im>\n    +    Based-on-patch-by: Brandon Williams <bwilliams.eng@gmail.com>\n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n      ## Documentation/config/extensions.adoc ##\n    -@@ Documentation/config/extensions.adoc: relativeWorktrees::\n    +@@ Documentation/config/extensions.adoc: relativeWorktrees:::\n      \trepaired with either the `--relative-paths` option or with the\n      \t`worktree.useRelativePaths` config set to `true`.\n      \n    -+submoduleEncoding::\n    ++submoduleEncoding:::\n     +\tIf enabled, submodule gitdir paths are encoded to avoid filesystem\n    -+\tconflicts due to nested gitdirs or case insensitivity. For now, only\n    -+\turl-encoding (rfc3986) is available, with a small addition to encode\n    -+\tuppercase to lowercase letters (`A  -> _a`, `B -> _b` and so on).\n    -+\tOther encoding or hashing methods may be added in the future.\n    -+\tAny preexisting non-encoded submodule gitdirs are used as-is, to\n    -+\tease migration and reduce risk of gitdirs not being recognized.\n    -+\n    - worktreeConfig::\n    ++\tconflicts due to nested gitdirs, case insensitivity or other issues\n    ++\tWhen enabled, the submodule.<name>.gitdir config is always set for\n    ++\tall submodulesand is the single point of authority for gitdir paths.\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\n     \n      ## Documentation/config/submodule.adoc ##\n     @@ Documentation/config/submodule.adoc: submodule.<name>.active::\n    - submodule.<name>.gitdir::\n    - \tThis option sets the gitdir path for submodule <name>, allowing users\n    - \tto override the default path or change the default path name encoding.\n    -+\tSubmodule gitdir encoding is enabled via `extensions.submoduleEncoding`\n    -+\t(see linkgit:git-config[1]). This config works both with the extension\n    -+\tenabled or disabled.\n    + \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n    + \tdetails.\n      \n    ++submodule.<name>.gitdir::\n    ++\tThis option sets the gitdir path for submodule <name>, allowing users to\n    ++\toverride the default path. Only works when `extensions.submoduleEncoding`\n    ++\tis enabled, otherwise does nothing. See linkgit:git-config[1] for details.\n    ++\n      submodule.active::\n      \tA repeated field which contains a pathspec used to match against a\n    + \tsubmodule's path to determine if the submodule is of interest to git\n    +\n    + ## builtin/submodule--helper.c ##\n    +@@ builtin/submodule--helper.c: static int module_summary(int argc, const char **argv, const char *prefix,\n    + \treturn ret;\n    + }\n    + \n    ++static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n    ++\t\t\t struct repository *repo)\n    ++{\n    ++\tstruct strbuf gitdir = STRBUF_INIT;\n    ++\n    ++\tif (argc != 2)\n    ++\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n    ++\n    ++\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n    ++\n    ++\tprintf(\"%s\\n\", gitdir.buf);\n    ++\n    ++\tstrbuf_release(&gitdir);\n    ++\treturn 0;\n    ++}\n    ++\n    + struct sync_cb {\n    + \tconst char *prefix;\n    + \tconst char *super_prefix;\n    +@@ builtin/submodule--helper.c: int cmd_submodule__helper(int argc,\n    + \t\tNULL\n    + \t};\n    + \tstruct option options[] = {\n    ++\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n    + \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n    + \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n    + \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\n    +\n    + ## repository.c ##\n    +@@ repository.c: int repo_init(struct repository *repo,\n    + \trepo->repository_format_worktree_config = format.worktree_config;\n    + \trepo->repository_format_relative_worktrees = format.relative_worktrees;\n    + \trepo->repository_format_precious_objects = format.precious_objects;\n    ++\trepo->repository_format_submodule_encoding = format.submodule_encoding;\n    + \n    + \t/* take ownership of format.partial_clone */\n    + \trepo->repository_format_partial_clone = format.partial_clone;\n     \n      ## repository.h ##\n     @@ repository.h: struct repository {\n    @@ setup.h: struct repository_format {\n      \tint compat_hash_algo;\n     \n      ## submodule.c ##\n    -@@ submodule.c: int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    - \tchar *p;\n    +@@\n    + #include \"commit-reach.h\"\n    + #include \"read-cache-ll.h\"\n    + #include \"setup.h\"\n    ++#include \"url.h\"\n    + \n    + static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n    + static int initialized_fetch_ref_tips;\n    +@@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n    + \treturn ret;\n    + }\n    + \n    ++/*\n    ++ * Find the last submodule name in the gitdir path (modules can be nested).\n    ++ * Returns a pointer into `path` to the beginning of the name or NULL if not found.\n    ++ */\n    ++static char *find_last_submodule_name(char *git_dir_path)\n    ++{\n    ++\tconst char *modules_marker = \"/modules/\";\n    ++\tchar *p = git_dir_path;\n    ++\tchar *last = NULL;\n    ++\n    ++\twhile ((p = strstr(p, modules_marker))) {\n    ++\t\tlast = p + strlen(modules_marker);\n    ++\t\tp++;\n    ++\t}\n    ++\n    ++\treturn last;\n    ++}\n    ++\n    + int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    + {\n    + \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n    +-\tchar *p;\n    ++\tchar *p = git_dir + len - suffix_len;\n    ++\tbool suffixes_match = !strcmp(p, submodule_name);\n      \tint ret = 0;\n      \n    -+\t/*\n    -+\t * Skip these checks when extensions.submoduleEncoding is enabled because\n    -+\t * it fixes the nesting issues and the suffixes will not match by design.\n    -+\t */\n    -+\tif (the_repository->repository_format_submodule_encoding)\n    +-\tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n    +-\t    strcmp(p, submodule_name))\n    +-\t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n    +-\t\t    submodule_name, git_dir);\n    +-\n    + \t/*\n    + \t * We prevent the contents of sibling submodules' git directories to\n    + \t * clash.\n    +@@ submodule.c: int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    + \t * but the latter directory is already designated to contain the hooks\n    + \t * of the former.\n    + \t */\n    +-\tfor (; *p; p++) {\n    ++\tfor (; *p && suffixes_match; p++) {\n    + \t\tif (is_dir_sep(*p)) {\n    + \t\t\tchar c = *p;\n    + \n    +@@ submodule.c: int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    + \t\t}\n    + \t}\n    + \n    ++\t/* tests after this check are only for encoded names, when the extension is enabled */\n    ++\tif (!the_repository->repository_format_submodule_encoding)\n     +\t\treturn 0;\n     +\n    - \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n    - \t    strcmp(p, submodule_name))\n    - \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n    ++\t/* Prevent the use of '/' in names */\n    ++\tp = find_last_submodule_name(git_dir);\n    ++\tif (p && strchr(p, '/') != NULL)\n    ++\t\treturn error(\"submodule gitdir name '%s' contains unexpected '/'\", p);\n    ++\n    + \treturn 0;\n    + }\n    + \n     @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n      \treturn ret;\n      }\n      \n    -+static void strbuf_addstr_case_encode(struct strbuf *dst, const char *src)\n    ++static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n    ++\t\t\t\t\t     const char *submodule_name)\n     +{\n    -+\tfor (; *src; src++) {\n    -+\t\tunsigned char c = *src;\n    -+\t\tif (c >= 'A' && c <= 'Z') {\n    -+\t\t\tstrbuf_addch(dst, '_');\n    -+\t\t\tstrbuf_addch(dst, c - 'A' + 'a');\n    -+\t\t} else {\n    -+\t\t\tstrbuf_addch(dst, c);\n    -+\t\t}\n    -+\t}\n    ++\tchar *key;\n    ++\n    ++\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n    ++\t\treturn -1;\n    ++\n    ++\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n    ++\trepo_config_set_gently(the_repository, key, gitdir_path->buf);\n    ++\tFREE_AND_NULL(key);\n    ++\n    ++\treturn 0;\n    ++\n     +}\n     +\n      void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n      \t\t\t      const char *submodule_name)\n      {\n    --\t/*\n    ++\tconst char *gitdir;\n    ++\tchar *key;\n    ++\n    ++\trepo_git_path_append(r, buf, \"modules/\");\n    ++\tstrbuf_addstr(buf, submodule_name);\n    ++\n    ++\t/* If extensions.submoduleEncoding is disabled, use the plain path set above */\n    ++\tif (!r->repository_format_submodule_encoding)\n    ++\t\treturn;\n    ++\n    ++\t/* Extension is enabled: use the gitdir config if it exists */\n    ++\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n    ++\tif (!repo_config_get_string_tmp(r, key, &gitdir)) {\n    ++\t\tstrbuf_reset(buf);\n    ++\t\tstrbuf_addstr(buf, gitdir);\n    ++\t\tFREE_AND_NULL(key);\n    ++\t\treturn;\n    ++\t}\n    ++\tFREE_AND_NULL(key);\n    ++\n    + \t/*\n     -\t * NEEDSWORK: The current way of mapping a submodule's name to\n     -\t * its location in .git/modules/ has problems with some naming\n     -\t * schemes. For example, if a submodule is named \"foo\" and\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     -\t * submodule.<name>.gitdir config in .gitmodules that repo\n     -\t * administrators can explicitly set. Nothing has been decided,\n     -\t * so for now, just append the name at the end of the path.\n    --\t */\n    - \tchar *gitdir_path, *key;\n    - \n    - \t/* Allow config override. */\n    -@@ submodule.c: void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n    - \n    - \trepo_git_path_append(r, buf, \"modules/\");\n    - \tstrbuf_addstr(buf, submodule_name);\n    ++\t * The gitdir config does not exist, even though the extension is enabled.\n    ++\t * Therefore we are in one of the following cases:\n    + \t */\n     +\n    -+\t/* Existing legacy non-encoded names are used as-is */\n    -+\tif (is_git_directory(buf->buf))\n    ++\t/* Case 1: legacy migration of valid plain submodule names */\n    ++\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n     +\t\treturn;\n     +\n    -+\tif (the_repository->repository_format_submodule_encoding) {\n    -+\t\tstruct strbuf tmp = STRBUF_INIT;\n    ++\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n    ++\tstrbuf_reset(buf);\n    + \trepo_git_path_append(r, buf, \"modules/\");\n    +-\tstrbuf_addstr(buf, submodule_name);\n    ++\tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n    ++\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n    ++\t\treturn;\n     +\n    -+\t\tstrbuf_reset(buf);\n    -+\t\trepo_git_path_append(r, buf, \"modules/\");\n    ++\t/* Case 3: error out */\n    ++\tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n    ++\t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\n    ++\t    submodule_name, submodule_name);\n    + }\n    +\n    + ## t/lib-verify-submodule-gitdir-path.sh (new) ##\n    +@@\n    ++# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n     +\n    -+\t\tstrbuf_addstr_urlencode(&tmp, submodule_name, is_rfc3986_unreserved);\n    -+\t\tstrbuf_addstr_case_encode(buf, tmp.buf);\n    ++# This does not check filesystem existence. That is done in submodule.c via the\n    ++# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n    ++# might or might not exist (e.g. when adding a new submodule), so this only\n    ++# checks the expected configuration path, which might be overridden by the user.\n     +\n    -+\t\tstrbuf_release(&tmp);\n    -+\t}\n    - }\n    ++verify_submodule_gitdir_path() {\n    ++\trepo=\"$1\" &&\n    ++\tname=\"$2\" &&\n    ++\tpath=\"$3\" &&\n    ++\t(\n    ++\t\tcd \"$repo\" &&\n    ++\t\t# Compute expected absolute path\n    ++\t\texpected=\"$(git rev-parse --git-common-dir)/$path\" &&\n    ++\t\texpected=\"$(test-tool path-utils real_path \"$expected\")\" &&\n    ++\t\t# Compute actual absolute path\n    ++\t\tactual=\"$(git submodule--helper gitdir \"$name\")\" &&\n    ++\t\tactual=\"$(test-tool path-utils real_path \"$actual\")\" &&\n    ++\t\techo \"$expected\" >expect &&\n    ++\t\techo \"$actual\" >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t)\n    ++}\n     \n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n    @@ t/t7425-submodule-encoding.sh (new)\n     +\n     +test_expect_success 'verify submodule name is properly encoded' '\n     +\tverify_submodule_gitdir_path main legacy modules/legacy &&\n    -+\tverify_submodule_gitdir_path main \"New Sub\" modules/_new%20_sub\n    ++\tverify_submodule_gitdir_path main \"New Sub\" \"modules/New Sub\"\n     +'\n     +\n     +test_expect_success 'clone from repo with both legacy and new-style submodules' '\n    @@ t/t7425-submodule-encoding.sh (new)\n     +\t\tcd cloned-encoding &&\n     +\n     +\t\ttest_path_is_dir .git/modules/legacy &&\n    -+\t\ttest_path_is_dir .git/modules/_new%20_sub &&\n    ++\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n     +\n     +\t\tgit submodule status >list &&\n     +\t\ttest_grep \"$legacy_rev legacy\" list &&\n    @@ t/t7425-submodule-encoding.sh (new)\n     +\t\tgit submodule update --init --recursive &&\n     +\n     +\t\ttest_path_is_dir .git/modules/legacy &&\n    -+\t\ttest_path_is_dir .git/modules/_new%20_sub &&\n    ++\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n     +\n     +\t\t# Verify both submodules are at the expected commits\n     +\t\tgit submodule status >list &&\n    @@ t/t7425-submodule-encoding.sh (new)\n     +'\n     +\n     +test_done\n    +\n    + ## t/t9902-completion.sh ##\n    +@@ t/t9902-completion.sh: test_expect_success 'git config set - variable name - __git_compute_second_level\n    + \tsubmodule.sub.fetchRecurseSubmodules Z\n    + \tsubmodule.sub.ignore Z\n    + \tsubmodule.sub.active Z\n    ++\tsubmodule.sub.gitdir Z\n    + \tEOF\n    + '\n    + \n5:  775cf131bf < -:  ---------- submodule: error out if gitdir name is too long\n-:  ---------- > 4:  01a5b10d5a submodule: fix case-folding gitdir filesystem colisions\n\nAdrian Ratiu (4):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n  submodule: add extension to encode gitdir paths\n  submodule: fix case-folding gitdir filesystem colisions\n\n Documentation/config/extensions.adoc  |   6 +\n Documentation/config/submodule.adoc   |   5 +\n builtin/credential-store.c            |   7 +-\n builtin/submodule--helper.c           |  30 ++++-\n repository.c                          |   1 +\n repository.h                          |   1 +\n setup.c                               |   7 ++\n setup.h                               |   1 +\n submodule.c                           | 155 ++++++++++++++++++++-----\n t/lib-verify-submodule-gitdir-path.sh |  24 ++++\n t/meson.build                         |   1 +\n t/t7425-submodule-encoding.sh         | 161 ++++++++++++++++++++++++++\n t/t9902-completion.sh                 |   1 +\n url.c                                 |  23 ++++\n url.h                                 |   3 +\n 15 files changed, 387 insertions(+), 39 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-encoding.sh\n\n-- \n2.51.0\n\n"},{"id":"530372","messageId":"20251107150547.3272180-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251107150547.3272180-1-adrian.ratiu@collabora.com","subject":"[PATCH v4 3/4] submodule: add extension to encode gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-07T15:05:46Z","receivedAt":"2025-11-07T15:06:47Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a submoduleEncoding extension which fixes filesystem collisions by\nencoding gitdir paths. At a high level, this implements a mechanism to\nencode -> validate -> retry until a working gitdir path is found.\n\nCredit goes to Junio for coming up with this design: encoding is only\napplied when necessary, e.g. uppercase characters are encoded only on\ncase-folding filesystems and only if a real conflict is detected.\n\nTo make this work, we rely on the submodule.<name>.gitdir config as the\nsingle source of truth for gitidir paths: the config is always set when\nthe extension is enabled. Users who care about gitdir paths are expected\nto get/set the config and not the underlying encoding implementation.\n\nThis commit adds the basic encoding logic which addresses nested gitdirs.\nThe next commit fixes case-folding, the next commit fixes names longer\nthan NAME_MAX. The idea is the encoding can be improved over time in a\nway which is transparent to users.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nBased-on-patch-by: Brandon Williams <bwilliams.eng@gmail.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc  |   6 ++\n Documentation/config/submodule.adoc   |   5 +\n builtin/submodule--helper.c           |  17 +++\n repository.c                          |   1 +\n repository.h                          |   1 +\n setup.c                               |   7 ++\n setup.h                               |   1 +\n submodule.c                           | 110 ++++++++++++++-----\n t/lib-verify-submodule-gitdir-path.sh |  24 +++++\n t/meson.build                         |   1 +\n t/t7425-submodule-encoding.sh         | 146 ++++++++++++++++++++++++++\n t/t9902-completion.sh                 |   1 +\n 12 files changed, 294 insertions(+), 26 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-encoding.sh\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 532456644b..e33040fff5 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -73,6 +73,12 @@ relativeWorktrees:::\n \trepaired with either the `--relative-paths` option or with the\n \t`worktree.useRelativePaths` config set to `true`.\n \n+submoduleEncoding:::\n+\tIf enabled, submodule gitdir paths are encoded to avoid filesystem\n+\tconflicts due to nested gitdirs, case insensitivity or other issues\n+\tWhen enabled, the submodule.<name>.gitdir config is always set for\n+\tall submodulesand is the single point of authority for gitdir paths.\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/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 0672d99117..ddaadc3dc5 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -52,6 +52,11 @@ submodule.<name>.active::\n \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n \tdetails.\n \n+submodule.<name>.gitdir::\n+\tThis option sets the gitdir path for submodule <name>, allowing users to\n+\toverride the default path. Only works when `extensions.submoduleEncoding`\n+\tis enabled, otherwise does nothing. See linkgit:git-config[1] for details.\n+\n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\n \tsubmodule's path to determine if the submodule is of interest to git\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 2873b2780e..abd20eee53 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1208,6 +1208,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3591,6 +3607,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\ndiff --git a/repository.c b/repository.c\nindex 6faf5c7398..26a21c0d71 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -288,6 +288,7 @@ int repo_init(struct repository *repo,\n \trepo->repository_format_worktree_config = format.worktree_config;\n \trepo->repository_format_relative_worktrees = format.relative_worktrees;\n \trepo->repository_format_precious_objects = format.precious_objects;\n+\trepo->repository_format_submodule_encoding = format.submodule_encoding;\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 5808a5d610..7e39b2acf7 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -158,6 +158,7 @@ struct repository {\n \tint repository_format_worktree_config;\n \tint repository_format_relative_worktrees;\n \tint repository_format_precious_objects;\n+\tint repository_format_submodule_encoding;\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 7086741e6c..bf6e815105 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -687,6 +687,9 @@ static enum extension_result handle_extension(const char *var,\n \t} else if (!strcmp(ext, \"relativeworktrees\")) {\n \t\tdata->relative_worktrees = git_config_bool(var, value);\n \t\treturn EXTENSION_OK;\n+\t} else if (!strcmp(ext, \"submoduleencoding\")) {\n+\t\tdata->submodule_encoding = git_config_bool(var, value);\n+\t\treturn EXTENSION_OK;\n \t}\n \treturn EXTENSION_UNKNOWN;\n }\n@@ -1865,6 +1868,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\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\tthe_repository->repository_format_submodule_encoding =\n+\t\t\t\trepo_fmt.submodule_encoding;\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@@ -1963,6 +1968,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_submodule_encoding =\n+\t\tfmt->submodule_encoding;\n \tthe_repository->repository_format_relative_worktrees =\n \t\tfmt->relative_worktrees;\n \tthe_repository->repository_format_partial_clone =\ndiff --git a/setup.h b/setup.h\nindex 8522fa8575..66ec1ceba5 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -130,6 +130,7 @@ struct repository_format {\n \tchar *partial_clone; /* value of extensions.partialclone */\n \tint worktree_config;\n \tint relative_worktrees;\n+\tint submodule_encoding;\n \tint is_bare;\n \tint hash_algo;\n \tint compat_hash_algo;\ndiff --git a/submodule.c b/submodule.c\nindex 35c55155f7..ceaff0c1aa 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -31,6 +31,7 @@\n #include \"commit-reach.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"url.h\"\n \n static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n static int initialized_fetch_ref_tips;\n@@ -2256,17 +2257,31 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n+/*\n+ * Find the last submodule name in the gitdir path (modules can be nested).\n+ * Returns a pointer into `path` to the beginning of the name or NULL if not found.\n+ */\n+static char *find_last_submodule_name(char *git_dir_path)\n+{\n+\tconst char *modules_marker = \"/modules/\";\n+\tchar *p = git_dir_path;\n+\tchar *last = NULL;\n+\n+\twhile ((p = strstr(p, modules_marker))) {\n+\t\tlast = p + strlen(modules_marker);\n+\t\tp++;\n+\t}\n+\n+\treturn last;\n+}\n+\n int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n {\n \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n-\tchar *p;\n+\tchar *p = git_dir + len - suffix_len;\n+\tbool suffixes_match = !strcmp(p, submodule_name);\n \tint ret = 0;\n \n-\tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n-\t    strcmp(p, submodule_name))\n-\t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n-\t\t    submodule_name, git_dir);\n-\n \t/*\n \t * We prevent the contents of sibling submodules' git directories to\n \t * clash.\n@@ -2277,7 +2292,7 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \t * but the latter directory is already designated to contain the hooks\n \t * of the former.\n \t */\n-\tfor (; *p; p++) {\n+\tfor (; *p && suffixes_match; p++) {\n \t\tif (is_dir_sep(*p)) {\n \t\t\tchar c = *p;\n \n@@ -2294,6 +2309,15 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \t\t}\n \t}\n \n+\t/* tests after this check are only for encoded names, when the extension is enabled */\n+\tif (!the_repository->repository_format_submodule_encoding)\n+\t\treturn 0;\n+\n+\t/* Prevent the use of '/' in names */\n+\tp = find_last_submodule_name(git_dir);\n+\tif (p && strchr(p, '/') != NULL)\n+\t\treturn error(\"submodule gitdir name '%s' contains unexpected '/'\", p);\n+\n \treturn 0;\n }\n \n@@ -2581,29 +2605,63 @@ int submodule_to_gitdir(struct repository *repo,\n \treturn ret;\n }\n \n+static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n+\t\t\t\t\t     const char *submodule_name)\n+{\n+\tchar *key;\n+\n+\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n+\t\treturn -1;\n+\n+\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\trepo_config_set_gently(the_repository, key, gitdir_path->buf);\n+\tFREE_AND_NULL(key);\n+\n+\treturn 0;\n+\n+}\n+\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n+\tconst char *gitdir;\n+\tchar *key;\n+\n+\trepo_git_path_append(r, buf, \"modules/\");\n+\tstrbuf_addstr(buf, submodule_name);\n+\n+\t/* If extensions.submoduleEncoding is disabled, use the plain path set above */\n+\tif (!r->repository_format_submodule_encoding)\n+\t\treturn;\n+\n+\t/* Extension is enabled: use the gitdir config if it exists */\n+\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\tif (!repo_config_get_string_tmp(r, key, &gitdir)) {\n+\t\tstrbuf_reset(buf);\n+\t\tstrbuf_addstr(buf, gitdir);\n+\t\tFREE_AND_NULL(key);\n+\t\treturn;\n+\t}\n+\tFREE_AND_NULL(key);\n+\n \t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n+\t * The gitdir config does not exist, even though the extension is enabled.\n+\t * Therefore we are in one of the following cases:\n \t */\n+\n+\t/* Case 1: legacy migration of valid plain submodule names */\n+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\treturn;\n+\n+\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\tstrbuf_reset(buf);\n \trepo_git_path_append(r, buf, \"modules/\");\n-\tstrbuf_addstr(buf, submodule_name);\n+\tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\treturn;\n+\n+\t/* Case 3: error out */\n+\tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n+\t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\n+\t    submodule_name, submodule_name);\n }\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..62794df976\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,24 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n+\n+# This does not check filesystem existence. That is done in submodule.c via the\n+# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n+# might or might not exist (e.g. when adding a new submodule), so this only\n+# checks the expected configuration path, which might be overridden by the user.\n+\n+verify_submodule_gitdir_path() {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\t# Compute expected absolute path\n+\t\texpected=\"$(git rev-parse --git-common-dir)/$path\" &&\n+\t\texpected=\"$(test-tool path-utils real_path \"$expected\")\" &&\n+\t\t# Compute actual absolute path\n+\t\tactual=\"$(git submodule--helper gitdir \"$name\")\" &&\n+\t\tactual=\"$(test-tool path-utils real_path \"$actual\")\" &&\n+\t\techo \"$expected\" >expect &&\n+\t\techo \"$actual\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/meson.build b/t/meson.build\nindex a5531df415..4187b35aee 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -884,6 +884,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-encoding.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-encoding.sh b/t/t7425-submodule-encoding.sh\nnew file mode 100755\nindex 0000000000..a42d358f5b\n--- /dev/null\n+++ b/t/t7425-submodule-encoding.sh\n@@ -0,0 +1,146 @@\n+#!/bin/sh\n+\n+test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+\tgit config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed encoded and non-encoded submodules' '\n+\tgit init -b main legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init -b main new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init -b main main &&\n+\t(\n+\t\tcd main &&\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submoduleEncoding true &&\n+\n+\t\tgit submodule add ../new-sub \"New Sub\" &&\n+\t\ttest_commit new\n+\t)\n+'\n+\n+test_expect_success 'verify submodule name is properly encoded' '\n+\tverify_submodule_gitdir_path main legacy modules/legacy &&\n+\tverify_submodule_gitdir_path main \"New Sub\" \"modules/New Sub\"\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned-non-encoding &&\n+\t(\n+\t\tcd cloned-non-encoding &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/\"New Sub\" &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t) &&\n+\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules main cloned-encoding &&\n+\t(\n+\t\tcd cloned-encoding &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to encoded submodules' '\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C main config receive.denyCurrentBranch updateInstead &&\n+\t(\n+\t\tcd cloned-encoding &&\n+\n+\t\tgit -C legacy switch --track -C main origin/main  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C \"New Sub\" switch --track -C main origin/main &&\n+\t\ttest_commit -C \"New Sub\" second-commit &&\n+\t\tgit -C \"New Sub\" push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C main origin/main  &&\n+\t\tgit add legacy \"New Sub\" &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of encoded sibling submodules must not be nested' '\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules nested clone_nested &&\n+\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules --jobs=2 nested clone_parallel &&\n+\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n+\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks\n+'\n+\n+test_done\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 964e1f1569..ffb9c8b522 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n \tsubmodule.sub.fetchRecurseSubmodules Z\n \tsubmodule.sub.ignore Z\n \tsubmodule.sub.active Z\n+\tsubmodule.sub.gitdir Z\n \tEOF\n '\n \n-- \n2.51.0\n\n"},{"id":"530373","messageId":"20251107150547.3272180-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251107150547.3272180-1-adrian.ratiu@collabora.com","subject":"[PATCH v4 2/4] builtin/credential-store: move is_rfc3986_unreserved to url.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-07T15:05:45Z","receivedAt":"2025-11-07T15:06:48Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"is_rfc3986_unreserved() was moved to credential-store.c and was made\nstatic by f89854362c (credential-store: move related functions to\ncredential-store file, 2023-06-06) under a correct assumption, at the\ntime, that it was the only place using it.\n\nHowever now we need it to apply URL-encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back as a\npublic function exposed via url.h, instead of the old helper path\n(strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n\nThis function will be used by submodule.c in the next commit.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c |  7 +------\n url.c                      | 11 +++++++++++\n url.h                      |  2 ++\n 3 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..bc1453c6b2 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -7,6 +7,7 @@\n #include \"path.h\"\n #include \"string-list.h\"\n #include \"parse-options.h\"\n+#include \"url.h\"\n #include \"write-or-die.h\"\n \n static struct lock_file credential_lock;\n@@ -76,12 +77,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/url.c b/url.c\nindex 282b12495a..0fb1859b28 100644\n--- a/url.c\n+++ b/url.c\n@@ -3,6 +3,17 @@\n #include \"strbuf.h\"\n #include \"url.h\"\n \n+/*\n+ * The set of unreserved characters as per STD66 (RFC3986) is\n+ * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n+ * components without percent-encoding.\n+ */\n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex 2a27c34277..131a262066 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,4 +21,6 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+int is_rfc3986_unreserved(char ch);\n+\n #endif /* URL_H */\n-- \n2.51.0\n\n"},{"id":"530374","messageId":"20251107150547.3272180-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251107150547.3272180-1-adrian.ratiu@collabora.com","subject":"[PATCH v4 1/4] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-07T15:05:44Z","receivedAt":"2025-11-07T15:06:50Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded modules gitdir path leading to test failures.\n\nCall the submodule_name_to_gitdir() helper instead, which was invented\nexactly for this purpose and is already used by all the other locations\nwhich work on gitdirs.\n\nAlso narrow the scope of the submod_gitdir_path variable which is not\nused anymore in the updated \"else\" branch.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 13 +++++++------\n 1 file changed, 7 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex fcd73abe53..2873b2780e 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3187,13 +3187,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n \n static int add_submodule(const struct add_data *add_data)\n {\n-\tchar *submod_gitdir_path;\n \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n \tint ret = -1;\n \n \t/* perhaps the path already exists and is already a git repo, else clone it */\n \tif (is_directory(add_data->sm_path)) {\n+\t\tchar *submod_gitdir_path;\n \t\tstruct strbuf sm_path = STRBUF_INIT;\n \t\tstrbuf_addstr(&sm_path, add_data->sm_path);\n \t\tsubmod_gitdir_path = xstrfmt(\"%s/.git\", add_data->sm_path);\n@@ -3207,10 +3207,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3219,8 +3220,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3238,7 +3239,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.51.0\n\n"},{"id":"530375","messageId":"20251107150547.3272180-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251107150547.3272180-1-adrian.ratiu@collabora.com","subject":"[PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-07T15:05:47Z","receivedAt":"2025-11-07T15:06:51Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new check in validate_submodule_git_dir() to detect and\nprevent case-folding filesystem colisions. When this new check\nis triggered, a stricter casefolding aware URI encoding is used\nto percent-encode uppercase characters, e.g. Foo becomes %46oo.\n\nBy using this check/retry mechanism the uppercase encoding is\nonly applied when necessary, so case-sensitive filesystems are\nnot affected.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n submodule.c                   | 47 +++++++++++++++++++++++++++++++++--\n t/t7425-submodule-encoding.sh | 15 +++++++++++\n url.c                         | 12 +++++++++\n url.h                         |  1 +\n 4 files changed, 73 insertions(+), 2 deletions(-)\n\ndiff --git a/submodule.c b/submodule.c\nindex ceaff0c1aa..ecbffac2c6 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2280,7 +2280,7 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n \tchar *p = git_dir + len - suffix_len;\n \tbool suffixes_match = !strcmp(p, submodule_name);\n-\tint ret = 0;\n+\tint ret = 0, config_ignorecase = 0;\n \n \t/*\n \t * We prevent the contents of sibling submodules' git directories to\n@@ -2318,6 +2318,42 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \tif (p && strchr(p, '/') != NULL)\n \t\treturn error(\"submodule gitdir name '%s' contains unexpected '/'\", p);\n \n+\t/* Prevent conflicts on case-folding filesystems */\n+\trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n+\tif (ignore_case || config_ignorecase) {\n+\t\tchar *lower_gitdir = xstrdup(git_dir);\n+\t\tchar *module_name = find_last_submodule_name(lower_gitdir);\n+\n+\t\tif (module_name) {\n+\t\t\tfor (p = module_name; *p; p++)\n+\t\t\t\t*p = tolower(*p);\n+\n+\t\t\t/*\n+\t\t\t * If lower path is different and already exists, check for collision.\n+\t\t\t * Intentionally double-check to eliminate false-positives.\n+\t\t\t */\n+\t\t\tif (strcmp(lower_gitdir, git_dir) && is_git_directory(lower_gitdir)) {\n+\t\t\t\tchar *canonical = real_pathdup(git_dir, 0);\n+\t\t\t\tif (canonical) {\n+\t\t\t\t\tstruct strbuf norm_git_dir = STRBUF_INIT;\n+\t\t\t\t\tstrbuf_addstr(&norm_git_dir, git_dir);\n+\t\t\t\t\tstrbuf_normalize_path(&norm_git_dir);\n+\n+\t\t\t\t\tif (strcmp(canonical, norm_git_dir.buf))\n+\t\t\t\t\t\tret = error(_(\"submodule git dir '%s' \"\n+\t\t\t\t\t\t\t      \"collides with '%s'\"),\n+\t\t\t\t\t\t\t    canonical, norm_git_dir.buf);\n+\n+\t\t\t\t\tstrbuf_release(&norm_git_dir);\n+\t\t\t\t\tFREE_AND_NULL(canonical);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\n+\t\tFREE_AND_NULL(lower_gitdir);\n+\t\treturn ret;\n+\t}\n+\n \treturn 0;\n }\n \n@@ -2653,13 +2689,20 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n \t\treturn;\n \n-\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\t/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n \tstrbuf_reset(buf);\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n \tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n \t\treturn;\n \n+\t/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */\n+\tstrbuf_reset(buf);\n+\trepo_git_path_append(r, buf, \"modules/\");\n+\tstrbuf_addstr_urlencode(buf, submodule_name, is_casefolding_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\treturn;\n+\n \t/* Case 3: error out */\n \tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n \t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\ndiff --git a/t/t7425-submodule-encoding.sh b/t/t7425-submodule-encoding.sh\nindex a42d358f5b..f92b3e6338 100755\n--- a/t/t7425-submodule-encoding.sh\n+++ b/t/t7425-submodule-encoding.sh\n@@ -143,4 +143,19 @@ test_expect_success 'submodule git dir nesting detection must work with parallel\n \tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks\n '\n \n+test_expect_success 'verify case-folding conflict is correctly encoded' '\n+\tgit clone -c extensions.submoduleEncoding=true -c core.ignoreCase=true main cloned-folding &&\n+\t(\n+\t\tcd cloned-folding &&\n+\n+\t\tgit submodule add ../new-sub \"folding\" &&\n+\t\ttest_commit lowercase &&\n+\n+\t\tgit submodule add ../new-sub \"FoldinG\" &&\n+\t\ttest_commit uppercase\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-folding \"folding\" \"modules/folding\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FoldinG\" \"modules/%46oldin%47\"\n+'\n+\n test_done\ndiff --git a/url.c b/url.c\nindex 0fb1859b28..057e6e5c6e 100644\n--- a/url.c\n+++ b/url.c\n@@ -14,6 +14,18 @@ int is_rfc3986_unreserved(char ch)\n \t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n }\n \n+/*\n+ * This is a variant of is_rfc3986_unreserved() that treats uppercase\n+ * letters as \"reserved\". This forces them to be percent-encoded, allowing\n+ * 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.\n+ */\n+int is_casefolding_rfc3986_unreserved(char c)\n+{\n+\treturn (c >= 'a' && c <= 'z') ||\n+\t       (c >= '0' && c <= '9') ||\n+\t       c == '-' || c == '.' || c == '_' || c == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex 131a262066..92e3c63514 100644\n--- a/url.h\n+++ b/url.h\n@@ -22,5 +22,6 @@ void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n int is_rfc3986_unreserved(char ch);\n+int is_casefolding_rfc3986_unreserved(char c);\n \n #endif /* URL_H */\n-- \n2.51.0\n\n"},{"id":"530403","messageId":"20251108T182050Z.vbNv4y2kizC1@fnord.qqx.org","threadId":"63967","inReplyTo":"20251107150547.3272180-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Aaron Schrab","fromEmail":"aaron@schrab.com","sentAt":"2025-11-08T18:20:50Z","receivedAt":"2025-11-08T18:20:55Z","isPatch":true,"sender":{"key":"aaron@schrab.com","avatar":"https://avatars.githubusercontent.com/u/39620?v=4"},"body":"At 17:05 +0200 07 Nov 2025, Adrian Ratiu <adrian.ratiu@collabora.com> wrote:\n>Add a new check in validate_submodule_git_dir() to detect and\n>prevent case-folding filesystem colisions. When this new check\n>is triggered, a stricter casefolding aware URI encoding is used\n>to percent-encode uppercase characters, e.g. Foo becomes %46oo.\n>\n>By using this check/retry mechanism the uppercase encoding is\n>only applied when necessary, so case-sensitive filesystems are\n>not affected.\n\nWhat happens if `Foo` is added first and doesn't conflict with anything, \nthen later a new submodule is added which would naturally get the name \n`foo` which would conflict and doesn't have any upper case characters to \nencode to avoid the conflict?\n"},{"id":"530453","messageId":"87ecq5ke2m.fsf@gentoo.mail-host-address-is-not-set","threadId":"63967","inReplyTo":"20251108T182050Z.vbNv4y2kizC1@fnord.qqx.org","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-10T17:11:29Z","receivedAt":"2025-11-10T17:11:54Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Sat, 08 Nov 2025, Aaron Schrab <aaron@schrab.com> wrote:\n> At 17:05 +0200 07 Nov 2025, Adrian Ratiu \n> <adrian.ratiu@collabora.com> wrote: \n>>Add a new check in validate_submodule_git_dir() to detect and \n>>prevent case-folding filesystem colisions. When this new check \n>>is triggered, a stricter casefolding aware URI encoding is used \n>>to percent-encode uppercase characters, e.g. Foo becomes %46oo. \n>>By using this check/retry mechanism the uppercase encoding is \n>>only applied when necessary, so case-sensitive filesystems are \n>>not affected. \n> \n> What happens if `Foo` is added first and doesn't conflict with \n> anything,  then later a new submodule is added which would \n> naturally get the name  `foo` which would conflict and doesn't \n> have any upper case characters to  encode to avoid the conflict? \n\nWhat an excellent question. Thank you!\n\nRight now, in v4, in this case the user adding the second `foo` \nmodule will have to manually set the submodule.foo.gitdir config \nto avoid the conflict, because Foo already uses the coliding path.\n\nI can add a test specifically for this edge case to exercise \nconfig overrides on case-folding file-systems conflicts.\n\nOR...\n\nMaybe we could derive a new path automatically (eg foo2 or foo_, \nsuggestions welcome) and use it if valid. This way, there is no \nuser intervention.\n\nDo you have any preference?\n\nThanks again,\nAdrian\n"},{"id":"530454","messageId":"20251110T173154Z.Hhi6cUjqDOat@fnord.qqx.org","threadId":"63967","inReplyTo":"87ecq5ke2m.fsf@gentoo.mail-host-address-is-not-set","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Aaron Schrab","fromEmail":"aaron@schrab.com","sentAt":"2025-11-10T17:31:54Z","receivedAt":"2025-11-10T17:32:00Z","isPatch":true,"sender":{"key":"aaron@schrab.com","avatar":"https://avatars.githubusercontent.com/u/39620?v=4"},"body":"At 19:11 +0200 10 Nov 2025, Adrian Ratiu <adrian.ratiu@collabora.com> wrote:\n>On Sat, 08 Nov 2025, Aaron Schrab <aaron@schrab.com> wrote:\n>>What happens if `Foo` is added first and doesn't conflict with \n>>anything,  then later a new submodule is added which would naturally \n>>get the name  `foo` which would conflict and doesn't have any upper \n>>case characters to  encode to avoid the conflict?\n\n>Right now, in v4, in this case the user adding the second `foo` module \n>will have to manually set the submodule.foo.gitdir config to avoid the \n>conflict, because Foo already uses the coliding path.\n\nI think that description minimizes the impact. I'd think that anyone \nwith a prior clone (on a case-folding file system) would need to take \nthat action after pulling the change that added the new submodule.\n\nIf action were needed only when running `git submodule add`, I think \nthat would be fine. But requiring that action in all clones seems a bit \nmuch. Some of those clones may even be managed with automation making it \neven more of a problem to add that new configuration.\n\nThe action may even be required in new clones, unless the submodule \nsetup process for new clones sorts entries so that ones with capital \nletters come later. Since some common collation rules (thinking mainly \nof the `C` locale) will put capital letters first I think that's \nunlikely to be the case.\n\n>Maybe we could derive a new path automatically (eg foo2 or foo_, \n>suggestions welcome) and use it if valid. This way, there is no user \n>intervention.\n>\n>Do you have any preference?\n\nI certainly don't have a *strong* preference. But, I think `foo2` seems \na bit clearer. Although the implied strategy there for multiple \nconflicting names may be too complex for a situation that will likely be \nexceedingly rare.\n"},{"id":"530460","messageId":"87bjl9kaji.fsf@collabora.com","threadId":"63967","inReplyTo":"20251110T173154Z.Hhi6cUjqDOat@fnord.qqx.org","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-10T18:27:45Z","receivedAt":"2025-11-10T18:28:10Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 10 Nov 2025, Aaron Schrab <aaron@schrab.com> wrote:\n> At 19:11 +0200 10 Nov 2025, Adrian Ratiu \n> <adrian.ratiu@collabora.com> wrote: \n>>On Sat, 08 Nov 2025, Aaron Schrab <aaron@schrab.com> wrote: \n>>>What happens if `Foo` is added first and doesn't conflict with \n>>>anything,  then later a new submodule is added which would \n>>>naturally  get the name  `foo` which would conflict and doesn't \n>>>have any upper  case characters to  encode to avoid the \n>>>conflict? \n> \n>>Right now, in v4, in this case the user adding the second `foo` \n>>module  will have to manually set the submodule.foo.gitdir \n>>config to avoid the  conflict, because Foo already uses the \n>>coliding path. \n> \n> I think that description minimizes the impact. I'd think that \n> anyone  with a prior clone (on a case-folding file system) would \n> need to take  that action after pulling the change that added \n> the new submodule.\n\nNo, because the extension is disabled by default.\n\n> \n> If action were needed only when running `git submodule add`, I \n> think  that would be fine. But requiring that action in all \n> clones seems a bit  much. Some of those clones may even be \n> managed with automation making it  even more of a problem to add \n> that new configuration. \n\nThe extension is opt-in: by default it does nothing. :)\n\nWhen the extension is enabled, all existing submodules are \nautomatically added to submodule.<name>.gitdir config without user \nintervention.\n\nEnabling the extension guarantees that config exists for all \nrepositories, unless there is a conflict we cannot solve, like you \npointed above, which is typically only for very rare corner-cases \n(that's the intention).\n\n> \n> The action may even be required in new clones, unless the \n> submodule  setup process for new clones sorts entries so that \n> ones with capital  letters come later. Since some common \n> collation rules (thinking mainly  of the `C` locale) will put \n> capital letters first I think that's  unlikely to be the case. \n\nThe more I think about this, the more I'm convinced the second \noption (see bleow) is the way to go, which is also more in line \nwith Junio's design to try a new path when validation fails and \nrepeat.\n\n>>Maybe we could derive a new path automatically (eg foo2 or foo_, \n>>suggestions welcome) and use it if valid. This way, there is no \n>>user  intervention.   Do you have any preference? \n> \n> I certainly don't have a *strong* preference. But, I think \n> `foo2` seems  a bit clearer. Although the implied strategy there \n> for multiple  conflicting names may be too complex for a \n> situation that will likely be  exceedingly rare. \n\nThe resolution algorithm is pretty simple:\n1. Create a path candidate.\n2. Validate if it works: if not, go back to step 1.\n\nWe can do this any number of times for all the edge cases we can \ndetect.\n\nIf foo2 also has a conflict, we can just try something else.\n\nIf all else fails, we can even hash the submodule name ;)\n"},{"id":"530465","messageId":"xmqqwm3xzots.fsf@gitster.g","threadId":"63967","inReplyTo":"87ecq5ke2m.fsf@gentoo.mail-host-address-is-not-set","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-10T19:10:07Z","receivedAt":"2025-11-10T19:10:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> On Sat, 08 Nov 2025, Aaron Schrab <aaron@schrab.com> wrote:\n>> At 17:05 +0200 07 Nov 2025, Adrian Ratiu \n>> <adrian.ratiu@collabora.com> wrote: \n>>>Add a new check in validate_submodule_git_dir() to detect and \n>>>prevent case-folding filesystem colisions. When this new check \n>>>is triggered, a stricter casefolding aware URI encoding is used \n>>>to percent-encode uppercase characters, e.g. Foo becomes %46oo. \n>>>By using this check/retry mechanism the uppercase encoding is \n>>>only applied when necessary, so case-sensitive filesystems are \n>>>not affected. \n\nThe .gitdir name munging is a local thing, so it makes sense to do\nthe casefold mitigation only the filesystem is case folding one,\n\nYour code seems to compare directory names textually, and downcasing\nthe proposed name for some reason, but I am not sure why we need any\nof these complexity.  Wouldn't it be the matter of actually trying\nto mkdir(2) the name presented (either \"foo\" or \"Foo\") and see if\nthat fails?  If it fails (most likely with EEXIST if case folding is\ngetting in the way, but for any reason), the name is unusable and we\nneed to \"tweak\" the name to a usable one at that point by retrying.\nOnce we find a usable name, we can remember the fact that we already\ncreated a directory for it and reuse that empty directory in the\ncode where we used to do mkdir(2), no?\n\n> Maybe we could derive a new path automatically (eg foo2 or foo_, \n> suggestions welcome) and use it if valid. This way, there is no \n> user intervention.\n>\n> Do you have any preference?\n\nIf adding 'foo' and then an attempt to add 'Foo' will automatically\nassign a name that does not conflict with 'foo' to the newly added\nsubmodule, then the users would expect the same to happen if the\norder to add them are swapped, wouldn't they?\n\nIOW, I do not see why the code wants to treat uppercase and\nlowercase letters any differently, and suspect that it might be the\nsource of additional complication.  Also, if there is an existing\nmodule with a funny path \"%46oo\", you cannot just encode \"Foo\" into\n\"%46oo\" to avoid crashes with 'foo' and be done anyway, so it feels\nlike we are inviting more bugs by special casing certain paths (and\nnot encoding or checking others).  Don't we have an issue similar to\n\"case folding\" in macOS wrt UTF-8 canonicalization, too?  An\nidentical Unicode string may be canonicalized in two ways, so in a\npresence of a submodule named one way, the other submodule named in\nthe other canonicalization, while their names may be with different\nbyte sequences, cannot co-exist in the same directory next to each\nother.  \"Try to mkdir(2) the new name, and see if it succeeds, and\nif so use the resulting empty directory\" approach would cover that\ncase with the same mechanism as you need to use for case folding\nfilesystems, I would imagine.\n\n"},{"id":"530477","messageId":"878qgdjxvc.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqwm3xzots.fsf@gitster.g","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-10T23:01:27Z","receivedAt":"2025-11-10T23:01:49Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hi Junio,\n\nOn Mon, 10 Nov 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> On Sat, 08 Nov 2025, Aaron Schrab <aaron@schrab.com> wrote: \n>>> At 17:05 +0200 07 Nov 2025, Adrian Ratiu \n>>> <adrian.ratiu@collabora.com> wrote:  \n>>>>Add a new check in validate_submodule_git_dir() to detect and \n>>>>prevent case-folding filesystem colisions. When this new check \n>>>>is triggered, a stricter casefolding aware URI encoding is \n>>>>used  to percent-encode uppercase characters, e.g. Foo becomes \n>>>>%46oo.  By using this check/retry mechanism the uppercase \n>>>>encoding is  only applied when necessary, so case-sensitive \n>>>>filesystems are  not affected.  \n> \n> The .gitdir name munging is a local thing, so it makes sense to \n> do the casefold mitigation only the filesystem is case folding \n> one,\n\nThat is correct. Case-sensitive filesystems will stop before \nreaching the \"Case 2.2\" attempt, because their gitdirs will pass \nvalidation in the earlier steps.\n \n> \n> Your code seems to compare directory names textually, and \n> downcasing the proposed name for some reason, but I am not sure \n> why we need any of these complexity.  Wouldn't it be the matter \n> of actually trying to mkdir(2) the name presented (either \"foo\" \n> or \"Foo\") and see if that fails?  If it fails (most likely with \n> EEXIST if case folding is getting in the way, but for any \n> reason), the name is unusable and we need to \"tweak\" the name to \n> a usable one at that point by retrying.  Once we find a usable \n> name, we can remember the fact that we already created a \n> directory for it and reuse that empty directory in the code \n> where we used to do mkdir(2), no?\n\nI tried the mkdir approach. Unfortunately it does not work because \nsubmodule_name_to_gitdir() is called on all submodule paths: both \nexisting or new, valid or non-valid.\n\nSo if a dir already exists, we do not know if there is a \nconflict. It might just be a normal valid path verification of an \nexisting module.\n\nThis is why I had to come up with the double-test using to_lower() \nto compute a common path, canonicalization and checking for \nexistence when there is a difference via is_git_directory(). In \nthis case there is always a conflict and is the only reliable way \nI colud think of.\n\nI am very much in favor for finding a simpler check, however we \nneed something at least as reliable as the current double-test, \nwhich passes all cases I could think of and all CI tests.\n \n> \n>> Maybe we could derive a new path automatically (eg foo2 or \n>> foo_,  suggestions welcome) and use it if valid. This way, \n>> there is no  user intervention. \n>> \n>> Do you have any preference? \n> \n> If adding 'foo' and then an attempt to add 'Foo' will \n> automatically assign a name that does not conflict with 'foo' to \n> the newly added submodule, then the users would expect the same \n> to happen if the order to add them are swapped, wouldn't they?\n\nYes and it's a valid expectation. :)\n\nI just missed this corner-case which Aaron brought up (many thanks \nagain Aaron!).\n\nIt's a simple fix which I'll do in v5: if validation fails, we \njust need to come up with another name and retry.\n\nThe probability of conflicts decreases exponentially with each \nretry. By the 4-5 retry the probability is so small, at that \npoint, if we're that desperate, we might just hash the name or use \na random string.\n\n> \n> IOW, I do not see why the code wants to treat uppercase and \n> lowercase letters any differently, and suspect that it might be \n> the source of additional complication.  Also, if there is an \n> existing module with a funny path \"%46oo\", you cannot just \n> encode \"Foo\" into \"%46oo\" to avoid crashes with 'foo' and be \n> done anyway, so it feels like we are inviting more bugs by \n> special casing certain paths (and not encoding or checking \n> others).  Don't we have an issue similar to \"case folding\" in \n> macOS wrt UTF-8 canonicalization, too?  An identical Unicode \n> string may be canonicalized in two ways, so in a presence of a \n> submodule named one way, the other submodule named in the other \n> canonicalization, while their names may be with different byte \n> sequences, cannot co-exist in the same directory next to each \n> other.  \"Try to mkdir(2) the new name, and see if it succeeds, \n> and if so use the resulting empty directory\" approach would \n> cover that case with the same mechanism as you need to use for \n> case folding filesystems, I would imagine. \n\nFoo and \"%46oo\" is another possible conflict, yes, but only when \nboth \"foo\" and \"%46oo\" already exist. The deeper you go in the \nretry cycles, the more unlikely conflicts become.\n\nWe just need to detect this conflict, come up with another name \nand retry. Retry until one sticks.\n\nAgain, the probability of conflicts decreases exponentially with \neach retry. :) If we're desperate: hash the name or try a random \nstring before giving up and throwing an error, which is \"Case 3\" \nin the current patch.\n\n(I wrote above why mkdir cannot solve the detection problem, hope \nit makes sense).\n\nI think it's also worth mentioning a key idea of this design: we \ndon't need to detect & fix all corner cases. We can iterate and \nimprove both the conflict detection and path generation over time, \nsince they are opaque to users, who rely only on the resulting \nsubmodule.name.gitdir config we publish.\n\nI know it seems like a game of \"whack-a-mole\", but even if we fix \njust half of the possible conflicts, the likeliest ones, we still \nare in a much better situation than before. At some point we're \napproaching diminishing returns, so extremely rare conflicts might \nnot even be worth fixing.\n\nI will add a test for this \"Foo\" + \"foo\" + \"%46oo\" in v5 as \nwell. :)\n"},{"id":"530478","messageId":"xmqqjyzxxyt0.fsf@gitster.g","threadId":"63967","inReplyTo":"878qgdjxvc.fsf@collabora.com","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-10T23:17:31Z","receivedAt":"2025-11-10T23:17:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> I tried the mkdir approach. Unfortunately it does not work because \n> submodule_name_to_gitdir() is called on all submodule paths: both \n> existing or new, valid or non-valid.\n>\n> So if a dir already exists, we do not know if there is a \n> conflict. It might just be a normal valid path verification of an \n> existing module.\n\nPuzzled.  Wouldn't that only mean that submodule-name-to-gitdir is a\nwrong place to see if the name of a directory you are planning to\nuse for a newly added submodule is available?  IOW, don't you need a\nmore specific new helper function and perform the check in there?\n"},{"id":"530501","messageId":"875xbgkahb.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqjyzxxyt0.fsf@gitster.g","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-11T12:41:20Z","receivedAt":"2025-11-11T12:41:44Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 10 Nov 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes: \n> \n>> I tried the mkdir approach. Unfortunately it does not work \n>> because  submodule_name_to_gitdir() is called on all submodule \n>> paths: both  existing or new, valid or non-valid. \n>> \n>> So if a dir already exists, we do not know if there is a \n>> conflict. It might just be a normal valid path verification of \n>> an  existing module. \n> \n> Puzzled.  Wouldn't that only mean that submodule-name-to-gitdir \n> is a wrong place to see if the name of a directory you are \n> planning to use for a newly added submodule is available?  IOW, \n> don't you need a more specific new helper function and perform \n> the check in there? \n\nI thought long and hard about this in each version since v1 and \nkeep coming to the same answer: we need to do it here and can't \navoid it.\n\nThe reason is that submodule_name_to_gitdir() is the unified API \nwhere submodule gitdirs get computed consistently and validation \ngoes hand in hand with computing the valid gitdirs. If you look at \nall the places where validate_submodule_git_dir() is currently \ncalled, it's always immediately after submodule_name_to_gitdir(), \non its result.\n\nIt even makes sense to remove the validation calls outside \nsubmodule_name_to_gitdir() and do it inside it for each path \nattempt, like we do for the encoding cases, so API users don't \nhave to manually validate the result each time. \n\nThis reminds me: we should also validate the gitdir path we get \nvia the submodule.%s.gitdir config. Will fix this as well in v5.\n\nAnother related question I've been pondering is whether to create \ntwo separate validation functions, one for legacy and one for \nencoding.  It doesn't win us much (currently we return early if \nextension/encoding is enabled in the unified function), but we can \ndo this in v5 as well.\n\nTo recap, what I intend to do in v5 (up to now) is:\n- Address Aaron's case-fold corner-case of creating Foo then foo.\n- Address Junio's case-fold corner-case of creating foo + Foo + \n  %46oo.\n- (Important) Also validate the submodule.%s.gitdir config value.\n- Move validation checks inside submodule_name_to_gitdir() to \nensure they run each time, instead of having API users validate \nthe gitdir result each time.  - (Undecided) Split the validation \nfunction in two for the legacy vs encoding/extension cases.\n- (Undecided) If all our path attempts fail, try hashing the name \nand see if that works before erroring out.\n\nEspecially if we implement the last point with hashing, the \nchances for conflicts are close to 0.\n\nHere's an interesting idea:\n\nMaybe it's easier to just hash everything when the extension is \nenabled since users now always have the submodule.%s.gitdir \nconfig, which is also accesible via the submodule--helper?\n\nHashing + config would eliminate all this corner-case \ncomplexity. :)\n"},{"id":"530589","messageId":"871pm3jmnp.fsf@collabora.com","threadId":"63967","inReplyTo":"20251107150547.3272180-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH v4 4/4] submodule: fix case-folding gitdir filesystem colisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-12T15:28:10Z","receivedAt":"2025-11-12T15:28:44Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Fri, 07 Nov 2025, Adrian Ratiu <adrian.ratiu@collabora.com> \nwrote:\n> diff --git a/submodule.c b/submodule.c index \n> ceaff0c1aa..ecbffac2c6 100644 --- a/submodule.c +++ \n> b/submodule.c @@ -2280,7 +2280,7 @@ int \n> validate_submodule_git_dir(char *git_dir, const char \n> *submodule_name) \n>  \tsize_t len = strlen(git_dir), suffix_len = \n>  strlen(submodule_name); char *p = git_dir + len - suffix_len; \n>  bool suffixes_match = !strcmp(p, submodule_name); \n> -\tint ret = 0; +\tint ret = 0, config_ignorecase = 0; \n>   /* * We prevent the contents of sibling submodules' git \n>  directories to \n> @@ -2318,6 +2318,42 @@ int validate_submodule_git_dir(char \n> *git_dir, const char *submodule_name) \n>  \tif (p && strchr(p, '/') != NULL) return error(\"submodule \n>  gitdir name '%s' contains unexpected '/'\", p);  \n> +\t/* Prevent conflicts on case-folding filesystems */ + \n> repo_config_get_bool(the_repository, \"core.ignorecase\", \n> &config_ignorecase); +\tif (ignore_case || \n> config_ignorecase) { +\t\tchar *lower_gitdir = \n> xstrdup(git_dir); +\t\tchar *module_name = \n> find_last_submodule_name(lower_gitdir); + +\t\tif \n> (module_name) { +\t\t\tfor (p = module_name; *p; \n> p++) +\t\t\t\t*p = tolower(*p); + + \n> /* +\t\t\t * If lower path is different and already \n> exists, check for collision.  +\t\t\t * \n> Intentionally double-check to eliminate false-positives.  + \n> */ +\t\t\tif (strcmp(lower_gitdir, git_dir) && \n> is_git_directory(lower_gitdir)) { + \n> char *canonical = real_pathdup(git_dir, 0); + \n> if (canonical) { +\t\t\t\t\tstruct \n> strbuf norm_git_dir = STRBUF_INIT; + \n> strbuf_addstr(&norm_git_dir, git_dir); + \n> strbuf_normalize_path(&norm_git_dir); + + \n> if (strcmp(canonical, norm_git_dir.buf)) + \n> ret = error(_(\"submodule git dir '%s' \" + \n> \"collides with '%s'\"), + \n> canonical, norm_git_dir.buf); + + \n> strbuf_release(&norm_git_dir); + \n> FREE_AND_NULL(canonical); +\t\t\t\t} + \n> } +\t\t} + +\t\tFREE_AND_NULL(lower_gitdir); + \n> return ret; +\t} + \n>  \treturn 0; } \n\nI think I came up with a better case-folding conflict detection \nimplementation.\n\nIn a nutshell, for the next iteration (v5), I intend to do:\n\nDIR *dir = opendir(modules_dir);\n...\n/* Check for another directory under .git/modules that differs \nonly in case. */ while ((de = readdir(dir)) != NULL) { if \n(!strcmp(de->d_name, \".\") || !strcmp(de->d_name, \"..\"))  continue; \nif (!strcasecmp(de->d_name, submodule_name) && strcmp(de->d_name, \nsubmodule_name)) { closedir(dir); return error(_(\"submodule name \n'%s' collides with '%s' \" \"on case-insensitive filesystem\"), \nsubmodule_name, de->d_name); } }\n\nWe look at existing submodules and we have a conflict if both are \ntrue:\n1. Names are equal ignoring the case (case insensitive equality).\n2. Names are NOT equal considering the case (case sensitive \ninequality).\n\nThis is simpler, more robest and passes all my test cases.\n\nWill leave v4 on the ML until next week in case there is more feedback,\nthen I'll send v5 using this check (I'll also add more tests).\n"},{"id":"530719","messageId":"6m72swbxcm2gi2wtvgc4yxid3o64qbuckzzguzg3mzd6rmrvx5@i55v6c2nq5e4","threadId":"63967","inReplyTo":"20251107150547.3272180-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v4 0/4] Encode submodule gitdir names to avoid conflicts","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2025-11-14T23:03:12Z","receivedAt":"2025-11-14T23:03:20Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2025.11.07 17:05, Adrian Ratiu wrote:\n> Hello everyone,\n> \n> For those new to this series, we are adding an extension to encode submodule\n> gitdir paths to avoid filesystem conflicts.\n\n\nDisclaimer for the list: Google is funding Adrian's work on this series;\nnoting this now for transparency.\n\n\nHi Adrian, sorry for missing the last couple of versions of this series.\n\nThe switch to using an extension may complicate our migration a bit.\nBackground for the list: Google has been using an early version of this\nsubmodule encoding scheme for years. We have a lot of users'\nrepositories with this encoding scheme in place on disk, but with no\ncorresponding extensions.submoduleEncoding config.\n\nI've done some limited testing; the good news is that it looks like\nusing this series with pre-encoded submodules still works, regardless of\nthe value of extensions.submoduleEncoding. It would be nice to add some\ntests in V5 that we can create some submodules with the extension\nenabled, and then disable it later and still work with the encoded\nsubmodules (and then maybe enable it once again).\n\nThe first difficulty I see is that there's not a good way to\nautomatically migrate existing repos to the new extension; we'll have to\nask users to manually set configs on each of their repos. While we are\nable to distribute default Git configs for our users,\n`core.repositoryFormatValue` and `extensions.*` are obviously special\ncases that can't be applied from non-repo-local configs. I don't know\nwhat could be changed in this series to avoid the issue, so I guess I'll\ninstead just ask the list for ideas for automating this migration. One\nidea is to carry a tiny downstream patch to force-enable\n`extensions.submoduleEncoding` regardless of the local config, but maybe\nsomeone else has a better idea.\n\nA second issue is that we'd like to be able to set submoduleEncoding for\nnew repositories, without requiring passing a config on the command\nline. Perhaps we could add another config option analogous to\n`init.defaultObjectFormat` that we can set in our locally-distributed\nconfig.\n"},{"id":"530816","messageId":"87cy5gbs61.fsf@gentoo.mail-host-address-is-not-set","threadId":"63967","inReplyTo":"6m72swbxcm2gi2wtvgc4yxid3o64qbuckzzguzg3mzd6rmrvx5@i55v6c2nq5e4","subject":"Re: [PATCH v4 0/4] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-17T15:22:14Z","receivedAt":"2025-11-17T15:22:39Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hi Josh and thanks for testing & the feedback.\n\nOn Fri, 14 Nov 2025, Josh Steadmon <steadmon@google.com> wrote:\n> The switch to using an extension may complicate our migration a \n> bit.  Background for the list: Google has been using an early \n> version of this submodule encoding scheme for years. We have a \n> lot of users' repositories with this encoding scheme in place on \n> disk, but with no corresponding extensions.submoduleEncoding \n> config. \n> \n> I've done some limited testing; the good news is that it looks \n> like using this series with pre-encoded submodules still works, \n> regardless of the value of extensions.submoduleEncoding. It \n> would be nice to add some tests in V5 that we can create some \n> submodules with the extension enabled, and then disable it later \n> and still work with the encoded submodules (and then maybe \n> enable it once again). \n\nYes, I'd expect forward-migration (enabling the extension) to be \nvery smooth. I will add some tests for backwards migration \n(disabling the extension) as well in v5.\n\n> \n> The first difficulty I see is that there's not a good way to \n> automatically migrate existing repos to the new extension; we'll \n> have to ask users to manually set configs on each of their \n> repos. While we are able to distribute default Git configs for \n> our users, `core.repositoryFormatValue` and `extensions.*` are \n> obviously special cases that can't be applied from \n> non-repo-local configs. I don't know what could be changed in \n> this series to avoid the issue, so I guess I'll instead just ask \n> the list for ideas for automating this migration. One idea is to \n> carry a tiny downstream patch to force-enable \n> `extensions.submoduleEncoding` regardless of the local config, \n> but maybe someone else has a better idea.\n\nI propose to use a build-time configuration option to force-enable \n/ set the config automatically for these types of situations.\n\nThis way you just add a flag to your builds and don't need to \ncarry downstream patches, though it could also be solved with \none-liner downstream patch. :)\n \n> \n> A second issue is that we'd like to be able to set \n> submoduleEncoding for new repositories, without requiring \n> passing a config on the command line. Perhaps we could add \n> another config option analogous to `init.defaultObjectFormat` \n> that we can set in our locally-distributed config. \n\nThis will also be solved by the build-time config option I \nproposed above.\n\nI plan to send v5 soon and I'll include these changes, if nobody \nobjects, along with everything else I've got ready.\n"},{"id":"531009","messageId":"20251119211030.2008441-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251119211030.2008441-1-adrian.ratiu@collabora.com","subject":"[PATCH v5 2/7] builtin/credential-store: move is_rfc3986_unreserved to url.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-19T21:10:25Z","receivedAt":"2025-11-19T21:11:33Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"is_rfc3986_unreserved() was moved to credential-store.c and was made\nstatic by f89854362c (credential-store: move related functions to\ncredential-store file, 2023-06-06) under a correct assumption, at the\ntime, that it was the only place using it.\n\nHowever now we need it to apply URL-encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back as a\npublic function exposed via url.h, instead of the old helper path\n(strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n\nThis function will be used by submodule.c in the next commit.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c |  7 +------\n url.c                      | 11 +++++++++++\n url.h                      |  2 ++\n 3 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..bc1453c6b2 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -7,6 +7,7 @@\n #include \"path.h\"\n #include \"string-list.h\"\n #include \"parse-options.h\"\n+#include \"url.h\"\n #include \"write-or-die.h\"\n \n static struct lock_file credential_lock;\n@@ -76,12 +77,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/url.c b/url.c\nindex 282b12495a..0fb1859b28 100644\n--- a/url.c\n+++ b/url.c\n@@ -3,6 +3,17 @@\n #include \"strbuf.h\"\n #include \"url.h\"\n \n+/*\n+ * The set of unreserved characters as per STD66 (RFC3986) is\n+ * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n+ * components without percent-encoding.\n+ */\n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex 2a27c34277..131a262066 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,4 +21,6 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+int is_rfc3986_unreserved(char ch);\n+\n #endif /* URL_H */\n-- \n2.51.0\n\n"},{"id":"531010","messageId":"20251119211030.2008441-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251119211030.2008441-1-adrian.ratiu@collabora.com","subject":"[PATCH v5 1/7] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-19T21:10:24Z","receivedAt":"2025-11-19T21:11:35Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded modules gitdir path leading to test failures.\n\nCall the submodule_name_to_gitdir() helper instead, which was invented\nexactly for this purpose and is already used by all the other locations\nwhich work on gitdirs.\n\nAlso narrow the scope of the submod_gitdir_path variable which is not\nused anymore in the updated \"else\" branch.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 13 +++++++------\n 1 file changed, 7 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex fcd73abe53..2873b2780e 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3187,13 +3187,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n \n static int add_submodule(const struct add_data *add_data)\n {\n-\tchar *submod_gitdir_path;\n \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n \tint ret = -1;\n \n \t/* perhaps the path already exists and is already a git repo, else clone it */\n \tif (is_directory(add_data->sm_path)) {\n+\t\tchar *submod_gitdir_path;\n \t\tstruct strbuf sm_path = STRBUF_INIT;\n \t\tstrbuf_addstr(&sm_path, add_data->sm_path);\n \t\tsubmod_gitdir_path = xstrfmt(\"%s/.git\", add_data->sm_path);\n@@ -3207,10 +3207,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3219,8 +3220,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3238,7 +3239,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.51.0\n\n"},{"id":"531011","messageId":"20251119211030.2008441-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH v5 0/7] Encode submodule gitdir names to avoid conflicts","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-19T21:10:23Z","receivedAt":"2025-11-19T21:11:52Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello everyone,\n\nFor those new to the series, we are adding an extension to fix submodule\ngitdir path conflicts due to nested gitdirs or case-folding filesystems.\n\nv5 contains code cleanups, more corner-case fixes and a few new commits\nbriefly described below, after which a range-diff follows.\n\nAs always this is based on the latest master, I've checked for conflicts\nwith the next/seen branches, pushed to GitHub [1] and CI passes [2].\n\nMany thanks to all reviewers!\n\n1: https://github.com/10ne1/git/tree/dev/aratiu/encoding-v5\n2: https://github.com/10ne1/git/actions/runs/19515373994\n\nChanges between v4 -> v5:\n* Derive some new gitdir names in case the uppercase-extended encoding fails. (Aaron)\n* Add more case-folding and extension disabling / cloning test corner-cases. (Aaron, Junio & Josh)\n* Run the gitdir validation on the submodules.<name>.gitdir config after reading it. (Adrian)\n* New commit: add a build-time option to enable the extension. (Josh)\n* New commit: fallback to hashing if all encoding attempts fail. (Adrian)\n* New commit: always validate the gitdir in submodule_name_to_git_dir. (Adrian)\n* Split encoding validation in a separate function and leave non-enc validation intact. (Adrian)\n* Add a CASE_INSENSITIVE_FS prereq for the case-folding tests. This will also significantly\n  simplify case-folding validation, by not attempting to run on case-sensitive FS. (Adrian)\n* Improve case-folding conflict detection by making validation more robust. (Adrian)\n* Minor code fixes and improvements (spelling, error checking, indentation). (Adrian)\n\nRange-diff between v4 -> v5:\n1:  7d34507692 = 1:  9d5855f3bf submodule--helper: use submodule_name_to_gitdir in add_submodule\n2:  1e609bdd1a = 2:  8cfa970a9d builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n-:  ---------- > 3:  7bcadf1116 submodule: always validate gitdirs inside submodule_name_to_gitdir\n3:  c51fd58669 ! 4:  1b5d0b50ef submodule: add extension to encode gitdir paths\n    @@ Documentation/config/extensions.adoc: relativeWorktrees:::\n      \n     +submoduleEncoding:::\n     +\tIf enabled, submodule gitdir paths are encoded to avoid filesystem\n    -+\tconflicts due to nested gitdirs, case insensitivity or other issues\n    ++\tconflicts due to nested gitdirs, case insensitivity or other issues.\n     +\tWhen enabled, the submodule.<name>.gitdir config is always set for\n    -+\tall submodulesand is the single point of authority for gitdir paths.\n    ++\tall submodules and is the single point of authority for gitdir paths.\n     +\n      worktreeConfig:::\n      \tIf enabled, then worktrees will load config settings from the\n    @@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n      }\n      \n     +/*\n    -+ * Find the last submodule name in the gitdir path (modules can be nested).\n    -+ * Returns a pointer into `path` to the beginning of the name or NULL if not found.\n    ++ * Encoded gitdir validation function used when extensions.submoduleEncoding is enabled.\n    ++ * This does not print errors like the non-encoded version, because encoding is supposed\n    ++ * to mitigate / fix all these.\n     + */\n    -+static char *find_last_submodule_name(char *git_dir_path)\n    ++static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n     +{\n     +\tconst char *modules_marker = \"/modules/\";\n    -+\tchar *p = git_dir_path;\n    -+\tchar *last = NULL;\n    ++\tchar *p = git_dir, *last_submodule_name = NULL;\n     +\n    ++\tif (!the_repository->repository_format_submodule_encoding)\n    ++\t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n    ++\t\t    \"extensions.submoduleEncoding enabled.\");\n    ++\n    ++\t/* Find the last submodule name in the gitdir path (modules can be nested). */\n     +\twhile ((p = strstr(p, modules_marker))) {\n    -+\t\tlast = p + strlen(modules_marker);\n    ++\t\tlast_submodule_name = p + strlen(modules_marker);\n     +\t\tp++;\n     +\t}\n     +\n    -+\treturn last;\n    ++\t/* Prevent the use of '/' in encoded names */\n    ++\tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n    ++\t\treturn -1;\n    ++\n    ++\treturn 0;\n     +}\n     +\n    - int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    + static int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n      {\n      \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n    --\tchar *p;\n    -+\tchar *p = git_dir + len - suffix_len;\n    -+\tbool suffixes_match = !strcmp(p, submodule_name);\n    + \tchar *p;\n      \tint ret = 0;\n      \n    --\tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n    --\t    strcmp(p, submodule_name))\n    --\t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n    --\t\t    submodule_name, git_dir);\n    --\n    - \t/*\n    - \t * We prevent the contents of sibling submodules' git directories to\n    - \t * clash.\n    -@@ submodule.c: int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    - \t * but the latter directory is already designated to contain the hooks\n    - \t * of the former.\n    - \t */\n    --\tfor (; *p; p++) {\n    -+\tfor (; *p && suffixes_match; p++) {\n    - \t\tif (is_dir_sep(*p)) {\n    - \t\t\tchar c = *p;\n    - \n    -@@ submodule.c: int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    - \t\t}\n    - \t}\n    - \n    -+\t/* tests after this check are only for encoded names, when the extension is enabled */\n    -+\tif (!the_repository->repository_format_submodule_encoding)\n    -+\t\treturn 0;\n    -+\n    -+\t/* Prevent the use of '/' in names */\n    -+\tp = find_last_submodule_name(git_dir);\n    -+\tif (p && strchr(p, '/') != NULL)\n    -+\t\treturn error(\"submodule gitdir name '%s' contains unexpected '/'\", p);\n    ++\tif (the_repository->repository_format_submodule_encoding)\n    ++\t\tBUG(\"validate_submodule_git_dir() must be called with \"\n    ++\t\t    \"extensions.submoduleEncoding disabled.\");\n     +\n    - \treturn 0;\n    - }\n    - \n    + \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n    + \t    strcmp(p, submodule_name))\n    + \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n     @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n      \treturn ret;\n      }\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     +{\n     +\tchar *key;\n     +\n    -+\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n    ++\tif (validate_submodule_encoded_git_dir(gitdir_path->buf, submodule_name))\n     +\t\treturn -1;\n     +\n     +\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     +\tFREE_AND_NULL(key);\n     +\n     +\treturn 0;\n    -+\n     +}\n     +\n      void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     +\tstrbuf_addstr(buf, submodule_name);\n     +\n     +\t/* If extensions.submoduleEncoding is disabled, use the plain path set above */\n    -+\tif (!r->repository_format_submodule_encoding)\n    -+\t\treturn;\n    ++\tif (!r->repository_format_submodule_encoding) {\n    ++\t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n    ++\t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n    ++\t\t\t      \"git dir\"), buf->buf);\n    ++\n    ++\t\treturn; /* plain gitdir is valid for use */\n    ++\t}\n     +\n     +\t/* Extension is enabled: use the gitdir config if it exists */\n     +\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     +\t\tstrbuf_reset(buf);\n     +\t\tstrbuf_addstr(buf, gitdir);\n     +\t\tFREE_AND_NULL(key);\n    ++\n    ++\t\t/* validate because users might have modified the config */\n    ++\t\tif (validate_submodule_encoded_git_dir(buf->buf, submodule_name))\n    ++\t\t\tdie(_(\"Invalid 'submodule.%s.gitdir' config: '%s' please check \"\n    ++\t\t\t      \"if it is unique or conflicts with another module\"),\n    ++\t\t\t    submodule_name, gitdir);\n    ++\n     +\t\treturn;\n     +\t}\n     +\tFREE_AND_NULL(key);\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     +\tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n     +\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n     +\t\treturn;\n    -+\n    -+\t/* Case 3: error out */\n    + \n    +-\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n    +-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n    +-\t\t      \"git dir\"), buf->buf);\n    ++\t/* Case 3: Nothing worked: error out */\n     +\tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n     +\t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\n     +\t    submodule_name, submodule_name);\n    @@ t/t7425-submodule-encoding.sh (new)\n     +\t\tgit submodule add ../legacy-sub legacy &&\n     +\t\ttest_commit legacy-sub &&\n     +\n    ++\t\t# trigger the \"die_path_inside_submodule\" check\n    ++\t\ttest_must_fail git submodule add ../new-sub \"legacy/nested\" &&\n    ++\n     +\t\tgit config core.repositoryformatversion 1 &&\n     +\t\tgit config extensions.submoduleEncoding true &&\n     +\n     +\t\tgit submodule add ../new-sub \"New Sub\" &&\n    -+\t\ttest_commit new\n    ++\t\ttest_commit new &&\n    ++\n    ++\t\t# retrigger the \"die_path_inside_submodule\" check with encoding\n    ++\t\ttest_must_fail git submodule add ../new-sub \"New Sub/nested2\"\n     +\t)\n     +'\n     +\n    @@ t/t7425-submodule-encoding.sh (new)\n     +\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks\n     +'\n     +\n    ++test_expect_success 'disabling extensions.submoduleEncoding prevents nested submodules' '\n    ++\t(\n    ++\t\tcd clone_nested &&\n    ++\t\t# disable extension and verify failure\n    ++\t\tgit config extensions.submoduleEncoding false &&\n    ++\t\ttest_must_fail git submodule add ./thing2 hippo/foobar &&\n    ++\t\t# re-enable extension and verify it works\n    ++\t\tgit config extensions.submoduleEncoding true &&\n    ++\t\tgit submodule add ./thing2 hippo/foobar\n    ++\t)\n    ++'\n    ++\n     +test_done\n     \n      ## t/t9902-completion.sh ##\n4:  01a5b10d5a < -:  ---------- submodule: fix case-folding gitdir filesystem colisions\n-:  ---------- > 5:  2bf1c116a2 submodule: fix case-folding gitdir filesystem colisions\n-:  ---------- > 6:  b607d7ca39 submodule: use hashed name for gitdir\n-:  ---------- > 7:  9b4890cfd2 meson/Makefile: allow setting submodule encoding at build time\n\nAdrian Ratiu (7):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n  submodule: always validate gitdirs inside submodule_name_to_gitdir\n  submodule: add extension to encode gitdir paths\n  submodule: fix case-folding gitdir filesystem colisions\n  submodule: use hashed name for gitdir\n  meson/Makefile: allow setting submodule encoding at build time\n\n Documentation/config/extensions.adoc  |   8 +\n Documentation/config/submodule.adoc   |   5 +\n Makefile                              |   5 +\n builtin/credential-store.c            |   7 +-\n builtin/submodule--helper.c           |  51 +++--\n configure.ac                          |  23 +++\n meson.build                           |   4 +\n meson_options.txt                     |   2 +\n repository.c                          |   1 +\n repository.h                          |   1 +\n setup.c                               |  15 ++\n setup.h                               |   1 +\n submodule.c                           | 234 ++++++++++++++++++-----\n submodule.h                           |   5 -\n t/lib-verify-submodule-gitdir-path.sh |  24 +++\n t/meson.build                         |   1 +\n t/t7425-submodule-encoding.sh         | 258 ++++++++++++++++++++++++++\n t/t9902-completion.sh                 |   1 +\n url.c                                 |  23 +++\n url.h                                 |   3 +\n 20 files changed, 591 insertions(+), 81 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-encoding.sh\n\n-- \n2.51.0\n\n"},{"id":"531012","messageId":"20251119211030.2008441-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251119211030.2008441-1-adrian.ratiu@collabora.com","subject":"[PATCH v5 3/7] submodule: always validate gitdirs inside submodule_name_to_gitdir","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-19T21:10:26Z","receivedAt":"2025-11-19T21:11:52Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Move the ad-hoc validation checks sprinkled across the source tree,\nafter calling submodule_name_to_gitdir() into the function proper,\nwhich now always validates the gitdir before returning it.\n\nThis also makes parallel operations a bit safer due to checking and\nerroring out each time the unified API detects a problem instead of\nhaving one extra hardcoded validation check in submodule--helper.c.\n\nIt simplifies the API usage as well since users who don't have to\nvalidate the submodule_name_to_gitdir() result themselves anymore\nand reduces the risks of API users forgetting to validate.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 21 ---------------------\n submodule.c                 | 30 ++++++------------------------\n submodule.h                 |  5 -----\n 3 files changed, 6 insertions(+), 50 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 2873b2780e..9914ca0786 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1703,10 +1703,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n \t\t\t\t\t\t    clone_data->path);\n \n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\n \tif (!file_exists(sm_gitdir)) {\n \t\tif (clone_data->require_init && !stat(clone_data_path, &st) &&\n \t\t    !is_empty_dir(clone_data_path))\n@@ -1780,23 +1776,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tfree(path);\n \t}\n \n-\t/*\n-\t * We already performed this check at the beginning of this function,\n-\t * before cloning the objects. This tries to detect racy behavior e.g.\n-\t * in parallel clones, where another process could easily have made the\n-\t * gitdir nested _after_ it was created.\n-\t *\n-\t * To prevent further harm coming from this unintentionally-nested\n-\t * gitdir, let's disable it by deleting the `HEAD` file.\n-\t */\n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {\n-\t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n-\t\tunlink(head);\n-\t\tfree(head);\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\t}\n-\n \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n \n \tp = repo_submodule_path(the_repository, clone_data_path, \"config\");\ndiff --git a/submodule.c b/submodule.c\nindex 35c55155f7..8ef028f26b 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2153,30 +2153,11 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \n \tif (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {\n \t\tif (old_head) {\n-\t\t\tif (!submodule_uses_gitfile(path))\n-\t\t\t\tabsorb_git_dir_into_superproject(path,\n-\t\t\t\t\t\t\t\t super_prefix);\n-\t\t\telse {\n-\t\t\t\tchar *dotgit = xstrfmt(\"%s/.git\", path);\n-\t\t\t\tchar *git_dir = xstrdup(read_gitfile(dotgit));\n-\n-\t\t\t\tfree(dotgit);\n-\t\t\t\tif (validate_submodule_git_dir(git_dir,\n-\t\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n-\t\t\t\t\t      \"another submodule's git dir\"),\n-\t\t\t\t\t    git_dir);\n-\t\t\t\tfree(git_dir);\n-\t\t\t}\n+\t\t\tabsorb_git_dir_into_superproject(path, super_prefix);\n \t\t} else {\n \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n \t\t\t\t\t\t sub->name);\n-\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n-\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n-\t\t\t\t      \"submodule's git dir\"),\n-\t\t\t\t    gitdir.buf);\n \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n \t\t\tstrbuf_release(&gitdir);\n \n@@ -2256,7 +2237,7 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+static int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n {\n \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n \tchar *p;\n@@ -2355,9 +2336,6 @@ static void relocate_single_git_dir_into_superproject(const char *path,\n \t\tdie(_(\"could not lookup name for submodule '%s'\"), path);\n \n \tsubmodule_name_to_gitdir(&new_gitdir, the_repository, sub->name);\n-\tif (validate_submodule_git_dir(new_gitdir.buf, sub->name) < 0)\n-\t\tdie(_(\"refusing to move '%s' into an existing git dir\"),\n-\t\t    real_old_git_dir);\n \tif (safe_create_leading_directories_const(the_repository, new_gitdir.buf) < 0)\n \t\tdie(_(\"could not create directory '%s'\"), new_gitdir.buf);\n \treal_new_git_dir = real_pathdup(new_gitdir.buf, 1);\n@@ -2606,4 +2584,8 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t */\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n+\n+\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n+\t\t      \"git dir\"), buf->buf);\n }\ndiff --git a/submodule.h b/submodule.h\nindex b10e16e6c0..0b7692bc20 100644\n--- a/submodule.h\n+++ b/submodule.h\n@@ -137,11 +137,6 @@ int submodule_to_gitdir(struct repository *repo,\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name);\n \n-/*\n- * Make sure that no submodule's git dir is nested in a sibling submodule's.\n- */\n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name);\n-\n /*\n  * Make sure that the given submodule path does not follow symlinks.\n  */\n-- \n2.51.0\n\n"},{"id":"531013","messageId":"20251119211030.2008441-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251119211030.2008441-1-adrian.ratiu@collabora.com","subject":"[PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-19T21:10:27Z","receivedAt":"2025-11-19T21:11:53Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a submoduleEncoding extension which fixes filesystem collisions by\nencoding gitdir paths. At a high level, this implements a mechanism to\nencode -> validate -> retry until a working gitdir path is found.\n\nCredit goes to Junio for coming up with this design: encoding is only\napplied when necessary, e.g. uppercase characters are encoded only on\ncase-folding filesystems and only if a real conflict is detected.\n\nTo make this work, we rely on the submodule.<name>.gitdir config as the\nsingle source of truth for gitidir paths: the config is always set when\nthe extension is enabled. Users who care about gitdir paths are expected\nto get/set the config and not the underlying encoding implementation.\n\nThis commit adds the basic encoding logic which addresses nested gitdirs.\nThe next commit fixes case-folding, the next commit fixes names longer\nthan NAME_MAX. The idea is the encoding can be improved over time in a\nway which is transparent to users.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nBased-on-patch-by: Brandon Williams <bwilliams.eng@gmail.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc  |   6 +\n Documentation/config/submodule.adoc   |   5 +\n builtin/submodule--helper.c           |  17 +++\n repository.c                          |   1 +\n repository.h                          |   1 +\n setup.c                               |   7 ++\n setup.h                               |   1 +\n submodule.c                           | 117 ++++++++++++++----\n t/lib-verify-submodule-gitdir-path.sh |  24 ++++\n t/meson.build                         |   1 +\n t/t7425-submodule-encoding.sh         | 164 ++++++++++++++++++++++++++\n t/t9902-completion.sh                 |   1 +\n 12 files changed, 323 insertions(+), 22 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-encoding.sh\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 532456644b..4861d01894 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -73,6 +73,12 @@ relativeWorktrees:::\n \trepaired with either the `--relative-paths` option or with the\n \t`worktree.useRelativePaths` config set to `true`.\n \n+submoduleEncoding:::\n+\tIf enabled, submodule gitdir paths are encoded to avoid filesystem\n+\tconflicts due to nested gitdirs, case insensitivity or other issues.\n+\tWhen enabled, the submodule.<name>.gitdir config is always set for\n+\tall submodules and is the single point of authority for gitdir paths.\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/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 0672d99117..ddaadc3dc5 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -52,6 +52,11 @@ submodule.<name>.active::\n \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n \tdetails.\n \n+submodule.<name>.gitdir::\n+\tThis option sets the gitdir path for submodule <name>, allowing users to\n+\toverride the default path. Only works when `extensions.submoduleEncoding`\n+\tis enabled, otherwise does nothing. See linkgit:git-config[1] for details.\n+\n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\n \tsubmodule's path to determine if the submodule is of interest to git\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 9914ca0786..72440121a8 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1208,6 +1208,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3570,6 +3586,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\ndiff --git a/repository.c b/repository.c\nindex 6faf5c7398..26a21c0d71 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -288,6 +288,7 @@ int repo_init(struct repository *repo,\n \trepo->repository_format_worktree_config = format.worktree_config;\n \trepo->repository_format_relative_worktrees = format.relative_worktrees;\n \trepo->repository_format_precious_objects = format.precious_objects;\n+\trepo->repository_format_submodule_encoding = format.submodule_encoding;\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 5808a5d610..7e39b2acf7 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -158,6 +158,7 @@ struct repository {\n \tint repository_format_worktree_config;\n \tint repository_format_relative_worktrees;\n \tint repository_format_precious_objects;\n+\tint repository_format_submodule_encoding;\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 7086741e6c..bf6e815105 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -687,6 +687,9 @@ static enum extension_result handle_extension(const char *var,\n \t} else if (!strcmp(ext, \"relativeworktrees\")) {\n \t\tdata->relative_worktrees = git_config_bool(var, value);\n \t\treturn EXTENSION_OK;\n+\t} else if (!strcmp(ext, \"submoduleencoding\")) {\n+\t\tdata->submodule_encoding = git_config_bool(var, value);\n+\t\treturn EXTENSION_OK;\n \t}\n \treturn EXTENSION_UNKNOWN;\n }\n@@ -1865,6 +1868,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\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\tthe_repository->repository_format_submodule_encoding =\n+\t\t\t\trepo_fmt.submodule_encoding;\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@@ -1963,6 +1968,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_submodule_encoding =\n+\t\tfmt->submodule_encoding;\n \tthe_repository->repository_format_relative_worktrees =\n \t\tfmt->relative_worktrees;\n \tthe_repository->repository_format_partial_clone =\ndiff --git a/setup.h b/setup.h\nindex 8522fa8575..66ec1ceba5 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -130,6 +130,7 @@ struct repository_format {\n \tchar *partial_clone; /* value of extensions.partialclone */\n \tint worktree_config;\n \tint relative_worktrees;\n+\tint submodule_encoding;\n \tint is_bare;\n \tint hash_algo;\n \tint compat_hash_algo;\ndiff --git a/submodule.c b/submodule.c\nindex 8ef028f26b..07cb4694cf 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -31,6 +31,7 @@\n #include \"commit-reach.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"url.h\"\n \n static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n static int initialized_fetch_ref_tips;\n@@ -2237,12 +2238,43 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n+/*\n+ * Encoded gitdir validation function used when extensions.submoduleEncoding is enabled.\n+ * This does not print errors like the non-encoded version, because encoding is supposed\n+ * to mitigate / fix all these.\n+ */\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+{\n+\tconst char *modules_marker = \"/modules/\";\n+\tchar *p = git_dir, *last_submodule_name = NULL;\n+\n+\tif (!the_repository->repository_format_submodule_encoding)\n+\t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n+\t\t    \"extensions.submoduleEncoding enabled.\");\n+\n+\t/* Find the last submodule name in the gitdir path (modules can be nested). */\n+\twhile ((p = strstr(p, modules_marker))) {\n+\t\tlast_submodule_name = p + strlen(modules_marker);\n+\t\tp++;\n+\t}\n+\n+\t/* Prevent the use of '/' in encoded names */\n+\tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n static int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n {\n \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n \tchar *p;\n \tint ret = 0;\n \n+\tif (the_repository->repository_format_submodule_encoding)\n+\t\tBUG(\"validate_submodule_git_dir() must be called with \"\n+\t\t    \"extensions.submoduleEncoding disabled.\");\n+\n \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n \t    strcmp(p, submodule_name))\n \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n@@ -2559,33 +2591,74 @@ int submodule_to_gitdir(struct repository *repo,\n \treturn ret;\n }\n \n+static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n+\t\t\t\t\t     const char *submodule_name)\n+{\n+\tchar *key;\n+\n+\tif (validate_submodule_encoded_git_dir(gitdir_path->buf, submodule_name))\n+\t\treturn -1;\n+\n+\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\trepo_config_set_gently(the_repository, key, gitdir_path->buf);\n+\tFREE_AND_NULL(key);\n+\n+\treturn 0;\n+}\n+\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n+\tconst char *gitdir;\n+\tchar *key;\n+\n+\trepo_git_path_append(r, buf, \"modules/\");\n+\tstrbuf_addstr(buf, submodule_name);\n+\n+\t/* If extensions.submoduleEncoding is disabled, use the plain path set above */\n+\tif (!r->repository_format_submodule_encoding) {\n+\t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n+\t\t\t      \"git dir\"), buf->buf);\n+\n+\t\treturn; /* plain gitdir is valid for use */\n+\t}\n+\n+\t/* Extension is enabled: use the gitdir config if it exists */\n+\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\tif (!repo_config_get_string_tmp(r, key, &gitdir)) {\n+\t\tstrbuf_reset(buf);\n+\t\tstrbuf_addstr(buf, gitdir);\n+\t\tFREE_AND_NULL(key);\n+\n+\t\t/* validate because users might have modified the config */\n+\t\tif (validate_submodule_encoded_git_dir(buf->buf, submodule_name))\n+\t\t\tdie(_(\"Invalid 'submodule.%s.gitdir' config: '%s' please check \"\n+\t\t\t      \"if it is unique or conflicts with another module\"),\n+\t\t\t    submodule_name, gitdir);\n+\n+\t\treturn;\n+\t}\n+\tFREE_AND_NULL(key);\n+\n \t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n+\t * The gitdir config does not exist, even though the extension is enabled.\n+\t * Therefore we are in one of the following cases:\n \t */\n+\n+\t/* Case 1: legacy migration of valid plain submodule names */\n+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\treturn;\n+\n+\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\tstrbuf_reset(buf);\n \trepo_git_path_append(r, buf, \"modules/\");\n-\tstrbuf_addstr(buf, submodule_name);\n+\tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\treturn;\n \n-\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), buf->buf);\n+\t/* Case 3: Nothing worked: error out */\n+\tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n+\t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\n+\t    submodule_name, submodule_name);\n }\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..62794df976\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,24 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n+\n+# This does not check filesystem existence. That is done in submodule.c via the\n+# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n+# might or might not exist (e.g. when adding a new submodule), so this only\n+# checks the expected configuration path, which might be overridden by the user.\n+\n+verify_submodule_gitdir_path() {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\t# Compute expected absolute path\n+\t\texpected=\"$(git rev-parse --git-common-dir)/$path\" &&\n+\t\texpected=\"$(test-tool path-utils real_path \"$expected\")\" &&\n+\t\t# Compute actual absolute path\n+\t\tactual=\"$(git submodule--helper gitdir \"$name\")\" &&\n+\t\tactual=\"$(test-tool path-utils real_path \"$actual\")\" &&\n+\t\techo \"$expected\" >expect &&\n+\t\techo \"$actual\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/meson.build b/t/meson.build\nindex a5531df415..4187b35aee 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -884,6 +884,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-encoding.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-encoding.sh b/t/t7425-submodule-encoding.sh\nnew file mode 100755\nindex 0000000000..f877887549\n--- /dev/null\n+++ b/t/t7425-submodule-encoding.sh\n@@ -0,0 +1,164 @@\n+#!/bin/sh\n+\n+test_description='submodules handle mixed legacy and new (encoded) style gitdir paths'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+\tgit config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed encoded and non-encoded submodules' '\n+\tgit init -b main legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init -b main new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init -b main main &&\n+\t(\n+\t\tcd main &&\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\t# trigger the \"die_path_inside_submodule\" check\n+\t\ttest_must_fail git submodule add ../new-sub \"legacy/nested\" &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submoduleEncoding true &&\n+\n+\t\tgit submodule add ../new-sub \"New Sub\" &&\n+\t\ttest_commit new &&\n+\n+\t\t# retrigger the \"die_path_inside_submodule\" check with encoding\n+\t\ttest_must_fail git submodule add ../new-sub \"New Sub/nested2\"\n+\t)\n+'\n+\n+test_expect_success 'verify submodule name is properly encoded' '\n+\tverify_submodule_gitdir_path main legacy modules/legacy &&\n+\tverify_submodule_gitdir_path main \"New Sub\" \"modules/New Sub\"\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned-non-encoding &&\n+\t(\n+\t\tcd cloned-non-encoding &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/\"New Sub\" &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t) &&\n+\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules main cloned-encoding &&\n+\t(\n+\t\tcd cloned-encoding &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to encoded submodules' '\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C main config receive.denyCurrentBranch updateInstead &&\n+\t(\n+\t\tcd cloned-encoding &&\n+\n+\t\tgit -C legacy switch --track -C main origin/main  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C \"New Sub\" switch --track -C main origin/main &&\n+\t\ttest_commit -C \"New Sub\" second-commit &&\n+\t\tgit -C \"New Sub\" push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C main origin/main  &&\n+\t\tgit add legacy \"New Sub\" &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of encoded sibling submodules must not be nested' '\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules nested clone_nested &&\n+\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone -c extensions.submoduleEncoding=true --recurse-submodules --jobs=2 nested clone_parallel &&\n+\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n+\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks\n+'\n+\n+test_expect_success 'disabling extensions.submoduleEncoding prevents nested submodules' '\n+\t(\n+\t\tcd clone_nested &&\n+\t\t# disable extension and verify failure\n+\t\tgit config extensions.submoduleEncoding false &&\n+\t\ttest_must_fail git submodule add ./thing2 hippo/foobar &&\n+\t\t# re-enable extension and verify it works\n+\t\tgit config extensions.submoduleEncoding true &&\n+\t\tgit submodule add ./thing2 hippo/foobar\n+\t)\n+'\n+\n+test_done\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 964e1f1569..ffb9c8b522 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n \tsubmodule.sub.fetchRecurseSubmodules Z\n \tsubmodule.sub.ignore Z\n \tsubmodule.sub.active Z\n+\tsubmodule.sub.gitdir Z\n \tEOF\n '\n \n-- \n2.51.0\n\n"},{"id":"531014","messageId":"20251119211030.2008441-6-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251119211030.2008441-1-adrian.ratiu@collabora.com","subject":"[PATCH v5 5/7] submodule: fix case-folding gitdir filesystem colisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-19T21:10:28Z","receivedAt":"2025-11-19T21:11:56Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new check when extension.submoduleEncoding is enabled to\ndetect and prevent case-folding filesystem colisions. When this\nnew check is triggered, a stricter casefolding aware URI encoding\nis used to percent-encode uppercase characters.\n\nBy using this check/retry mechanism the uppercase encoding is\nonly applied when necessary, so case-sensitive filesystems are\nnot affected.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n submodule.c                   | 79 ++++++++++++++++++++++++++++++++++-\n t/t7425-submodule-encoding.sh | 35 ++++++++++++++++\n url.c                         | 12 ++++++\n url.h                         |  1 +\n 4 files changed, 125 insertions(+), 2 deletions(-)\n\ndiff --git a/submodule.c b/submodule.c\nindex 07cb4694cf..b3f74f7e3c 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2238,15 +2238,58 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n+static int check_casefolding_conflict(const char *git_dir,\n+\t\t\t\t      const char *submodule_name,\n+\t\t\t\t      const bool suffixes_match)\n+{\n+\tchar *p, *modules_dir = xstrdup(git_dir);\n+\tstruct dirent *de;\n+\tDIR *dir = NULL;\n+\tint ret = 0;\n+\n+\tif ((p = find_last_dir_sep(modules_dir)))\n+\t\t*p = '\\0';\n+\n+\t/* No conflict is possible if modules_dir doesn't exist (first clone) */\n+\tif (!is_directory(modules_dir))\n+\t\tgoto cleanup;\n+\n+\tdir = opendir(modules_dir);\n+\tif (!dir) {\n+\t\tret = -1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/* Check for another directory under .git/modules that differs only in case. */\n+\twhile ((de = readdir(dir))) {\n+\t\tif (!strcmp(de->d_name, \".\") || !strcmp(de->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tif ((suffixes_match || is_git_directory(git_dir)) &&\n+\t\t    !strcasecmp(de->d_name, submodule_name) &&\n+\t\t    strcmp(de->d_name, submodule_name)) {\n+\t\t\tret = -1; /* collision found */\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+cleanup:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tFREE_AND_NULL(modules_dir);\n+\treturn ret;\n+}\n+\n /*\n  * Encoded gitdir validation function used when extensions.submoduleEncoding is enabled.\n  * This does not print errors like the non-encoded version, because encoding is supposed\n  * to mitigate / fix all these.\n  */\n-static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name)\n {\n \tconst char *modules_marker = \"/modules/\";\n \tchar *p = git_dir, *last_submodule_name = NULL;\n+\tint config_ignorecase = 0;\n \n \tif (!the_repository->repository_format_submodule_encoding)\n \t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n@@ -2262,6 +2305,14 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n \t\treturn -1;\n \n+\t/* Prevent conflicts on case-folding filesystems */\n+\trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n+\tif (ignore_case || config_ignorecase) {\n+\t\tbool suffixes_match = !strcmp(last_submodule_name, submodule_name);\n+\t\treturn check_casefolding_conflict(git_dir, submodule_name,\n+\t\t\t\t\t\t  suffixes_match);\n+\t}\n+\n \treturn 0;\n }\n \n@@ -2650,13 +2701,37 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n \t\treturn;\n \n-\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\t/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n \tstrbuf_reset(buf);\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n \tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n \t\treturn;\n \n+\t/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */\n+\tstrbuf_reset(buf);\n+\trepo_git_path_append(r, buf, \"modules/\");\n+\tstrbuf_addstr_urlencode(buf, submodule_name, is_casefolding_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\treturn;\n+\n+\t/* Case 2.3: Try some derived gitdir names, see if one sticks */\n+\tfor (char c = '0'; c <= '9'; c++) {\n+\t\tstrbuf_reset(buf);\n+\t\trepo_git_path_append(r, buf, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n+\t\tstrbuf_addch(buf, c);\n+\t\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\t\treturn;\n+\n+\t\tstrbuf_reset(buf);\n+\t\trepo_git_path_append(r, buf, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(buf, submodule_name, is_casefolding_rfc3986_unreserved);\n+\t\tstrbuf_addch(buf, c);\n+\t\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\t\treturn;\n+\t}\n+\n \t/* Case 3: Nothing worked: error out */\n \tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n \t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\ndiff --git a/t/t7425-submodule-encoding.sh b/t/t7425-submodule-encoding.sh\nindex f877887549..093238939a 100755\n--- a/t/t7425-submodule-encoding.sh\n+++ b/t/t7425-submodule-encoding.sh\n@@ -161,4 +161,39 @@ test_expect_success 'disabling extensions.submoduleEncoding prevents nested subm\n \t)\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are correctly encoded' '\n+\tgit clone -c extensions.submoduleEncoding=true main cloned-folding &&\n+\t(\n+\t\tcd cloned-folding &&\n+\n+\t\t# conflict: the \"folding\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"folding\" &&\n+\t\ttest_commit lowercase &&\n+\t\tgit submodule add ../new-sub \"FoldinG\" &&\n+\t\ttest_commit uppercase &&\n+\n+\t\t# conflict: the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"FOO\" &&\n+\t\ttest_commit uppercase-foo &&\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\ttest_commit lowercase-foo &&\n+\n+\t\t# create a multi conflict between foobar, fooBar and foo%42ar\n+\t\t# the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"foobar\" &&\n+\t\ttest_commit lowercase-foobar &&\n+\t\tgit submodule add ../new-sub \"foo%42ar\" &&\n+\t\ttest_commit encoded-foo%42ar &&\n+\t\tgit submodule add ../new-sub \"fooBar\" &&\n+\t\ttest_commit mixed-fooBar\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-folding \"folding\" \"modules/folding\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FoldinG\" \"modules/%46oldin%47\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FOO\" \"modules/FOO\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foobar\" \"modules/foobar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo%42ar\" \"modules/foo%42ar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n+'\n+\n test_done\ndiff --git a/url.c b/url.c\nindex 0fb1859b28..057e6e5c6e 100644\n--- a/url.c\n+++ b/url.c\n@@ -14,6 +14,18 @@ int is_rfc3986_unreserved(char ch)\n \t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n }\n \n+/*\n+ * This is a variant of is_rfc3986_unreserved() that treats uppercase\n+ * letters as \"reserved\". This forces them to be percent-encoded, allowing\n+ * 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.\n+ */\n+int is_casefolding_rfc3986_unreserved(char c)\n+{\n+\treturn (c >= 'a' && c <= 'z') ||\n+\t       (c >= '0' && c <= '9') ||\n+\t       c == '-' || c == '.' || c == '_' || c == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex 131a262066..92e3c63514 100644\n--- a/url.h\n+++ b/url.h\n@@ -22,5 +22,6 @@ void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n int is_rfc3986_unreserved(char ch);\n+int is_casefolding_rfc3986_unreserved(char c);\n \n #endif /* URL_H */\n-- \n2.51.0\n\n"},{"id":"531015","messageId":"20251119211030.2008441-7-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251119211030.2008441-1-adrian.ratiu@collabora.com","subject":"[PATCH v5 6/7] submodule: use hashed name for gitdir","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-19T21:10:29Z","receivedAt":"2025-11-19T21:11:57Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"If none of the previous steps work and we reach case 2.4, try to\nhash the submodule name and see if that can be a valid gitdir\nbefore giving up and throwing an error.\n\nThis is a \"last resort\" type of measure to avoid conflicts since\nit loses the gitdir human readability. Itis not such a big deal\nbecause users are now supposed to use the submodule.<name>.gitdir\nconfig as the single source of truth for gitdir paths.\n\nThis logic will be reached in very rare cases, as can be seen in\nthe test we added.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n submodule.c                   | 20 +++++++++++-\n t/t7425-submodule-encoding.sh | 59 +++++++++++++++++++++++++++++++++++\n 2 files changed, 78 insertions(+), 1 deletion(-)\n\ndiff --git a/submodule.c b/submodule.c\nindex b3f74f7e3c..2c0df96d55 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2660,8 +2660,12 @@ static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n+\tunsigned char raw_name_hash[GIT_MAX_RAWSZ];\n+\tchar hex_name_hash[GIT_MAX_HEXSZ + 1];\n+\tstruct git_hash_ctx ctx;\n \tconst char *gitdir;\n-\tchar *key;\n+\tchar *key, header[128];\n+\tint header_len;\n \n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n@@ -2732,6 +2736,20 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\treturn;\n \t}\n \n+\t/* Case 2.4: If all the above failed, try a hash of the name as a last resort */\n+\theader_len = snprintf(header, sizeof(header), \"blob %zu\", strlen(submodule_name));\n+\tthe_hash_algo->init_fn(&ctx);\n+\tthe_hash_algo->update_fn(&ctx, header, header_len);\n+\tthe_hash_algo->update_fn(&ctx, \"\\0\", 1);\n+\tthe_hash_algo->update_fn(&ctx, submodule_name, strlen(submodule_name));\n+\tthe_hash_algo->final_fn(raw_name_hash, &ctx);\n+\thash_to_hex_algop_r(hex_name_hash, raw_name_hash, the_hash_algo);\n+\tstrbuf_reset(buf);\n+\trepo_git_path_append(r, buf, \"modules/\");\n+\tstrbuf_addstr(buf, hex_name_hash);\n+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n+\t\treturn;\n+\n \t/* Case 3: Nothing worked: error out */\n \tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n \t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\ndiff --git a/t/t7425-submodule-encoding.sh b/t/t7425-submodule-encoding.sh\nindex 093238939a..10703b34c8 100755\n--- a/t/t7425-submodule-encoding.sh\n+++ b/t/t7425-submodule-encoding.sh\n@@ -196,4 +196,63 @@ test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are corre\n \tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a last resort' '\n+\tgit clone -c extensions.submoduleEncoding=true main cloned-hash &&\n+\t(\n+\t\tcd cloned-hash &&\n+\n+\t\t# conflict: add all submodule conflicting variants until we reach the\n+\t\t# final hashing conflict resolution for submodule \"foo\"\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\tgit submodule add ../new-sub \"foo0\" &&\n+\t\tgit submodule add ../new-sub \"foo1\" &&\n+\t\tgit submodule add ../new-sub \"foo2\" &&\n+\t\tgit submodule add ../new-sub \"foo3\" &&\n+\t\tgit submodule add ../new-sub \"foo4\" &&\n+\t\tgit submodule add ../new-sub \"foo5\" &&\n+\t\tgit submodule add ../new-sub \"foo6\" &&\n+\t\tgit submodule add ../new-sub \"foo7\" &&\n+\t\tgit submodule add ../new-sub \"foo8\" &&\n+\t\tgit submodule add ../new-sub \"foo9\" &&\n+\t\tgit submodule add ../new-sub \"%46oo\" &&\n+\t\tgit submodule add ../new-sub \"%46oo0\" &&\n+\t\tgit submodule add ../new-sub \"%46oo1\" &&\n+\t\tgit submodule add ../new-sub \"%46oo2\" &&\n+\t\tgit submodule add ../new-sub \"%46oo3\" &&\n+\t\tgit submodule add ../new-sub \"%46oo4\" &&\n+\t\tgit submodule add ../new-sub \"%46oo5\" &&\n+\t\tgit submodule add ../new-sub \"%46oo6\" &&\n+\t\tgit submodule add ../new-sub \"%46oo7\" &&\n+\t\tgit submodule add ../new-sub \"%46oo8\" &&\n+\t\tgit submodule add ../new-sub \"%46oo9\" &&\n+\t\ttest_commit add-foo-variants &&\n+\t\tgit submodule add ../new-sub \"Foo\" &&\n+\t\ttest_commit add-uppercase-foo\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo\" \"modules/foo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo0\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo1\" \"modules/foo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo2\" \"modules/foo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo3\" \"modules/foo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo4\" \"modules/foo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo5\" \"modules/foo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo6\" \"modules/foo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo7\" \"modules/foo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo8\" \"modules/foo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo9\" \"modules/foo9\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo\" \"modules/%46oo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo0\" \"modules/%46oo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo1\" \"modules/%46oo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo2\" \"modules/%46oo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo3\" \"modules/%46oo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo4\" \"modules/%46oo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo5\" \"modules/%46oo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo6\" \"modules/%46oo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo7\" \"modules/%46oo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo8\" \"modules/%46oo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo9\" \"modules/%46oo9\" &&\n+\thash=$(printf \"Foo\" | git hash-object --stdin) &&\n+\tverify_submodule_gitdir_path cloned-hash \"Foo\" \"modules/${hash}\"\n+'\n+\n test_done\n-- \n2.51.0\n\n"},{"id":"531016","messageId":"20251119211030.2008441-8-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251119211030.2008441-1-adrian.ratiu@collabora.com","subject":"[PATCH v5 7/7] meson/Makefile: allow setting submodule encoding at build time","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-11-19T21:10:30Z","receivedAt":"2025-11-19T21:11:57Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Some users find it difficult to distribute repo config changes for\nenabling extensions.submoduleEncoding, or to enable it by passing\nthe config via cmdline, so we add a build-time option which can\nenable the extension for convenience.\n\nIt is still disabled by default and the build-time default is\noverridden by the repo-specific configs.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc |  2 ++\n Makefile                             |  5 +++++\n configure.ac                         | 23 +++++++++++++++++++++++\n meson.build                          |  4 ++++\n meson_options.txt                    |  2 ++\n setup.c                              |  8 ++++++++\n 6 files changed, 44 insertions(+)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 4861d01894..436f7fb52e 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -78,6 +78,8 @@ submoduleEncoding:::\n \tconflicts due to nested gitdirs, case insensitivity or other issues.\n \tWhen enabled, the submodule.<name>.gitdir config is always set for\n \tall submodules and is the single point of authority for gitdir paths.\n+\tCan also be enabled via the submodule-encoding build option. The repo\n+\tconfig takes precedence over the build-time default.\n \n worktreeConfig:::\n \tIf enabled, then worktrees will load config settings from the\ndiff --git a/Makefile b/Makefile\nindex 7e0f77e298..4c7bf75c68 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -2344,6 +2344,11 @@ ifdef INCLUDE_LIBGIT_RS\n \tBASIC_CFLAGS += -fvisibility=hidden\n endif\n \n+ifdef SUBMODULE_ENCODING_BY_DEFAULT\n+\t# Set submoduleEncoding extension default specified at build time\n+\tBASIC_CFLAGS += -DSUBMODULE_ENCODING_BY_DEFAULT=$(SUBMODULE_ENCODING_BY_DEFAULT)\n+endif\n+\n ifeq ($(TCLTK_PATH),)\n NO_TCLTK = NoThanks\n endif\ndiff --git a/configure.ac b/configure.ac\nindex cfb50112bf..202b1e309b 100644\n--- a/configure.ac\n+++ b/configure.ac\n@@ -229,6 +229,29 @@ AC_ARG_ENABLE([cssmin],\n   GIT_CONF_SUBST([CSSMIN])\n ])\n \n+# Define option to enable the submodule encoding extension by default\n+AC_ARG_ENABLE([submodule-encoding],\n+ [AS_HELP_STRING([--enable-submodule-encoding],\n+  [Enable the submoduleEncoding extension by default at build time.]\n+  [--disable-submodule-encoding will keep the current default (disabled).])],\n+[\n+case \"$enableval\" in\n+  yes) SUBMODULE_ENCODING_BY_DEFAULT=1\n+       AC_MSG_NOTICE([Submodule encoding will be enabled by default.])\n+       ;;\n+  no)  SUBMODULE_ENCODING_BY_DEFAULT=0\n+       AC_MSG_NOTICE([Submodule encoding will not be enabled by default.])\n+       ;;\n+  *)   AC_MSG_ERROR([--enable-submodule-encoding takes yes or no.])\n+       ;;\n+esac\n+],\n+[\n+  SUBMODULE_ENCODING_BY_DEFAULT=0\n+])\n+\n+GIT_CONF_SUBST([SUBMODULE_ENCODING_BY_DEFAULT])\n+\n ## Site configuration (override autodetection)\n ## --with-PACKAGE[=ARG] and --without-PACKAGE\n AC_MSG_NOTICE([CHECKS for site configuration])\ndiff --git a/meson.build b/meson.build\nindex 1f95a06edb..992d433efd 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -945,6 +945,10 @@ else\n   build_options_config.set('NO_PERL_CPAN_FALLBACKS', '')\n endif\n \n+submodule_encoding_by_default = get_option('submodule-encoding').to_int()\n+libgit_c_args += '-DSUBMODULE_ENCODING_BY_DEFAULT=' + submodule_encoding_by_default.to_string()\n+build_options_config.set('SUBMODULE_ENCODING_BY_DEFAULT', submodule_encoding_by_default)\n+\n zlib_backend = get_option('zlib_backend')\n if zlib_backend in ['auto', 'zlib-ng']\n   zlib_ng = dependency('zlib-ng', required: zlib_backend == 'zlib-ng')\ndiff --git a/meson_options.txt b/meson_options.txt\nindex e0be260ae1..d9d569a166 100644\n--- a/meson_options.txt\n+++ b/meson_options.txt\n@@ -21,6 +21,8 @@ option('runtime_prefix', type: 'boolean', value: false,\n   description: 'Resolve ancillary tooling and support files relative to the location of the runtime binary instead of hard-coding them into the binary.')\n option('sane_tool_path', type: 'array', value: [],\n   description: 'An array of paths to pick up tools from in case the normal tools are broken or lacking.')\n+option('submodule-encoding', type: 'boolean', value: false,\n+  description: 'Enable submoduleEncoding extension by default at build time.')\n \n # Build information compiled into Git and other parts like documentation.\n option('build_date', type: 'string', value: '',\ndiff --git a/setup.c b/setup.c\nindex bf6e815105..de2d4e7238 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -1747,6 +1747,14 @@ const char *setup_git_directory_gently(int *nongit_ok)\n \t */\n \trepo_config_clear(the_repository);\n \n+\t/*\n+\t * Set build-time default for submodule encoding.\n+\t * This can be overridden by the repository's config.\n+\t */\n+#ifdef SUBMODULE_ENCODING_BY_DEFAULT\n+\trepo_fmt.submodule_encoding = SUBMODULE_ENCODING_BY_DEFAULT;\n+#endif\n+\n \t/*\n \t * Let's assume that we are in a git repository.\n \t * If it turns out later that we are somewhere else, the value will be\n-- \n2.51.0\n\n"},{"id":"531693","messageId":"aTLNLnF_ZLpMnso-@pks.im","threadId":"63967","inReplyTo":"20251119211030.2008441-3-adrian.ratiu@collabora.com","subject":"Re: [PATCH v5 2/7] builtin/credential-store: move is_rfc3986_unreserved to url.[ch]","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-05T12:16:46Z","receivedAt":"2025-12-05T12:17:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:10:25PM +0200, Adrian Ratiu wrote:\n> is_rfc3986_unreserved() was moved to credential-store.c and was made\n> static by f89854362c (credential-store: move related functions to\n> credential-store file, 2023-06-06) under a correct assumption, at the\n> time, that it was the only place using it.\n> \n> However now we need it to apply URL-encoding to submodule names when\n> constructing gitdir paths, to avoid conflicts, so bring it back as a\n> public function exposed via url.h, instead of the old helper path\n> (strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n> \n> This function will be used by submodule.c in the next commit.\n\nNit: this statement isn't true anymore :)\n\n> diff --git a/url.c b/url.c\n> index 282b12495a..0fb1859b28 100644\n> --- a/url.c\n> +++ b/url.c\n> @@ -3,6 +3,17 @@\n>  #include \"strbuf.h\"\n>  #include \"url.h\"\n>  \n> +/*\n> + * The set of unreserved characters as per STD66 (RFC3986) is\n> + * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n> + * components without percent-encoding.\n> + */\n> +int is_rfc3986_unreserved(char ch)\n> +{\n> +\treturn isalnum(ch) ||\n> +\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n> +}\n> +\n>  int is_urlschemechar(int first_flag, int ch)\n>  {\n>  \t/*\n\nNit: the function documentation should rather live in the header file.\n\nPatrick\n"},{"id":"531694","messageId":"aTLNPKlDnsNzyZkC@pks.im","threadId":"63967","inReplyTo":"20251119211030.2008441-4-adrian.ratiu@collabora.com","subject":"Re: [PATCH v5 3/7] submodule: always validate gitdirs inside submodule_name_to_gitdir","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-05T12:17:00Z","receivedAt":"2025-12-05T12:17:07Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:10:26PM +0200, Adrian Ratiu wrote:\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index 2873b2780e..9914ca0786 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -1780,23 +1776,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n>  \t\tfree(path);\n>  \t}\n>  \n> -\t/*\n> -\t * We already performed this check at the beginning of this function,\n> -\t * before cloning the objects. This tries to detect racy behavior e.g.\n> -\t * in parallel clones, where another process could easily have made the\n> -\t * gitdir nested _after_ it was created.\n> -\t *\n> -\t * To prevent further harm coming from this unintentionally-nested\n> -\t * gitdir, let's disable it by deleting the `HEAD` file.\n> -\t */\n> -\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {\n> -\t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n> -\t\tunlink(head);\n> -\t\tfree(head);\n> -\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n> -\t\t      \"git dir\"), sm_gitdir);\n> -\t}\n> -\n>  \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n>  \n>  \tp = repo_submodule_path(the_repository, clone_data_path, \"config\");\n\nHm. This one is a bit puzzling to me. This seems to explicitly be a\ncheck about a TOCTOU-style race, where a concurrent process might have\ncreated the parent repository after our initial validation of the path.\nWe don't call `submodule_name_to_gitdir()` inbetween those two calls\nthough, so why is this not a concern anymore with the unified API?\n\n> diff --git a/submodule.c b/submodule.c\n> index 35c55155f7..8ef028f26b 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2153,30 +2153,11 @@ int submodule_move_head(const char *path, const char *super_prefix,\n>  \n>  \tif (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {\n>  \t\tif (old_head) {\n> -\t\t\tif (!submodule_uses_gitfile(path))\n> -\t\t\t\tabsorb_git_dir_into_superproject(path,\n> -\t\t\t\t\t\t\t\t super_prefix);\n> -\t\t\telse {\n> -\t\t\t\tchar *dotgit = xstrfmt(\"%s/.git\", path);\n> -\t\t\t\tchar *git_dir = xstrdup(read_gitfile(dotgit));\n> -\n> -\t\t\t\tfree(dotgit);\n> -\t\t\t\tif (validate_submodule_git_dir(git_dir,\n> -\t\t\t\t\t\t\t       sub->name) < 0)\n> -\t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n> -\t\t\t\t\t      \"another submodule's git dir\"),\n> -\t\t\t\t\t    git_dir);\n> -\t\t\t\tfree(git_dir);\n> -\t\t\t}\n> +\t\t\tabsorb_git_dir_into_superproject(path, super_prefix);\n>  \t\t} else {\n>  \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n>  \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n>  \t\t\t\t\t\t sub->name);\n> -\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n> -\t\t\t\t\t\t       sub->name) < 0)\n> -\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n> -\t\t\t\t      \"submodule's git dir\"),\n> -\t\t\t\t    gitdir.buf);\n>  \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n>  \t\t\tstrbuf_release(&gitdir);\n\nThe second case here makes sense to me, as we do call\n`submodule_name_to_gitdir()`. But in the first branch of the condition\nwe retrieve the path directly, so we're not guarded by the validation\nanymore, are we?\n\nPatrick\n"},{"id":"531695","messageId":"aTLNwHpLUcy-WsZs@pks.im","threadId":"63967","inReplyTo":"20251119211030.2008441-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-05T12:19:12Z","receivedAt":"2025-12-05T12:19:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:10:27PM +0200, Adrian Ratiu wrote:\n> Add a submoduleEncoding extension which fixes filesystem collisions by\n> encoding gitdir paths. At a high level, this implements a mechanism to\n> encode -> validate -> retry until a working gitdir path is found.\n> \n> Credit goes to Junio for coming up with this design: encoding is only\n> applied when necessary, e.g. uppercase characters are encoded only on\n> case-folding filesystems and only if a real conflict is detected.\n> \n> To make this work, we rely on the submodule.<name>.gitdir config as the\n> single source of truth for gitidir paths: the config is always set when\n\ns/gitidir/gitdir/\n\n> diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\n> index 532456644b..4861d01894 100644\n> --- a/Documentation/config/extensions.adoc\n> +++ b/Documentation/config/extensions.adoc\n> @@ -73,6 +73,12 @@ relativeWorktrees:::\n>  \trepaired with either the `--relative-paths` option or with the\n>  \t`worktree.useRelativePaths` config set to `true`.\n>  \n> +submoduleEncoding:::\n> +\tIf enabled, submodule gitdir paths are encoded to avoid filesystem\n> +\tconflicts due to nested gitdirs, case insensitivity or other issues.\n> +\tWhen enabled, the submodule.<name>.gitdir config is always set for\n> +\tall submodules and is the single point of authority for gitdir paths.\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\n\nI think the fact that the submodule gitdir paths are encoded now is\nsecondary to this repository extension. The more important fact is that\nthis changes the source of truth where the submodule gitdir path is\nactually derived from: before it was derived on the fly, whereas now it\nis persisted in the gitconfig.\n\nIt follows that because the source of truth is now a persistent entry in\nthe configuration, other implementations can read it without having to\nunderstand how exactly the value was computed in the first place. So an\nimplementation may arbitrarily change the algorithm it uses to derive\nthat path from now on, and it doesn't necessarily have to encode\nanything.\n\nSo I'd propose to rename the extension and rephrase its description\naccordingly. It could for example be called something along the lines of\n\"submodulePathConfig\".\n\n> diff --git a/submodule.c b/submodule.c\n> index 8ef028f26b..07cb4694cf 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2559,33 +2591,74 @@ int submodule_to_gitdir(struct repository *repo,\n>  \treturn ret;\n>  }\n>  \n> +static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n> +\t\t\t\t\t     const char *submodule_name)\n> +{\n> +\tchar *key;\n> +\n> +\tif (validate_submodule_encoded_git_dir(gitdir_path->buf, submodule_name))\n> +\t\treturn -1;\n> +\n> +\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n> +\trepo_config_set_gently(the_repository, key, gitdir_path->buf);\n> +\tFREE_AND_NULL(key);\n\nI think a simple call to `free()` should be sufficient here. There is no\nrisk of it being used afterwards.\n\n> +\treturn 0;\n> +}\n> +\n>  void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \t\t\t      const char *submodule_name)\n>  {\n> +\tconst char *gitdir;\n> +\tchar *key;\n> +\n> +\trepo_git_path_append(r, buf, \"modules/\");\n> +\tstrbuf_addstr(buf, submodule_name);\n> +\n> +\t/* If extensions.submoduleEncoding is disabled, use the plain path set above */\n> +\tif (!r->repository_format_submodule_encoding) {\n> +\t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n> +\t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n> +\t\t\t      \"git dir\"), buf->buf);\n> +\n> +\t\treturn; /* plain gitdir is valid for use */\n> +\t}\n> +\n> +\t/* Extension is enabled: use the gitdir config if it exists */\n> +\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n> +\tif (!repo_config_get_string_tmp(r, key, &gitdir)) {\n> +\t\tstrbuf_reset(buf);\n> +\t\tstrbuf_addstr(buf, gitdir);\n> +\t\tFREE_AND_NULL(key);\n> +\n> +\t\t/* validate because users might have modified the config */\n> +\t\tif (validate_submodule_encoded_git_dir(buf->buf, submodule_name))\n> +\t\t\tdie(_(\"Invalid 'submodule.%s.gitdir' config: '%s' please check \"\n> +\t\t\t      \"if it is unique or conflicts with another module\"),\n\nNit: error messages start with a lower-case character.\n\n> +\t\t\t    submodule_name, gitdir);\n> +\n> +\t\treturn;\n> +\t}\n> +\tFREE_AND_NULL(key);\n> +\n>  \t/*\n> -\t * NEEDSWORK: The current way of mapping a submodule's name to\n> -\t * its location in .git/modules/ has problems with some naming\n> -\t * schemes. For example, if a submodule is named \"foo\" and\n> -\t * another is named \"foo/bar\" (whether present in the same\n> -\t * superproject commit or not - the problem will arise if both\n> -\t * superproject commits have been checked out at any point in\n> -\t * time), or if two submodule names only have different cases in\n> -\t * a case-insensitive filesystem.\n> -\t *\n> -\t * There are several solutions, including encoding the path in\n> -\t * some way, introducing a submodule.<name>.gitdir config in\n> -\t * .git/config (not .gitmodules) that allows overriding what the\n> -\t * gitdir of a submodule would be (and teach Git, upon noticing\n> -\t * a clash, to automatically determine a non-clashing name and\n> -\t * to write such a config), or introducing a\n> -\t * submodule.<name>.gitdir config in .gitmodules that repo\n> -\t * administrators can explicitly set. Nothing has been decided,\n> -\t * so for now, just append the name at the end of the path.\n> +\t * The gitdir config does not exist, even though the extension is enabled.\n> +\t * Therefore we are in one of the following cases:\n>  \t */\n> +\n> +\t/* Case 1: legacy migration of valid plain submodule names */\n> +\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n> +\t\treturn;\n> +\n> +\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n> +\tstrbuf_reset(buf);\n>  \trepo_git_path_append(r, buf, \"modules/\");\n> -\tstrbuf_addstr(buf, submodule_name);\n> +\tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n> +\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n> +\t\treturn;\n>  \n> -\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n> -\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n> -\t\t      \"git dir\"), buf->buf);\n> +\t/* Case 3: Nothing worked: error out */\n> +\tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n> +\t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\n> +\t    submodule_name, submodule_name);\n\nIt feels somewhat fragile to me that we unconditionally handle these\ncases and try to find old submodule directories. If the extension is\nenabled I'd expect that the submodule configuration is the _only_ source\nof truth.\n\nMay I propose that we instead always error out in case the submodule\nconfiguration does not exist? In the best case we'd then give the user a\nnice error message that tells them how to run the migration manually.\n\n(Coming back from reading subsequent patches) Maybe what's putting me\noff is that this function is seemingly used for two things:\n\n  1. To derive the submodule path in case we know it should already\n     exist.\n\n  2. To compute the submodule path so we can end up writing it into the\n     \"submodule.*.gitdir\" variable.\n\nI think we should tell these two cases apart. In the first case I expect\nthat we never fall back to a computed name, but bail out in case the\nconfiguration key does not exist. And in the second case it of course\nmakes sense to compute the actual path that we want to store in the\nconfiguration.\n\nThanks!\n\nPatrick\n"},{"id":"531696","messageId":"aTLNxlKh02T_1PYB@pks.im","threadId":"63967","inReplyTo":"20251119211030.2008441-8-adrian.ratiu@collabora.com","subject":"Re: [PATCH v5 7/7] meson/Makefile: allow setting submodule encoding at build time","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-05T12:19:18Z","receivedAt":"2025-12-05T12:19:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:10:30PM +0200, Adrian Ratiu wrote:\n> Some users find it difficult to distribute repo config changes for\n> enabling extensions.submoduleEncoding, or to enable it by passing\n> the config via cmdline, so we add a build-time option which can\n> enable the extension for convenience.\n\nWouldn't it be more sensible to make this a runtime configuration key\nthat users can configure in their gitconfig?\n\nPatrick\n"},{"id":"531716","messageId":"871pl8g7se.fsf@collabora.com","threadId":"63967","inReplyTo":"aTLNLnF_ZLpMnso-@pks.im","subject":"Re: [PATCH v5 2/7] builtin/credential-store: move is_rfc3986_unreserved to url.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-05T17:25:37Z","receivedAt":"2025-12-05T17:25:57Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Fri, 05 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Wed, Nov 19, 2025 at 11:10:25PM +0200, Adrian Ratiu wrote:\n>> is_rfc3986_unreserved() was moved to credential-store.c and was made\n>> static by f89854362c (credential-store: move related functions to\n>> credential-store file, 2023-06-06) under a correct assumption, at the\n>> time, that it was the only place using it.\n>> \n>> However now we need it to apply URL-encoding to submodule names when\n>> constructing gitdir paths, to avoid conflicts, so bring it back as a\n>> public function exposed via url.h, instead of the old helper path\n>> (strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n>> \n>> This function will be used by submodule.c in the next commit.\n>\n> Nit: this statement isn't true anymore :)\n>\n\nIndeed, thanks for catching these.\n\nI will fix both nits in v6.\n\n>> diff --git a/url.c b/url.c\n>> index 282b12495a..0fb1859b28 100644\n>> --- a/url.c\n>> +++ b/url.c\n>> @@ -3,6 +3,17 @@\n>>  #include \"strbuf.h\"\n>>  #include \"url.h\"\n>>  \n>> +/*\n>> + * The set of unreserved characters as per STD66 (RFC3986) is\n>> + * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n>> + * components without percent-encoding.\n>> + */\n>> +int is_rfc3986_unreserved(char ch)\n>> +{\n>> +\treturn isalnum(ch) ||\n>> +\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n>> +}\n>> +\n>>  int is_urlschemechar(int first_flag, int ch)\n>>  {\n>>  \t/*\n>\n> Nit: the function documentation should rather live in the header file.\n>\n> Patrick\n"},{"id":"531721","messageId":"87y0ngeqto.fsf@collabora.com","threadId":"63967","inReplyTo":"aTLNPKlDnsNzyZkC@pks.im","subject":"Re: [PATCH v5 3/7] submodule: always validate gitdirs inside submodule_name_to_gitdir","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-05T18:17:23Z","receivedAt":"2025-12-05T18:17:40Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Fri, 05 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Wed, Nov 19, 2025 at 11:10:26PM +0200, Adrian Ratiu wrote:\n>> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n>> index 2873b2780e..9914ca0786 100644\n>> --- a/builtin/submodule--helper.c\n>> +++ b/builtin/submodule--helper.c\n>> @@ -1780,23 +1776,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n>>  \t\tfree(path);\n>>  \t}\n>>  \n>> -\t/*\n>> -\t * We already performed this check at the beginning of this function,\n>> -\t * before cloning the objects. This tries to detect racy behavior e.g.\n>> -\t * in parallel clones, where another process could easily have made the\n>> -\t * gitdir nested _after_ it was created.\n>> -\t *\n>> -\t * To prevent further harm coming from this unintentionally-nested\n>> -\t * gitdir, let's disable it by deleting the `HEAD` file.\n>> -\t */\n>> -\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {\n>> -\t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n>> -\t\tunlink(head);\n>> -\t\tfree(head);\n>> -\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n>> -\t\t      \"git dir\"), sm_gitdir);\n>> -\t}\n>> -\n>>  \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n>>  \n>>  \tp = repo_submodule_path(the_repository, clone_data_path, \"config\");\n>\n> Hm. This one is a bit puzzling to me. This seems to explicitly be a\n> check about a TOCTOU-style race, where a concurrent process might have\n> created the parent repository after our initial validation of the path.\n> We don't call `submodule_name_to_gitdir()` inbetween those two calls\n> though, so why is this not a concern anymore with the unified API?\n\nExcellent catch.\n\nIt still is a concern and this specific check needs to be added back.\n\nMy aim with this patch is to:\n1. Avoid redundant validation calls for submodule_name_to_gitdir().\n2. Avoid the risk of callers forgetting to validate.\n3. Ensure gitdir paths provided by users via configs are always valid,\nso I added the validation directly into submodule_name_to_gitdir().\n\nNothing prevents us from running the validation as many times as we\nneed and, in this case, it's perfectly justified to run it twice.\n\nI'll add it back in v6.\n\n>> diff --git a/submodule.c b/submodule.c\n>> index 35c55155f7..8ef028f26b 100644\n>> --- a/submodule.c\n>> +++ b/submodule.c\n>> @@ -2153,30 +2153,11 @@ int submodule_move_head(const char *path, const char *super_prefix,\n>>  \n>>  \tif (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {\n>>  \t\tif (old_head) {\n>> -\t\t\tif (!submodule_uses_gitfile(path))\n>> -\t\t\t\tabsorb_git_dir_into_superproject(path,\n>> -\t\t\t\t\t\t\t\t super_prefix);\n>> -\t\t\telse {\n>> -\t\t\t\tchar *dotgit = xstrfmt(\"%s/.git\", path);\n>> -\t\t\t\tchar *git_dir = xstrdup(read_gitfile(dotgit));\n>> -\n>> -\t\t\t\tfree(dotgit);\n>> -\t\t\t\tif (validate_submodule_git_dir(git_dir,\n>> -\t\t\t\t\t\t\t       sub->name) < 0)\n>> -\t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n>> -\t\t\t\t\t      \"another submodule's git dir\"),\n>> -\t\t\t\t\t    git_dir);\n>> -\t\t\t\tfree(git_dir);\n>> -\t\t\t}\n>> +\t\t\tabsorb_git_dir_into_superproject(path, super_prefix);\n>>  \t\t} else {\n>>  \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n>>  \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n>>  \t\t\t\t\t\t sub->name);\n>> -\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n>> -\t\t\t\t\t\t       sub->name) < 0)\n>> -\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n>> -\t\t\t\t      \"submodule's git dir\"),\n>> -\t\t\t\t    gitdir.buf);\n>>  \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n>>  \t\t\tstrbuf_release(&gitdir);\n>\n> The second case here makes sense to me, as we do call\n> `submodule_name_to_gitdir()`. But in the first branch of the condition\n> we retrieve the path directly, so we're not guarded by the validation\n> anymore, are we?\n\nThe !submodule_uses_gitfile(path) case never validated and relied on the\nvalidation done by absorb_git_dir_into_superproject() which calls both\nvalidate_submodule_path() and submodule_name_to_gitdir() and has\nadditional checks.\n\nSo in essence the validation moved to absorb_git_dir_into_superproject().\n\nIf you prefer I can try to refactor absorb_git_dir_into_superproject() a\nbit to make it clearer, otherwise I can just add back the validation\nhere as well... we can validate as many times as we like. :)\n"},{"id":"531726","messageId":"87v7ikeng2.fsf@collabora.com","threadId":"63967","inReplyTo":"aTLNwHpLUcy-WsZs@pks.im","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-05T19:30:21Z","receivedAt":"2025-12-05T19:30:43Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Fri, 05 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Wed, Nov 19, 2025 at 11:10:27PM +0200, Adrian Ratiu wrote:\n>> Add a submoduleEncoding extension which fixes filesystem collisions by\n>> encoding gitdir paths. At a high level, this implements a mechanism to\n>> encode -> validate -> retry until a working gitdir path is found.\n>> \n>> Credit goes to Junio for coming up with this design: encoding is only\n>> applied when necessary, e.g. uppercase characters are encoded only on\n>> case-folding filesystems and only if a real conflict is detected.\n>> \n>> To make this work, we rely on the submodule.<name>.gitdir config as the\n>> single source of truth for gitidir paths: the config is always set when\n>\n> s/gitidir/gitdir/\n\nAck, will fix.\n\n>\n>> diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\n>> index 532456644b..4861d01894 100644\n>> --- a/Documentation/config/extensions.adoc\n>> +++ b/Documentation/config/extensions.adoc\n>> @@ -73,6 +73,12 @@ relativeWorktrees:::\n>>  \trepaired with either the `--relative-paths` option or with the\n>>  \t`worktree.useRelativePaths` config set to `true`.\n>>  \n>> +submoduleEncoding:::\n>> +\tIf enabled, submodule gitdir paths are encoded to avoid filesystem\n>> +\tconflicts due to nested gitdirs, case insensitivity or other issues.\n>> +\tWhen enabled, the submodule.<name>.gitdir config is always set for\n>> +\tall submodules and is the single point of authority for gitdir paths.\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\n>\n> I think the fact that the submodule gitdir paths are encoded now is\n> secondary to this repository extension. The more important fact is that\n> this changes the source of truth where the submodule gitdir path is\n> actually derived from: before it was derived on the fly, whereas now it\n> is persisted in the gitconfig.\n>\n> It follows that because the source of truth is now a persistent entry in\n> the configuration, other implementations can read it without having to\n> understand how exactly the value was computed in the first place. So an\n> implementation may arbitrarily change the algorithm it uses to derive\n> that path from now on, and it doesn't necessarily have to encode\n> anything.\n>\n> So I'd propose to rename the extension and rephrase its description\n> accordingly. It could for example be called something along the lines of\n> \"submodulePathConfig\".\n\nI think this is a very reasonable suggestion, thanks!\n\nIf nobody has objections or better suggestions, I will rename the\nextension to \"submodulePathConfig\" and reword the description as you\nsuggested.\n\n>\n>> diff --git a/submodule.c b/submodule.c\n>> index 8ef028f26b..07cb4694cf 100644\n>> --- a/submodule.c\n>> +++ b/submodule.c\n>> @@ -2559,33 +2591,74 @@ int submodule_to_gitdir(struct repository *repo,\n>>  \treturn ret;\n>>  }\n>>  \n>> +static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n>> +\t\t\t\t\t     const char *submodule_name)\n>> +{\n>> +\tchar *key;\n>> +\n>> +\tif (validate_submodule_encoded_git_dir(gitdir_path->buf, submodule_name))\n>> +\t\treturn -1;\n>> +\n>> +\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n>> +\trepo_config_set_gently(the_repository, key, gitdir_path->buf);\n>> +\tFREE_AND_NULL(key);\n>\n> I think a simple call to `free()` should be sufficient here. There is no\n> risk of it being used afterwards.\n\nAck, will fix.\n\n>\n>> +\treturn 0;\n>> +}\n>> +\n>>  void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>>  \t\t\t      const char *submodule_name)\n>>  {\n>> +\tconst char *gitdir;\n>> +\tchar *key;\n>> +\n>> +\trepo_git_path_append(r, buf, \"modules/\");\n>> +\tstrbuf_addstr(buf, submodule_name);\n>> +\n>> +\t/* If extensions.submoduleEncoding is disabled, use the plain path set above */\n>> +\tif (!r->repository_format_submodule_encoding) {\n>> +\t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n>> +\t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n>> +\t\t\t      \"git dir\"), buf->buf);\n>> +\n>> +\t\treturn; /* plain gitdir is valid for use */\n>> +\t}\n>> +\n>> +\t/* Extension is enabled: use the gitdir config if it exists */\n>> +\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n>> +\tif (!repo_config_get_string_tmp(r, key, &gitdir)) {\n>> +\t\tstrbuf_reset(buf);\n>> +\t\tstrbuf_addstr(buf, gitdir);\n>> +\t\tFREE_AND_NULL(key);\n>> +\n>> +\t\t/* validate because users might have modified the config */\n>> +\t\tif (validate_submodule_encoded_git_dir(buf->buf, submodule_name))\n>> +\t\t\tdie(_(\"Invalid 'submodule.%s.gitdir' config: '%s' please check \"\n>> +\t\t\t      \"if it is unique or conflicts with another module\"),\n>\n> Nit: error messages start with a lower-case character.\n\nAck, will fix.\n\n>\n>> +\t\t\t    submodule_name, gitdir);\n>> +\n>> +\t\treturn;\n>> +\t}\n>> +\tFREE_AND_NULL(key);\n>> +\n>>  \t/*\n>> -\t * NEEDSWORK: The current way of mapping a submodule's name to\n>> -\t * its location in .git/modules/ has problems with some naming\n>> -\t * schemes. For example, if a submodule is named \"foo\" and\n>> -\t * another is named \"foo/bar\" (whether present in the same\n>> -\t * superproject commit or not - the problem will arise if both\n>> -\t * superproject commits have been checked out at any point in\n>> -\t * time), or if two submodule names only have different cases in\n>> -\t * a case-insensitive filesystem.\n>> -\t *\n>> -\t * There are several solutions, including encoding the path in\n>> -\t * some way, introducing a submodule.<name>.gitdir config in\n>> -\t * .git/config (not .gitmodules) that allows overriding what the\n>> -\t * gitdir of a submodule would be (and teach Git, upon noticing\n>> -\t * a clash, to automatically determine a non-clashing name and\n>> -\t * to write such a config), or introducing a\n>> -\t * submodule.<name>.gitdir config in .gitmodules that repo\n>> -\t * administrators can explicitly set. Nothing has been decided,\n>> -\t * so for now, just append the name at the end of the path.\n>> +\t * The gitdir config does not exist, even though the extension is enabled.\n>> +\t * Therefore we are in one of the following cases:\n>>  \t */\n>> +\n>> +\t/* Case 1: legacy migration of valid plain submodule names */\n>> +\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n>> +\t\treturn;\n>> +\n>> +\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n>> +\tstrbuf_reset(buf);\n>>  \trepo_git_path_append(r, buf, \"modules/\");\n>> -\tstrbuf_addstr(buf, submodule_name);\n>> +\tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n>> +\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n>> +\t\treturn;\n>>  \n>> -\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n>> -\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n>> -\t\t      \"git dir\"), buf->buf);\n>> +\t/* Case 3: Nothing worked: error out */\n>> +\tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n>> +\t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\n>> +\t    submodule_name, submodule_name);\n>\n> It feels somewhat fragile to me that we unconditionally handle these\n> cases and try to find old submodule directories. If the extension is\n> enabled I'd expect that the submodule configuration is the _only_ source\n> of truth.\n>\n> May I propose that we instead always error out in case the submodule\n> configuration does not exist? In the best case we'd then give the user a\n> nice error message that tells them how to run the migration manually.\n\nJunio told me to not do any kind of manual migration and just attempt\nnew names until one works and then use it consistently.\n\nThat's why the \"submodule.%s.gitdir\" path is always used if set and\nhas precedence (no new names are attempted). :)\n\n>\n> (Coming back from reading subsequent patches) Maybe what's putting me\n> off is that this function is seemingly used for two things:\n>\n>   1. To derive the submodule path in case we know it should already\n>      exist.\n>\n>   2. To compute the submodule path so we can end up writing it into the\n>      \"submodule.*.gitdir\" variable.\n>\n> I think we should tell these two cases apart. In the first case I expect\n> that we never fall back to a computed name, but bail out in case the\n> configuration key does not exist. And in the second case it of course\n> makes sense to compute the actual path that we want to store in the\n> configuration.\n\nI think I understand where you're coming from.\n\nEven before my patches, the unmodified submodule_name_to_gitdir() is\nused for both new (non-existing) and old (existing) submodules.\n\nIt has no way of knowing whether a submodule exists, whether it should\nexist, or whether a new name is required for a new clone, which will\neventually exist in the future.\n\nIf I also understood your suggestion, you just need an additional check\nto verify if the path pointed to by \"submodule.%s.gitdir\" is an existing\ngitdir and error out if not?\n\nOr did I misunderstood your suggestion and you mean to bail out if the\nconfig key is missing entirely for any submodule when the extension is\nenabled?\n\nThat would imply a manual migration by the user which is something both\nAaron and Junio asked me to avoid and Josh also said they want to avoid\nsetting any kind of config keys (or distributing configs), so that's why\nI also added the compile-time extension option, to ease the transition,\ntogether with the \"retry-on-fallback\" approach for setting the config.\n\nI am in favor of implementing the split you suggested, however how do we\nautomatically figure out if a name's gitdir **should** exist if you mean\nthe latter not the former? :)\n"},{"id":"531727","messageId":"87sedoemvr.fsf@collabora.com","threadId":"63967","inReplyTo":"aTLNxlKh02T_1PYB@pks.im","subject":"Re: [PATCH v5 7/7] meson/Makefile: allow setting submodule encoding at build time","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-05T19:42:32Z","receivedAt":"2025-12-05T19:42:54Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Fri, 05 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Wed, Nov 19, 2025 at 11:10:30PM +0200, Adrian Ratiu wrote:\n>> Some users find it difficult to distribute repo config changes for\n>> enabling extensions.submoduleEncoding, or to enable it by passing\n>> the config via cmdline, so we add a build-time option which can\n>> enable the extension for convenience.\n>\n> Wouldn't it be more sensible to make this a runtime configuration key\n> that users can configure in their gitconfig?\n\nThe request I got from a combination of feedback from Junio, Aaron and\nJosh is to avoid any kind of required user intervention or manual\nmigration, to find ways to automate the transition as much as possible.\n\nIf possible without even having to change or distribute configs or set\ncmdline parameters to enable the new extension (that is why I added this\noff-by-default build option in v5).\n\nWe could add a runtime gitconfig key in addition to the build and repo\nconfig options, I see no issue with that.\n\nI have no horse in this race btw, just trying to make everyone happy. :)\n"},{"id":"531730","messageId":"xmqqqzt87dgj.fsf@gitster.g","threadId":"63967","inReplyTo":"87v7ikeng2.fsf@collabora.com","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-05T22:47:56Z","receivedAt":"2025-12-05T22:48:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n>> It feels somewhat fragile to me that we unconditionally handle these\n>> cases and try to find old submodule directories. If the extension is\n>> enabled I'd expect that the submodule configuration is the _only_ source\n>> of truth.\n>>\n>> May I propose that we instead always error out in case the submodule\n>> configuration does not exist? In the best case we'd then give the user a\n>> nice error message that tells them how to run the migration manually.\n>\n> Junio told me to not do any kind of manual migration and just attempt\n> new names until one works and then use it consistently.\n\nIndeed, but I do not think that has much relevance to Patrick's\ncomment.  What I meant say was\n\n - With extension, we know that the repository will use the\n   configuration item as the sole source of truth.  Unlike the \"we\n   now store submodule dirs to munged paths\" design that we saw long\n   ago, which would have required us to sometimes move the existing\n   directories to match the munging scheme, we do not have to\n   manually migrate the existing directories.  Instead, we can just\n   record the pathnames they already use.\n\n - And with extension, when we add a new submodule, we would need to\n   give them an entry in the configuration that does not conflict\n   with those used by the existing submodules.  As Patrick mentions,\n   this name does not have to be done with any reversible mapping,\n   and third-party software and Git reimplementations can always\n   refer to the configuration to learn the path without knowing how\n   the path is chosen themselves.\n\n - The above two would ensure that the configuration would always\n   exist once a submodule enters our system (and \"git submodule init\"\n   is done to cause its gitdir created).\n\nSo, unless I am missing some corner case that still exists while\nbootstrapping a new style repository with extension, Patrick's\n\"error when configuration is missing\" sounds quite sensible to me.\n\nI am officially still on vacation, so I'd stop here for now ;-).\n"},{"id":"531731","messageId":"xmqqms3w7d9e.fsf@gitster.g","threadId":"63967","inReplyTo":"87sedoemvr.fsf@collabora.com","subject":"Re: [PATCH v5 7/7] meson/Makefile: allow setting submodule encoding at build time","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-05T22:52:13Z","receivedAt":"2025-12-05T22:52:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> On Fri, 05 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n>> On Wed, Nov 19, 2025 at 11:10:30PM +0200, Adrian Ratiu wrote:\n>>> Some users find it difficult to distribute repo config changes for\n>>> enabling extensions.submoduleEncoding, or to enable it by passing\n>>> the config via cmdline, so we add a build-time option which can\n>>> enable the extension for convenience.\n>>\n>> Wouldn't it be more sensible to make this a runtime configuration key\n>> that users can configure in their gitconfig?\n>\n> The request I got from a combination of feedback from Junio, Aaron and\n> Josh is to avoid any kind of required user intervention or manual\n> migration, to find ways to automate the transition as much as possible.\n\nHow would that lead to build-time behaviour change, though?\n\nUsers in managed environments like $CORP can rely on /etc/gitconfig\nor equivalents managed by their corp-eng, so I am having a hard time\nimagining why we need anything more than an configuration variable\nlooked at runtime.\n"},{"id":"531750","messageId":"aTQanNSlj6VxDY-n@pks.im","threadId":"63967","inReplyTo":"xmqqqzt87dgj.fsf@gitster.g","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-06T11:59:24Z","receivedAt":"2025-12-06T12:00:43Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 06, 2025 at 07:47:56AM +0900, Junio C Hamano wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n> \n> >> It feels somewhat fragile to me that we unconditionally handle these\n> >> cases and try to find old submodule directories. If the extension is\n> >> enabled I'd expect that the submodule configuration is the _only_ source\n> >> of truth.\n> >>\n> >> May I propose that we instead always error out in case the submodule\n> >> configuration does not exist? In the best case we'd then give the user a\n> >> nice error message that tells them how to run the migration manually.\n> >\n> > Junio told me to not do any kind of manual migration and just attempt\n> > new names until one works and then use it consistently.\n> \n> Indeed, but I do not think that has much relevance to Patrick's\n> comment.  What I meant say was\n> \n>  - With extension, we know that the repository will use the\n>    configuration item as the sole source of truth.  Unlike the \"we\n>    now store submodule dirs to munged paths\" design that we saw long\n>    ago, which would have required us to sometimes move the existing\n>    directories to match the munging scheme, we do not have to\n>    manually migrate the existing directories.  Instead, we can just\n>    record the pathnames they already use.\n> \n>  - And with extension, when we add a new submodule, we would need to\n>    give them an entry in the configuration that does not conflict\n>    with those used by the existing submodules.  As Patrick mentions,\n>    this name does not have to be done with any reversible mapping,\n>    and third-party software and Git reimplementations can always\n>    refer to the configuration to learn the path without knowing how\n>    the path is chosen themselves.\n> \n>  - The above two would ensure that the configuration would always\n>    exist once a submodule enters our system (and \"git submodule init\"\n>    is done to cause its gitdir created).\n> \n> So, unless I am missing some corner case that still exists while\n> bootstrapping a new style repository with extension, Patrick's\n> \"error when configuration is missing\" sounds quite sensible to me.\n> \n> I am officially still on vacation, so I'd stop here for now ;-).\n\nI guess the one edge case is when somebody manually turns on the\nextension after they have already initialized submodules. In that case\nthe gitdir paths would of course not exist in the gitconfig. But I think\nblindly falling back to a source of truth different than the\nconfiguration is wrong, as it would mean that the we have \"a single\nsource of truth unless we don't\". It would kind of defeat the whole\npurpose of the extension in my opinion, as implementations cannot rely\non it.\n\nMaybe the right approach would be to tell users to never manually enable\nthe extension and instead to provide a command that both:\n\n  - Persists the submodule gitdirs for any populated submodules in the\n    gitconfig.\n\n  - Enables the repsitory extension.\n\nIf we had that then we could count on the submodule gitdirs to exist in\nthe gitconfig, and if they don't we would die with an error message that\nindicates that the repository is broken, maybe even with a hint for the\nuser on how to fix it.\n\nPatrick\n"},{"id":"531751","messageId":"aTQbbFisaCG8N_Z_@pks.im","threadId":"63967","inReplyTo":"xmqqms3w7d9e.fsf@gitster.g","subject":"Re: [PATCH v5 7/7] meson/Makefile: allow setting submodule encoding at build time","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-06T12:02:52Z","receivedAt":"2025-12-06T12:03:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 06, 2025 at 07:52:13AM +0900, Junio C Hamano wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n> \n> > On Fri, 05 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> >> On Wed, Nov 19, 2025 at 11:10:30PM +0200, Adrian Ratiu wrote:\n> >>> Some users find it difficult to distribute repo config changes for\n> >>> enabling extensions.submoduleEncoding, or to enable it by passing\n> >>> the config via cmdline, so we add a build-time option which can\n> >>> enable the extension for convenience.\n> >>\n> >> Wouldn't it be more sensible to make this a runtime configuration key\n> >> that users can configure in their gitconfig?\n> >\n> > The request I got from a combination of feedback from Junio, Aaron and\n> > Josh is to avoid any kind of required user intervention or manual\n> > migration, to find ways to automate the transition as much as possible.\n> \n> How would that lead to build-time behaviour change, though?\n> \n> Users in managed environments like $CORP can rely on /etc/gitconfig\n> or equivalents managed by their corp-eng, so I am having a hard time\n> imagining why we need anything more than an configuration variable\n> looked at runtime.\n\nI guess you could kind of have both: make it a runtime configuration\nkey, but have its default depend on a build configuration. You could for\nexample auto-enable it in case `WITH_BREAKING_CHANGES` is enabled. But I\nmyself am not sure whether the latter would really be all that important\nin the first place.\n\nPatrick\n"},{"id":"531760","messageId":"xmqq8qffpnui.fsf@gitster.g","threadId":"63967","inReplyTo":"aTQanNSlj6VxDY-n@pks.im","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-06T16:38:29Z","receivedAt":"2025-12-06T16:38:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Maybe the right approach would be to tell users to never manually enable\n> the extension and instead to provide a command that both:\n>\n>   - Persists the submodule gitdirs for any populated submodules in the\n>     gitconfig.\n>\n>   - Enables the repsitory extension.\n>\n> If we had that then we could count on the submodule gitdirs to exist in\n> the gitconfig, and if they don't we would die with an error message that\n> indicates that the repository is broken, maybe even with a hint for the\n> user on how to fix it.\n\nI personally like the simplicity of this approach.\n\nI haven't however thought about operational complexity, if one has\nan existing user base that have been using a custom pathname munging\ncode that needs to be migrated to the new scheme.\n"},{"id":"531761","messageId":"xmqq4iq3pndu.fsf@gitster.g","threadId":"63967","inReplyTo":"aTQbbFisaCG8N_Z_@pks.im","subject":"Re: [PATCH v5 7/7] meson/Makefile: allow setting submodule encoding at build time","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-06T16:48:29Z","receivedAt":"2025-12-06T16:48:31Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n>> How would that lead to build-time behaviour change, though?\n>> \n>> Users in managed environments like $CORP can rely on /etc/gitconfig\n>> or equivalents managed by their corp-eng, so I am having a hard time\n>> imagining why we need anything more than an configuration variable\n>> looked at runtime.\n>\n> I guess you could kind of have both: make it a runtime configuration\n> key, but have its default depend on a build configuration. You could for\n> example auto-enable it in case `WITH_BREAKING_CHANGES` is enabled. But I\n> myself am not sure whether the latter would really be all that important\n> in the first place.\n\nMe neither ;-)\n"},{"id":"531823","messageId":"87tsy1uz2q.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqq8qffpnui.fsf@gitster.g","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-08T09:01:33Z","receivedAt":"2025-12-08T09:02:00Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Sun, 07 Dec 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> Maybe the right approach would be to tell users to never manually enable\n>> the extension and instead to provide a command that both:\n>>\n>>   - Persists the submodule gitdirs for any populated submodules in the\n>>     gitconfig.\n>>\n>>   - Enables the repsitory extension.\n>>\n>> If we had that then we could count on the submodule gitdirs to exist in\n>> the gitconfig, and if they don't we would die with an error message that\n>> indicates that the repository is broken, maybe even with a hint for the\n>> user on how to fix it.\n>\n> I personally like the simplicity of this approach.\n>\n> I haven't however thought about operational complexity, if one has\n> an existing user base that have been using a custom pathname munging\n> code that needs to be migrated to the new scheme.\n\nThe good news is that for all the real-world use cases I'm aware of,\nmigration to the new scheme should just work.\n\nThe problem we're discussing here (automatic fallback vs requiring the\ngitdir to always exist + using a migration tool) does add a bit of\noperational complexity, however it might be manageable.\n\nI have to see how this works in practice, especially with the users\nsetting the config (likely I'll add an error to block that path) and the\nbuild-config to enable the extension by default (will likely run the\nmigration tool automatically on existing repositories if enabled via the\nbuild-config).\n\nI will attempt this approach in v6.\n"},{"id":"531824","messageId":"87qzt5uyoc.fsf@collabora.com","threadId":"63967","inReplyTo":"aTQanNSlj6VxDY-n@pks.im","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-08T09:10:11Z","receivedAt":"2025-12-08T09:10:36Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Sat, 06 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Sat, Dec 06, 2025 at 07:47:56AM +0900, Junio C Hamano wrote:\n>> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>> \n>> >> It feels somewhat fragile to me that we unconditionally handle these\n>> >> cases and try to find old submodule directories. If the extension is\n>> >> enabled I'd expect that the submodule configuration is the _only_ source\n>> >> of truth.\n>> >>\n>> >> May I propose that we instead always error out in case the submodule\n>> >> configuration does not exist? In the best case we'd then give the user a\n>> >> nice error message that tells them how to run the migration manually.\n>> >\n>> > Junio told me to not do any kind of manual migration and just attempt\n>> > new names until one works and then use it consistently.\n>> \n>> Indeed, but I do not think that has much relevance to Patrick's\n>> comment.  What I meant say was\n>> \n>>  - With extension, we know that the repository will use the\n>>    configuration item as the sole source of truth.  Unlike the \"we\n>>    now store submodule dirs to munged paths\" design that we saw long\n>>    ago, which would have required us to sometimes move the existing\n>>    directories to match the munging scheme, we do not have to\n>>    manually migrate the existing directories.  Instead, we can just\n>>    record the pathnames they already use.\n>> \n>>  - And with extension, when we add a new submodule, we would need to\n>>    give them an entry in the configuration that does not conflict\n>>    with those used by the existing submodules.  As Patrick mentions,\n>>    this name does not have to be done with any reversible mapping,\n>>    and third-party software and Git reimplementations can always\n>>    refer to the configuration to learn the path without knowing how\n>>    the path is chosen themselves.\n>> \n>>  - The above two would ensure that the configuration would always\n>>    exist once a submodule enters our system (and \"git submodule init\"\n>>    is done to cause its gitdir created).\n>> \n>> So, unless I am missing some corner case that still exists while\n>> bootstrapping a new style repository with extension, Patrick's\n>> \"error when configuration is missing\" sounds quite sensible to me.\n>> \n>> I am officially still on vacation, so I'd stop here for now ;-).\n>\n> I guess the one edge case is when somebody manually turns on the\n> extension after they have already initialized submodules. In that case\n> the gitdir paths would of course not exist in the gitconfig. But I think\n> blindly falling back to a source of truth different than the\n> configuration is wrong, as it would mean that the we have \"a single\n> source of truth unless we don't\". It would kind of defeat the whole\n> purpose of the extension in my opinion, as implementations cannot rely\n> on it.\n\nHaven't thought about it that way and indeed it makes a lot of sense.\n\n100% agreed.\n\n>\n> Maybe the right approach would be to tell users to never manually enable\n> the extension and instead to provide a command that both:\n>\n>   - Persists the submodule gitdirs for any populated submodules in the\n>     gitconfig.\n>\n>   - Enables the repsitory extension.\n>\n> If we had that then we could count on the submodule gitdirs to exist in\n> the gitconfig, and if they don't we would die with an error message that\n> indicates that the repository is broken, maybe even with a hint for the\n> user on how to fix it.\n\nThank you Patrick for clarifying this, really appreciate it.\n\nI will implement this design in v6.\n"},{"id":"531825","messageId":"87o6o9uy24.fsf@collabora.com","threadId":"63967","inReplyTo":"aTQbbFisaCG8N_Z_@pks.im","subject":"Re: [PATCH v5 7/7] meson/Makefile: allow setting submodule encoding at build time","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-08T09:23:31Z","receivedAt":"2025-12-08T09:23:55Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Sat, 06 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Sat, Dec 06, 2025 at 07:52:13AM +0900, Junio C Hamano wrote:\n>> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>> \n>> > On Fri, 05 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n>> >> On Wed, Nov 19, 2025 at 11:10:30PM +0200, Adrian Ratiu wrote:\n>> >>> Some users find it difficult to distribute repo config changes for\n>> >>> enabling extensions.submoduleEncoding, or to enable it by passing\n>> >>> the config via cmdline, so we add a build-time option which can\n>> >>> enable the extension for convenience.\n>> >>\n>> >> Wouldn't it be more sensible to make this a runtime configuration key\n>> >> that users can configure in their gitconfig?\n>> >\n>> > The request I got from a combination of feedback from Junio, Aaron and\n>> > Josh is to avoid any kind of required user intervention or manual\n>> > migration, to find ways to automate the transition as much as possible.\n>> \n>> How would that lead to build-time behaviour change, though?\n>> \n>> Users in managed environments like $CORP can rely on /etc/gitconfig\n>> or equivalents managed by their corp-eng, so I am having a hard time\n>> imagining why we need anything more than an configuration variable\n>> looked at runtime.\n>\n> I guess you could kind of have both: make it a runtime configuration\n> key, but have its default depend on a build configuration. You could for\n> example auto-enable it in case `WITH_BREAKING_CHANGES` is enabled. But I\n> myself am not sure whether the latter would really be all that important\n> in the first place.\n\nI'll have to revisit this after I implement the migration command design\nyou suggested for v6, to figure out exactly how to best implement this.\n\nThank you for this suggestion as well, likely I'll do something very\nsimilar if it ends up being necessary.\n"},{"id":"531826","messageId":"87ldjdux6p.fsf@collabora.com","threadId":"63967","inReplyTo":"xmqqms3w7d9e.fsf@gitster.g","subject":"Re: [PATCH v5 7/7] meson/Makefile: allow setting submodule encoding at build time","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-08T09:42:22Z","receivedAt":"2025-12-08T09:42:43Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Sat, 06 Dec 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n>> On Fri, 05 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n>>> On Wed, Nov 19, 2025 at 11:10:30PM +0200, Adrian Ratiu wrote:\n>>>> Some users find it difficult to distribute repo config changes for\n>>>> enabling extensions.submoduleEncoding, or to enable it by passing\n>>>> the config via cmdline, so we add a build-time option which can\n>>>> enable the extension for convenience.\n>>>\n>>> Wouldn't it be more sensible to make this a runtime configuration key\n>>> that users can configure in their gitconfig?\n>>\n>> The request I got from a combination of feedback from Junio, Aaron and\n>> Josh is to avoid any kind of required user intervention or manual\n>> migration, to find ways to automate the transition as much as possible.\n>\n> How would that lead to build-time behaviour change, though?\n>\n> Users in managed environments like $CORP can rely on /etc/gitconfig\n> or equivalents managed by their corp-eng, so I am having a hard time\n> imagining why we need anything more than an configuration variable\n> looked at runtime.\n\nPlease see Josh's message:\n\nhttps://public-inbox.org/git/20250816213642.3517822-1-adrian.ratiu@collabora.com/T/#m7d0d75126c81bef7d3619e53da6fa0cd69426570\n\nA config variable looked up at runtime would solve most cases\nhighlighted there, except for the force-enable\n`extensions.submoduleEncoding` regardless of the local config.\n\nThat is why I added this option :) though we do not have a local config\nin v5 because I saw no reason for it at the time.\n\nFor v6 I will likely implement Patrick's suggestion to introduce a\nconfig variable and just set its default at build-time which seems like\nthe cleanest way to do it (and automatically run the v6 migration\ncommand instead of the current automatic fallback).\n\nThat would allow a smooth automatic transition which will address Josh's\nrequirements.\n"},{"id":"531828","messageId":"aTa6lr9AOPIZlw5M@pks.im","threadId":"63967","inReplyTo":"87tsy1uz2q.fsf@collabora.com","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-08T11:46:30Z","receivedAt":"2025-12-08T11:46:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Dec 08, 2025 at 11:01:33AM +0200, Adrian Ratiu wrote:\n> On Sun, 07 Dec 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> >\n> >> Maybe the right approach would be to tell users to never manually enable\n> >> the extension and instead to provide a command that both:\n> >>\n> >>   - Persists the submodule gitdirs for any populated submodules in the\n> >>     gitconfig.\n> >>\n> >>   - Enables the repsitory extension.\n> >>\n> >> If we had that then we could count on the submodule gitdirs to exist in\n> >> the gitconfig, and if they don't we would die with an error message that\n> >> indicates that the repository is broken, maybe even with a hint for the\n> >> user on how to fix it.\n> >\n> > I personally like the simplicity of this approach.\n> >\n> > I haven't however thought about operational complexity, if one has\n> > an existing user base that have been using a custom pathname munging\n> > code that needs to be migrated to the new scheme.\n> \n> The good news is that for all the real-world use cases I'm aware of,\n> migration to the new scheme should just work.\n> \n> The problem we're discussing here (automatic fallback vs requiring the\n> gitdir to always exist + using a migration tool) does add a bit of\n> operational complexity, however it might be manageable.\n> \n> I have to see how this works in practice, especially with the users\n> setting the config (likely I'll add an error to block that path) and the\n> build-config to enable the extension by default (will likely run the\n> migration tool automatically on existing repositories if enabled via the\n> build-config).\n> \n> I will attempt this approach in v6.\n\nOne suggestion: it might be sensible to move auto-migration into a\nsubsequent patch series. That way we can focus on the general approach\nof the new extension at first, and the potentially-bigger discussion\naround whether or not to auto-migrate users can then be had separately.\n\nPatrick\n"},{"id":"531837","messageId":"87ikehug94.fsf@collabora.com","threadId":"63967","inReplyTo":"aTa6lr9AOPIZlw5M@pks.im","subject":"Re: [PATCH v5 4/7] submodule: add extension to encode gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-08T15:48:07Z","receivedAt":"2025-12-08T15:48:31Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 08 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Mon, Dec 08, 2025 at 11:01:33AM +0200, Adrian Ratiu wrote:\n>> On Sun, 07 Dec 2025, Junio C Hamano <gitster@pobox.com> wrote:\n>> > Patrick Steinhardt <ps@pks.im> writes:\n>> >\n>> >> Maybe the right approach would be to tell users to never manually enable\n>> >> the extension and instead to provide a command that both:\n>> >>\n>> >>   - Persists the submodule gitdirs for any populated submodules in the\n>> >>     gitconfig.\n>> >>\n>> >>   - Enables the repsitory extension.\n>> >>\n>> >> If we had that then we could count on the submodule gitdirs to exist in\n>> >> the gitconfig, and if they don't we would die with an error message that\n>> >> indicates that the repository is broken, maybe even with a hint for the\n>> >> user on how to fix it.\n>> >\n>> > I personally like the simplicity of this approach.\n>> >\n>> > I haven't however thought about operational complexity, if one has\n>> > an existing user base that have been using a custom pathname munging\n>> > code that needs to be migrated to the new scheme.\n>> \n>> The good news is that for all the real-world use cases I'm aware of,\n>> migration to the new scheme should just work.\n>> \n>> The problem we're discussing here (automatic fallback vs requiring the\n>> gitdir to always exist + using a migration tool) does add a bit of\n>> operational complexity, however it might be manageable.\n>> \n>> I have to see how this works in practice, especially with the users\n>> setting the config (likely I'll add an error to block that path) and the\n>> build-config to enable the extension by default (will likely run the\n>> migration tool automatically on existing repositories if enabled via the\n>> build-config).\n>> \n>> I will attempt this approach in v6.\n>\n> One suggestion: it might be sensible to move auto-migration into a\n> subsequent patch series. That way we can focus on the general approach\n> of the new extension at first, and the potentially-bigger discussion\n> around whether or not to auto-migrate users can then be had separately.\n\nHm, yes, I started implementing the feedback and the closer I get to v6,\nthe more it feels like we should split this series in two or even three\nindependent patches/series:\n\n1. The base gitdir path extension and config infra, renamed as you\n   suggested, to be independent of the actual encoding.\n2. The encoding.\n3. The migration.\n\nLet's see where this takes us. :)\n"},{"id":"532116","messageId":"20251213080817.347922-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 00/10] Add submodulePathConfig extension and gitdir encoding","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:06Z","receivedAt":"2025-12-13T08:09:10Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello everyone,\n\nFor those new to the series, we're implementing a submodule gitdir\nextension which allows us to have a unified way to determine gitdirs\nand do things like encode submodule paths to avoid FS conflicts.\n\nI debated with myself whether to split this into 2 separate series,\nbecause after the latest redesign, our logic is cleanly split in 2:\n\nPatches 1-6 implement the basic mechanisms of the new extension.\nPatches 7-10 add submodule encoding on top.\n\nConsidering that all patches depend upon another and I'd be breaking\nreview continuity and range-diffs, I opted to send v6 as one series.\nIf anyone feels strongly about this, we could split in 2 series or\neven land the patches 1-6 and 7-10 separately.\n\nAs always, this is based on the latest master branch, I've checkd\nfor conflicts with next/seen, pushed to Github [1] and succesfully\nran the CI [2].\n\n1: https://github.com/10ne1/git/tree/dev/aratiu/encoding-v6\n2: https://github.com/10ne1/git/actions/runs/20188634517\n\nChanges in v6:\n* Renamed the extension from submoduleEncoding to submodulePathConfig (Patrick)\n* Decoupled the extension from the encoding conflict resolution (Patrick)\n* Simplified submodule_name_to_gitdir() to error out if gitdir config is missing (Patrick)\n* Not doing automated gitdir fallback migration anymore (Patrick)\n* Added a global config (not repo config) for enabling the extension (Patrick)\n* Added a migration command to submodule--helper (Patrick, Junio)\n* Removed the build-time configuration option (Adrian, Patrick, Junio)\n* Fixed a parallel clone race I introduced while moving gitdir validation (Peff)\n  Was able to reproduce this race when using stress-limit > 30 like the following:\n  make && (cd t && ./t7450-bad-git-dotfiles.sh --stress-limit=50)\n* Added back an extra validation in submodule_move_head() for logic consistency (Patrick)\n* Fixed minor nits, moved function comments to header files and so on (Patrick & Adrian)\n\nv5 -> v6 range-diff:\n 1:  9d5855f3bf =  1:  9d5855f3bf submodule--helper: use submodule_name_to_gitdir in add_submodule\n 3:  7bcadf1116 !  2:  df2d7703f1 submodule: always validate gitdirs inside submodule_name_to_gitdir\n    @@ Commit message\n         after calling submodule_name_to_gitdir() into the function proper,\n         which now always validates the gitdir before returning it.\n     \n    -    This also makes parallel operations a bit safer due to checking and\n    -    erroring out each time the unified API detects a problem instead of\n    -    having one extra hardcoded validation check in submodule--helper.c.\n    +    This simplifies the API and helps to:\n    +    1. Avoid redundant validation calls after submodule_name_to_gitdir().\n    +    2. Avoid the risk of callers forgetting to validate.\n    +    3. Ensure gitdir paths provided by users via configs are always valid\n    +       (config gitdir paths are added in a subsequent commit).\n     \n    -    It simplifies the API usage as well since users who don't have to\n    -    validate the submodule_name_to_gitdir() result themselves anymore\n    -    and reduces the risks of API users forgetting to validate.\n    +    The validation function can still be called as many times as needed\n    +    outside submodule_name_to_gitdir(), for example we keep two calls\n    +    which are still required, to avoid parallel clone races by re-running\n    +    the validation in builtin/submodule-helper.c.\n     \n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n    @@ builtin/submodule--helper.c: static int clone_submodule(const struct module_clon\n      \tif (!file_exists(sm_gitdir)) {\n      \t\tif (clone_data->require_init && !stat(clone_data_path, &st) &&\n      \t\t    !is_empty_dir(clone_data_path))\n    -@@ builtin/submodule--helper.c: static int clone_submodule(const struct module_clone_data *clone_data,\n    - \t\tfree(path);\n    - \t}\n    - \n    --\t/*\n    --\t * We already performed this check at the beginning of this function,\n    --\t * before cloning the objects. This tries to detect racy behavior e.g.\n    --\t * in parallel clones, where another process could easily have made the\n    --\t * gitdir nested _after_ it was created.\n    --\t *\n    --\t * To prevent further harm coming from this unintentionally-nested\n    --\t * gitdir, let's disable it by deleting the `HEAD` file.\n    --\t */\n    --\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) {\n    --\t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n    --\t\tunlink(head);\n    --\t\tfree(head);\n    --\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n    --\t\t      \"git dir\"), sm_gitdir);\n    --\t}\n    --\n    - \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n    - \n    - \tp = repo_submodule_path(the_repository, clone_data_path, \"config\");\n     \n      ## submodule.c ##\n     @@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n    - \n    - \tif (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {\n    - \t\tif (old_head) {\n    --\t\t\tif (!submodule_uses_gitfile(path))\n    --\t\t\t\tabsorb_git_dir_into_superproject(path,\n    --\t\t\t\t\t\t\t\t super_prefix);\n    --\t\t\telse {\n    --\t\t\t\tchar *dotgit = xstrfmt(\"%s/.git\", path);\n    --\t\t\t\tchar *git_dir = xstrdup(read_gitfile(dotgit));\n    --\n    --\t\t\t\tfree(dotgit);\n    --\t\t\t\tif (validate_submodule_git_dir(git_dir,\n    --\t\t\t\t\t\t\t       sub->name) < 0)\n    --\t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n    --\t\t\t\t\t      \"another submodule's git dir\"),\n    --\t\t\t\t\t    git_dir);\n    --\t\t\t\tfree(git_dir);\n    --\t\t\t}\n    -+\t\t\tabsorb_git_dir_into_superproject(path, super_prefix);\n    - \t\t} else {\n      \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n      \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n      \t\t\t\t\t\t sub->name);\n    @@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n      \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n      \t\t\tstrbuf_release(&gitdir);\n      \n    -@@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n    - \treturn ret;\n    - }\n    - \n    --int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    -+static int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n    - {\n    - \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n    - \tchar *p;\n     @@ submodule.c: static void relocate_single_git_dir_into_superproject(const char *path,\n      \t\tdie(_(\"could not lookup name for submodule '%s'\"), path);\n      \n    @@ submodule.c: void submodule_name_to_gitdir(struct strbuf *buf, struct repository\n     +\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n     +\t\t      \"git dir\"), buf->buf);\n      }\n    -\n    - ## submodule.h ##\n    -@@ submodule.h: int submodule_to_gitdir(struct repository *repo,\n    - void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n    - \t\t\t      const char *submodule_name);\n    - \n    --/*\n    -- * Make sure that no submodule's git dir is nested in a sibling submodule's.\n    -- */\n    --int validate_submodule_git_dir(char *git_dir, const char *submodule_name);\n    --\n    - /*\n    -  * Make sure that the given submodule path does not follow symlinks.\n    -  */\n -:  ---------- >  3:  fb5e6aec6a builtin/submodule--helper: add gitdir command\n -:  ---------- >  4:  d3f5cf47bd submodule: introduce extensions.submodulePathConfig\n -:  ---------- >  5:  131cf17a80 submodule: allow runtime enabling extensions.submodulePathConfig\n -:  ---------- >  6:  529c8f0618 submodule--helper: add gitdir migration command\n 2:  8cfa970a9d !  7:  66a7973f39 builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n    @@ Commit message\n         public function exposed via url.h, instead of the old helper path\n         (strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n     \n    -    This function will be used by submodule.c in the next commit.\n    +    This function will be used in subsequent commits which do the encoding.\n     \n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n    @@ url.c\n      #include \"strbuf.h\"\n      #include \"url.h\"\n      \n    -+/*\n    -+ * The set of unreserved characters as per STD66 (RFC3986) is\n    -+ * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n    -+ * components without percent-encoding.\n    -+ */\n     +int is_rfc3986_unreserved(char ch)\n     +{\n     +\treturn isalnum(ch) ||\n    @@ url.h: char *url_decode_parameter_value(const char **query);\n      void end_url_with_slash(struct strbuf *buf, const char *url);\n      void str_end_url_with_slash(const char *url, char **dest);\n      \n    ++/*\n    ++ * The set of unreserved characters as per STD66 (RFC3986) is\n    ++ * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n    ++ * components without percent-encoding.\n    ++ */\n     +int is_rfc3986_unreserved(char ch);\n     +\n      #endif /* URL_H */\n 4:  1b5d0b50ef <  -:  ---------- submodule: add extension to encode gitdir paths\n -:  ---------- >  8:  d8789b3d8f submodule--helper: fix filesystem collisions by encoding gitdir paths\n 5:  2bf1c116a2 !  9:  426c610fe1 submodule: fix case-folding gitdir filesystem colisions\n    @@ Metadata\n     Author: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n      ## Commit message ##\n    -    submodule: fix case-folding gitdir filesystem colisions\n    +    submodule: fix case-folding gitdir filesystem collisions\n     \n    -    Add a new check when extension.submoduleEncoding is enabled to\n    +    Add a new check when extension.submodulePathConfig is enabled, to\n         detect and prevent case-folding filesystem colisions. When this\n         new check is triggered, a stricter casefolding aware URI encoding\n         is used to percent-encode uppercase characters.\n    @@ Commit message\n     \n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n    + ## builtin/submodule--helper.c ##\n    +@@ builtin/submodule--helper.c: static void create_default_gitdir_config(const char *submodule_name)\n    + \t\treturn;\n    + \t}\n    + \n    +-\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n    ++\t/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n    + \tstrbuf_reset(&gitdir_path);\n    + \trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n    + \tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n    +@@ builtin/submodule--helper.c: static void create_default_gitdir_config(const char *submodule_name)\n    + \t\treturn;\n    + \t}\n    + \n    ++\t/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */\n    ++\tstrbuf_reset(&gitdir_path);\n    ++\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n    ++\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n    ++\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n    ++\t\treturn;\n    ++\n    ++\t/* Case 2.3: Try some derived gitdir names, see if one sticks */\n    ++\tfor (char c = '0'; c <= '9'; c++) {\n    ++\t\tstrbuf_reset(&gitdir_path);\n    ++\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n    ++\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n    ++\t\tstrbuf_addch(&gitdir_path, c);\n    ++\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n    ++\t\t\treturn;\n    ++\n    ++\t\tstrbuf_reset(&gitdir_path);\n    ++\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n    ++\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n    ++\t\tstrbuf_addch(&gitdir_path, c);\n    ++\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n    ++\t\t\treturn;\n    ++\t}\n    ++\n    + \t/* Case 3: nothing worked, error out */\n    + \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n    + \t      \"Please ensure it is set, for example by running something like: \"\n    +\n      ## submodule.c ##\n     @@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n      \treturn ret;\n    @@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n     +cleanup:\n     +\tif (dir)\n     +\t\tclosedir(dir);\n    -+\tFREE_AND_NULL(modules_dir);\n    ++\tfree(modules_dir);\n     +\treturn ret;\n     +}\n     +\n      /*\n    -  * Encoded gitdir validation function used when extensions.submoduleEncoding is enabled.\n    +  * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n       * This does not print errors like the non-encoded version, because encoding is supposed\n       * to mitigate / fix all these.\n       */\n    @@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n      \tchar *p = git_dir, *last_submodule_name = NULL;\n     +\tint config_ignorecase = 0;\n      \n    - \tif (!the_repository->repository_format_submodule_encoding)\n    + \tif (!the_repository->repository_format_submodule_path_cfg)\n      \t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n     @@ submodule.c: static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n      \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n    @@ submodule.c: static int validate_submodule_encoded_git_dir(char *git_dir, const\n      \treturn 0;\n      }\n      \n    -@@ submodule.c: void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n    - \tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n    - \t\treturn;\n    - \n    --\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n    -+\t/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n    - \tstrbuf_reset(buf);\n    - \trepo_git_path_append(r, buf, \"modules/\");\n    - \tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n    - \tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n    - \t\treturn;\n    - \n    -+\t/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */\n    -+\tstrbuf_reset(buf);\n    -+\trepo_git_path_append(r, buf, \"modules/\");\n    -+\tstrbuf_addstr_urlencode(buf, submodule_name, is_casefolding_rfc3986_unreserved);\n    -+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n    -+\t\treturn;\n    -+\n    -+\t/* Case 2.3: Try some derived gitdir names, see if one sticks */\n    -+\tfor (char c = '0'; c <= '9'; c++) {\n    -+\t\tstrbuf_reset(buf);\n    -+\t\trepo_git_path_append(r, buf, \"modules/\");\n    -+\t\tstrbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);\n    -+\t\tstrbuf_addch(buf, c);\n    -+\t\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n    -+\t\t\treturn;\n    -+\n    -+\t\tstrbuf_reset(buf);\n    -+\t\trepo_git_path_append(r, buf, \"modules/\");\n    -+\t\tstrbuf_addstr_urlencode(buf, submodule_name, is_casefolding_rfc3986_unreserved);\n    -+\t\tstrbuf_addch(buf, c);\n    -+\t\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n    -+\t\t\treturn;\n    -+\t}\n    -+\n    - \t/* Case 3: Nothing worked: error out */\n    - \tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n    - \t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\n     \n    - ## t/t7425-submodule-encoding.sh ##\n    -@@ t/t7425-submodule-encoding.sh: test_expect_success 'disabling extensions.submoduleEncoding prevents nested subm\n    + ## t/t7425-submodule-gitdir-path-extension.sh ##\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'disabling extensions.submodulePathConfig prevents nested su\n      \t)\n      '\n      \n     +test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are correctly encoded' '\n    -+\tgit clone -c extensions.submoduleEncoding=true main cloned-folding &&\n    ++\tgit clone -c extensions.submodulePathConfig=true main cloned-folding &&\n     +\t(\n     +\t\tcd cloned-folding &&\n     +\n    @@ url.c: int is_rfc3986_unreserved(char ch)\n      \t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n      }\n      \n    -+/*\n    -+ * This is a variant of is_rfc3986_unreserved() that treats uppercase\n    -+ * letters as \"reserved\". This forces them to be percent-encoded, allowing\n    -+ * 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.\n    -+ */\n     +int is_casefolding_rfc3986_unreserved(char c)\n     +{\n     +\treturn (c >= 'a' && c <= 'z') ||\n    @@ url.c: int is_rfc3986_unreserved(char ch)\n      \t/*\n     \n      ## url.h ##\n    -@@ url.h: void end_url_with_slash(struct strbuf *buf, const char *url);\n    - void str_end_url_with_slash(const char *url, char **dest);\n    - \n    +@@ url.h: void str_end_url_with_slash(const char *url, char **dest);\n    +  */\n      int is_rfc3986_unreserved(char ch);\n    -+int is_casefolding_rfc3986_unreserved(char c);\n      \n    ++/*\n    ++ * This is a variant of is_rfc3986_unreserved() that treats uppercase\n    ++ * letters as \"reserved\". This forces them to be percent-encoded, allowing\n    ++ * 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.\n    ++ */\n    ++int is_casefolding_rfc3986_unreserved(char c);\n    ++\n      #endif /* URL_H */\n 6:  b607d7ca39 ! 10:  80b1c1fb59 submodule: use hashed name for gitdir\n    @@ Metadata\n     Author: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n      ## Commit message ##\n    -    submodule: use hashed name for gitdir\n    +    submodule: hash the submodule name for the gitdir path\n     \n    -    If none of the previous steps work and we reach case 2.4, try to\n    -    hash the submodule name and see if that can be a valid gitdir\n    -    before giving up and throwing an error.\n    +    If none of the previous plain-text / encoding / derivation steps work\n    +    and case 2.4 is reached, then try a hash of the submodule name to see\n    +    if that can be a valid gitdir before giving up and throwing an error.\n     \n    -    This is a \"last resort\" type of measure to avoid conflicts since\n    -    it loses the gitdir human readability. Itis not such a big deal\n    -    because users are now supposed to use the submodule.<name>.gitdir\n    -    config as the single source of truth for gitdir paths.\n    -\n    -    This logic will be reached in very rare cases, as can be seen in\n    -    the test we added.\n    +    This is a \"last resort\" type of measure to avoid conflicts since it\n    +    loses the human readability of the gitdir path. This logic will be\n    +    reached in rare cases, as can be seen in the test we added.\n     \n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n    - ## submodule.c ##\n    -@@ submodule.c: static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n    - void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n    - \t\t\t      const char *submodule_name)\n    + ## builtin/submodule--helper.c ##\n    +@@ builtin/submodule--helper.c: static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n    + static void create_default_gitdir_config(const char *submodule_name)\n      {\n    -+\tunsigned char raw_name_hash[GIT_MAX_RAWSZ];\n    -+\tchar hex_name_hash[GIT_MAX_HEXSZ + 1];\n    + \tstruct strbuf gitdir_path = STRBUF_INIT;\n     +\tstruct git_hash_ctx ctx;\n    - \tconst char *gitdir;\n    --\tchar *key;\n    -+\tchar *key, header[128];\n    ++\tchar hex_name_hash[GIT_MAX_HEXSZ + 1], header[128];\n    ++\tunsigned char raw_name_hash[GIT_MAX_RAWSZ];\n     +\tint header_len;\n      \n    - \trepo_git_path_append(r, buf, \"modules/\");\n    - \tstrbuf_addstr(buf, submodule_name);\n    -@@ submodule.c: void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n    + \t/* The config is set only when extensions.submodulePathConfig is enabled */\n    + \tif (!the_repository->repository_format_submodule_path_cfg &&\n    +@@ builtin/submodule--helper.c: static void create_default_gitdir_config(const char *submodule_name)\n      \t\t\treturn;\n      \t}\n      \n    @@ submodule.c: void submodule_name_to_gitdir(struct strbuf *buf, struct repository\n     +\tthe_hash_algo->update_fn(&ctx, submodule_name, strlen(submodule_name));\n     +\tthe_hash_algo->final_fn(raw_name_hash, &ctx);\n     +\thash_to_hex_algop_r(hex_name_hash, raw_name_hash, the_hash_algo);\n    -+\tstrbuf_reset(buf);\n    -+\trepo_git_path_append(r, buf, \"modules/\");\n    -+\tstrbuf_addstr(buf, hex_name_hash);\n    -+\tif (!validate_and_set_submodule_gitdir(buf, submodule_name))\n    ++\tstrbuf_reset(&gitdir_path);\n    ++\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", hex_name_hash);\n    ++\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n    ++\t\tstrbuf_release(&gitdir_path);\n     +\t\treturn;\n    ++\t}\n     +\n    - \t/* Case 3: Nothing worked: error out */\n    - \tdie(_(\"Cannot construct a valid gitdir path for submodule '%s': \"\n    - \t      \"please set a unique git config for 'submodule.%s.gitdir'.\"),\n    + \t/* Case 3: nothing worked, error out */\n    + \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n    + \t      \"Please ensure it is set, for example by running something like: \"\n     \n    - ## t/t7425-submodule-encoding.sh ##\n    -@@ t/t7425-submodule-encoding.sh: test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are corre\n    + ## t/t7425-submodule-gitdir-path-extension.sh ##\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are corre\n      \tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n      '\n      \n     +test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a last resort' '\n    -+\tgit clone -c extensions.submoduleEncoding=true main cloned-hash &&\n    ++\tgit clone -c extensions.submodulePathConfig=true main cloned-hash &&\n     +\t(\n     +\t\tcd cloned-hash &&\n     +\n 7:  9b4890cfd2 <  -:  ---------- meson/Makefile: allow setting submodule encoding at build time\n\nAdrian Ratiu (10):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  submodule: always validate gitdirs inside submodule_name_to_gitdir\n  builtin/submodule--helper: add gitdir command\n  submodule: introduce extensions.submodulePathConfig\n  submodule: allow runtime enabling extensions.submodulePathConfig\n  submodule--helper: add gitdir migration command\n  builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n  submodule--helper: fix filesystem collisions by encoding gitdir paths\n  submodule: fix case-folding gitdir filesystem collisions\n  submodule: hash the submodule name for the gitdir path\n\n Documentation/config/extensions.adoc       |  12 +\n Documentation/config/submodule.adoc        |   7 +\n builtin/credential-store.c                 |   7 +-\n builtin/submodule--helper.c                | 204 +++++++++++-\n repository.c                               |   1 +\n repository.h                               |   1 +\n setup.c                                    |   7 +\n setup.h                                    |   1 +\n submodule.c                                | 160 +++++++--\n submodule.h                                |   2 +\n t/lib-verify-submodule-gitdir-path.sh      |  24 ++\n t/meson.build                              |   1 +\n t/t7425-submodule-gitdir-path-extension.sh | 369 +++++++++++++++++++++\n t/t9902-completion.sh                      |   1 +\n url.c                                      |  13 +\n url.h                                      |  14 +\n 16 files changed, 776 insertions(+), 48 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-gitdir-path-extension.sh\n\n-- \n2.51.2\n\n"},{"id":"532117","messageId":"20251213080817.347922-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 02/10] submodule: always validate gitdirs inside submodule_name_to_gitdir","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:08Z","receivedAt":"2025-12-13T08:09:11Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Move the ad-hoc validation checks sprinkled across the source tree,\nafter calling submodule_name_to_gitdir() into the function proper,\nwhich now always validates the gitdir before returning it.\n\nThis simplifies the API and helps to:\n1. Avoid redundant validation calls after submodule_name_to_gitdir().\n2. Avoid the risk of callers forgetting to validate.\n3. Ensure gitdir paths provided by users via configs are always valid\n   (config gitdir paths are added in a subsequent commit).\n\nThe validation function can still be called as many times as needed\noutside submodule_name_to_gitdir(), for example we keep two calls\nwhich are still required, to avoid parallel clone races by re-running\nthe validation in builtin/submodule-helper.c.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c |  4 ----\n submodule.c                 | 12 ++++--------\n 2 files changed, 4 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 13b5e4ed68..f1fc098614 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1699,10 +1699,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n \t\t\t\t\t\t    clone_data->path);\n \n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\n \tif (!file_exists(sm_gitdir)) {\n \t\tif (clone_data->require_init && !stat(clone_data_path, &st) &&\n \t\t    !is_empty_dir(clone_data_path))\ndiff --git a/submodule.c b/submodule.c\nindex 40a5c6fb9d..f645372a18 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2166,11 +2166,6 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n \t\t\t\t\t\t sub->name);\n-\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n-\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n-\t\t\t\t      \"submodule's git dir\"),\n-\t\t\t\t    gitdir.buf);\n \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n \t\t\tstrbuf_release(&gitdir);\n \n@@ -2349,9 +2344,6 @@ static void relocate_single_git_dir_into_superproject(const char *path,\n \t\tdie(_(\"could not lookup name for submodule '%s'\"), path);\n \n \tsubmodule_name_to_gitdir(&new_gitdir, the_repository, sub->name);\n-\tif (validate_submodule_git_dir(new_gitdir.buf, sub->name) < 0)\n-\t\tdie(_(\"refusing to move '%s' into an existing git dir\"),\n-\t\t    real_old_git_dir);\n \tif (safe_create_leading_directories_const(the_repository, new_gitdir.buf) < 0)\n \t\tdie(_(\"could not create directory '%s'\"), new_gitdir.buf);\n \treal_new_git_dir = real_pathdup(new_gitdir.buf, 1);\n@@ -2600,4 +2592,8 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t */\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n+\n+\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n+\t\t      \"git dir\"), buf->buf);\n }\n-- \n2.51.2\n\n"},{"id":"532118","messageId":"20251213080817.347922-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 03/10] builtin/submodule--helper: add gitdir command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:09Z","receivedAt":"2025-12-13T08:09:15Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This exposes the gitdir name computed by submodule_name_to_gitdir()\ninternally, to make it easier for users and tests to interact with it.\n\nNext commit will add a gitdir configuration, so this helper can also be\nused to easily query that config or validate any gitdir path the user\nsets (submodule_name_to_git_dir now runs the validation logic, since\nour previous commit).\n\nBased-on-patch-by: Brandon Williams <bwilliams.eng@gmail.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex f1fc098614..3bc139ff9c 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1204,6 +1204,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3583,6 +3599,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\n-- \n2.51.2\n\n"},{"id":"532119","messageId":"20251213080817.347922-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 01/10] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:07Z","receivedAt":"2025-12-13T08:09:16Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded modules gitdir path leading to test failures.\n\nCall the submodule_name_to_gitdir() helper instead, which was invented\nexactly for this purpose and is already used by all the other locations\nwhich work on gitdirs.\n\nAlso narrow the scope of the submod_gitdir_path variable which is not\nused anymore in the updated \"else\" branch.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 13 +++++++------\n 1 file changed, 7 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 35f6cf735e..13b5e4ed68 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3183,13 +3183,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n \n static int add_submodule(const struct add_data *add_data)\n {\n-\tchar *submod_gitdir_path;\n \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n \tint ret = -1;\n \n \t/* perhaps the path already exists and is already a git repo, else clone it */\n \tif (is_directory(add_data->sm_path)) {\n+\t\tchar *submod_gitdir_path;\n \t\tstruct strbuf sm_path = STRBUF_INIT;\n \t\tstrbuf_addstr(&sm_path, add_data->sm_path);\n \t\tsubmod_gitdir_path = xstrfmt(\"%s/.git\", add_data->sm_path);\n@@ -3203,10 +3203,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3215,8 +3216,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3234,7 +3235,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.51.2\n\n"},{"id":"532120","messageId":"20251213080817.347922-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 04/10] submodule: introduce extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:10Z","receivedAt":"2025-12-13T08:09:20Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"The idea of this extension is to abstract away the submodule gitdir\npath implementation: everyone is expected to use the config and not\nworry about how the path is computed internally, either in git or\nother implementations.\n\nWith this extension enabled, the submodule.<name>.gitdir repo config\nbecomes the single source of truth for all submodule gitdir paths.\n\nThe submodule.<name>.gitdir config is added automatically for all new\nsubmodules when this extension is enabled.\n\nGit will throw an error if the extension is enabled and a config is\nmissing, advising users how to migrate. Migration is manual for now.\n\nE.g. to add a missing config entry for an existing \"foo\" module:\ngit config submodule.foo.gitdir .git/modules/foo\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |   8 ++\n Documentation/config/submodule.adoc        |   7 ++\n builtin/submodule--helper.c                |  51 ++++++++\n repository.c                               |   1 +\n repository.h                               |   1 +\n setup.c                                    |   7 ++\n setup.h                                    |   1 +\n submodule.c                                |  59 +++++----\n t/lib-verify-submodule-gitdir-path.sh      |  24 ++++\n t/meson.build                              |   1 +\n t/t7425-submodule-gitdir-path-extension.sh | 138 +++++++++++++++++++++\n t/t9902-completion.sh                      |   1 +\n 12 files changed, 274 insertions(+), 25 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-gitdir-path-extension.sh\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 532456644b..6ce1dcc98b 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -73,6 +73,14 @@ relativeWorktrees:::\n \trepaired with either the `--relative-paths` option or with the\n \t`worktree.useRelativePaths` config set to `true`.\n \n+submodulePathConfig:::\n+\tIf enabled, the submodule.<name>.gitdir config is the single source of\n+\ttruth for submodule gitdir paths and is always set for new submodules.\n+\tGit will error if a module does not have submodule.<name>.gitdir set.\n+\tExisting pre-extension submodules need to be migrated by adding the\n+\tmissing config entries. This is done manually for now, e.g. for each\n+\tsubmodule: \"git config submodule.<name>.gitdir .git/modules/<name>\".\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/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 0672d99117..4cf7424cda 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -52,6 +52,13 @@ submodule.<name>.active::\n \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n \tdetails.\n \n+submodule.<name>.gitdir::\n+\tThis sets the gitdir path for submodule <name>. It only works when\n+\t`extensions.submodulePathConfig` is enabled, otherwise it does nothing.\n+\tWhen the extension is enabled, this config is the single source of truth\n+\tfor submodule gitdir paths and git will throw an error if it is missing.\n+\tSee linkgit:git-config[1] for details.\n+\n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\n \tsubmodule's path to determine if the submodule is of interest to git\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 3bc139ff9c..699ac32004 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -435,6 +435,52 @@ struct init_cb {\n };\n #define INIT_CB_INIT { 0 }\n \n+static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n+\t\t\t\t\t     const char *submodule_name)\n+{\n+\tconst char *value;\n+\tchar *key;\n+\n+\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n+\t\treturn -1;\n+\n+\t key = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\n+\t /* Nothing to do if the config already exists. */\n+\tif (!repo_config_get_string_tmp(the_repository, key, &value)) {\n+\t\tfree(key);\n+\t\treturn 0;\n+\t}\n+\n+\tif (repo_config_set_gently(the_repository, key, gitdir_path->buf)) {\n+\t\tfree(key);\n+\t\treturn -1;\n+\t}\n+\n+\tfree(key);\n+\treturn 0;\n+}\n+\n+static void create_default_gitdir_config(const char *submodule_name)\n+{\n+\tstruct strbuf gitdir_path = STRBUF_INIT;\n+\n+\t/* The config is set only when extensions.submodulePathConfig is enabled */\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\treturn;\n+\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n+\tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n+\t      \"Please ensure it is set, for example by running something like: \"\n+\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n+\t    submodule_name, submodule_name, submodule_name);\n+}\n+\n static void init_submodule(const char *path, const char *prefix,\n \t\t\t   const char *super_prefix,\n \t\t\t   unsigned int flags)\n@@ -511,6 +557,9 @@ static void init_submodule(const char *path, const char *prefix,\n \t\tif (repo_config_set_gently(the_repository, sb.buf, upd))\n \t\t\tdie(_(\"Failed to register update mode for submodule path '%s'\"), displaypath);\n \t}\n+\n+\tcreate_default_gitdir_config(sub->name);\n+\n \tstrbuf_release(&sb);\n \tfree(displaypath);\n \tfree(url);\n@@ -3574,6 +3623,8 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \tadd_data.progress = !!progress;\n \tadd_data.dissociate = !!dissociate;\n \n+\tcreate_default_gitdir_config(add_data.sm_name);\n+\n \tif (add_submodule(&add_data))\n \t\tgoto cleanup;\n \tconfigure_added_submodule(&add_data);\ndiff --git a/repository.c b/repository.c\nindex 863f24411b..6cb1247f4b 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -281,6 +281,7 @@ int repo_init(struct repository *repo,\n \trepo->repository_format_worktree_config = format.worktree_config;\n \trepo->repository_format_relative_worktrees = format.relative_worktrees;\n \trepo->repository_format_precious_objects = format.precious_objects;\n+\trepo->repository_format_submodule_path_cfg = format.submodule_path_cfg;\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 6063c4b846..7141237f97 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -165,6 +165,7 @@ struct repository {\n \tint repository_format_worktree_config;\n \tint repository_format_relative_worktrees;\n \tint repository_format_precious_objects;\n+\tint repository_format_submodule_path_cfg;\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 3a6a048620..428427d689 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -686,6 +686,9 @@ static enum extension_result handle_extension(const char *var,\n \t} else if (!strcmp(ext, \"relativeworktrees\")) {\n \t\tdata->relative_worktrees = git_config_bool(var, value);\n \t\treturn EXTENSION_OK;\n+\t} else if (!strcmp(ext, \"submodulepathconfig\")) {\n+\t\tdata->submodule_path_cfg = git_config_bool(var, value);\n+\t\treturn EXTENSION_OK;\n \t}\n \treturn EXTENSION_UNKNOWN;\n }\n@@ -1947,6 +1950,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\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\tthe_repository->repository_format_submodule_path_cfg =\n+\t\t\t\trepo_fmt.submodule_path_cfg;\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@@ -2045,6 +2050,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_submodule_path_cfg =\n+\t\tfmt->submodule_path_cfg;\n \tthe_repository->repository_format_relative_worktrees =\n \t\tfmt->relative_worktrees;\n \tthe_repository->repository_format_partial_clone =\ndiff --git a/setup.h b/setup.h\nindex d55dcc6608..0738dec244 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -167,6 +167,7 @@ struct repository_format {\n \tchar *partial_clone; /* value of extensions.partialclone */\n \tint worktree_config;\n \tint relative_worktrees;\n+\tint submodule_path_cfg;\n \tint is_bare;\n \tint hash_algo;\n \tint compat_hash_algo;\ndiff --git a/submodule.c b/submodule.c\nindex f645372a18..85ca7ea0fb 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2570,30 +2570,39 @@ int submodule_to_gitdir(struct repository *repo,\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n-\t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n-\t */\n-\trepo_git_path_append(r, buf, \"modules/\");\n-\tstrbuf_addstr(buf, submodule_name);\n+\tconst char *gitdir;\n+\tchar *key;\n+\tint ret;\n+\n+\t/* If extensions.submodulePathConfig is disabled, continue to use the plain path */\n+\tif (!r->repository_format_submodule_path_cfg) {\n+\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n+\t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n+\t\t\t      \"git dir\"), buf->buf);\n+\n+\t\treturn; /* plain gitdir is valid for use */\n+\t}\n+\n+\t/* Extension is enabled: use the gitdir config if it exists */\n+\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\tret = repo_config_get_string_tmp(r, key, &gitdir);\n+\tFREE_AND_NULL(key);\n+\n+\tif (!ret) {\n+\t\tstrbuf_addstr(buf, gitdir);\n+\n+\t\t/* validate because users might have modified the config */\n+\t\tif (validate_submodule_git_dir(buf->buf, submodule_name))\n+\t\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n+\t\t\t      \"if it is unique or conflicts with another module\"),\n+\t\t\t    submodule_name, gitdir);\n+\n+\t\treturn; /* gitdir from config is valid for use */\n+\t}\n \n-\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), buf->buf);\n+\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n+\t      \"Please ensure it is set, for example by running something like: \"\n+\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n+\t    submodule_name, submodule_name, submodule_name, submodule_name);\n }\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..62794df976\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,24 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n+\n+# This does not check filesystem existence. That is done in submodule.c via the\n+# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n+# might or might not exist (e.g. when adding a new submodule), so this only\n+# checks the expected configuration path, which might be overridden by the user.\n+\n+verify_submodule_gitdir_path() {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\t# Compute expected absolute path\n+\t\texpected=\"$(git rev-parse --git-common-dir)/$path\" &&\n+\t\texpected=\"$(test-tool path-utils real_path \"$expected\")\" &&\n+\t\t# Compute actual absolute path\n+\t\tactual=\"$(git submodule--helper gitdir \"$name\")\" &&\n+\t\tactual=\"$(test-tool path-utils real_path \"$actual\")\" &&\n+\t\techo \"$expected\" >expect &&\n+\t\techo \"$actual\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/meson.build b/t/meson.build\nindex d3d0be2822..c101faca31 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -886,6 +886,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-gitdir-path-extension.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nnew file mode 100755\nindex 0000000000..5d52a289f8\n--- /dev/null\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -0,0 +1,138 @@\n+#!/bin/sh\n+\n+test_description='submodulePathConfig extension works as expected'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+       git config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed extension submodules' '\n+\tgit init -b main legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init -b main new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init -b main main &&\n+\t(\n+\t\tcd main &&\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\t# trigger the \"die_path_inside_submodule\" check\n+\t\ttest_must_fail git submodule add ../new-sub \"legacy/nested\" &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\tgit submodule add ../new-sub \"New Sub\" &&\n+\t\ttest_commit new &&\n+\n+\t\t# retrigger the \"die_path_inside_submodule\" check with encoding\n+\t\ttest_must_fail git submodule add ../new-sub \"New Sub/nested2\"\n+       )\n+'\n+\n+test_expect_success 'verify new submodule gitdir config' '\n+\tgit -C main config submodule.\"New Sub\".gitdir > actual &&\n+\techo \".git/modules/New Sub\" > expect &&\n+\ttest_cmp expect actual &&\n+\tverify_submodule_gitdir_path main \"New Sub\" \"modules/New Sub\"\n+'\n+\n+test_expect_success 'manual add and verify legacy submodule gitdir config' '\n+\t# the legacy module should not contain a gitdir config, because it\n+\t# was added before the extension was enabled. Add and test it.\n+\ttest_must_fail git -C main config submodule.legacy.gitdir &&\n+\tgit -C main config submodule.legacy.gitdir .git/modules/legacy &&\n+\tgit -C main config submodule.legacy.gitdir > actual &&\n+\techo \".git/modules/legacy\" > expect &&\n+\ttest_cmp expect actual &&\n+\tverify_submodule_gitdir_path main \"legacy\" \"modules/legacy\"\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned-non-extension &&\n+\t(\n+\t\tcd cloned-non-extension &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/\"New Sub\" &&\n+\n+\t\ttest_must_fail git config submodule.legacy.gitdir &&\n+\t\ttest_must_fail git config submodule.\"New Sub\".gitdir &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t) &&\n+\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules main cloned-extension &&\n+\t(\n+\t\tcd cloned-extension &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\tgit config submodule.legacy.gitdir &&\n+\t\tgit config submodule.\"New Sub\".gitdir &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to encoded submodules' '\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C main config receive.denyCurrentBranch updateInstead &&\n+\t(\n+\t\tcd cloned-extension &&\n+\n+\t\tgit -C legacy switch --track -C main origin/main  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C \"New Sub\" switch --track -C main origin/main &&\n+\t\ttest_commit -C \"New Sub\" second-commit &&\n+\t\tgit -C \"New Sub\" push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C main origin/main  &&\n+\t\tgit add legacy \"New Sub\" &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_done\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 964e1f1569..ffb9c8b522 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n \tsubmodule.sub.fetchRecurseSubmodules Z\n \tsubmodule.sub.ignore Z\n \tsubmodule.sub.active Z\n+\tsubmodule.sub.gitdir Z\n \tEOF\n '\n \n-- \n2.51.2\n\n"},{"id":"532121","messageId":"20251213080817.347922-6-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 05/10] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:11Z","receivedAt":"2025-12-13T08:09:24Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This adds the ability to enable the new extension via a runtime\nconfig to avoid having to enable it in each repo configuration.\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |  2 +\n builtin/submodule--helper.c                |  8 +++-\n submodule.c                                |  4 +-\n submodule.h                                |  2 +\n t/t7425-submodule-gitdir-path-extension.sh | 46 ++++++++++++++++++++++\n 5 files changed, 59 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 6ce1dcc98b..929e4e1bf1 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -77,6 +77,8 @@ submodulePathConfig:::\n \tIf enabled, the submodule.<name>.gitdir config is the single source of\n \ttruth for submodule gitdir paths and is always set for new submodules.\n \tGit will error if a module does not have submodule.<name>.gitdir set.\n+\tThis extension can also be enabled as a global runtime config, with\n+\tthe local repository config having precedence (overwrites it).\n \tExisting pre-extension submodules need to be migrated by adding the\n \tmissing config entries. This is done manually for now, e.g. for each\n \tsubmodule: \"git config submodule.<name>.gitdir .git/modules/<name>\".\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 699ac32004..2b5b4f575b 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -466,7 +466,8 @@ static void create_default_gitdir_config(const char *submodule_name)\n \tstruct strbuf gitdir_path = STRBUF_INIT;\n \n \t/* The config is set only when extensions.submodulePathConfig is enabled */\n-\tif (!the_repository->repository_format_submodule_path_cfg)\n+\tif (!the_repository->repository_format_submodule_path_cfg &&\n+\t    !submodule_path_config_enabled)\n \t\treturn;\n \n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n@@ -3483,7 +3484,7 @@ static void die_on_repo_without_commits(const char *path)\n static int module_add(int argc, const char **argv, const char *prefix,\n \t\t      struct repository *repo UNUSED)\n {\n-\tint force = 0, quiet = 0, progress = 0, dissociate = 0;\n+\tint force = 0, quiet = 0, progress = 0, dissociate = 0, path_cfg_ext = 0;\n \tstruct add_data add_data = ADD_DATA_INIT;\n \tconst char *ref_storage_format = NULL;\n \tchar *to_free = NULL;\n@@ -3517,6 +3518,9 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n \n+\tif (!repo_config_get_bool(the_repository, \"extensions.submodulepathconfig\", &path_cfg_ext))\n+\t\tsubmodule_path_config_enabled = path_cfg_ext;\n+\n \tif (!is_writing_gitmodules_ok())\n \t\tdie(_(\"please make sure that the .gitmodules file is in the working tree\"));\n \ndiff --git a/submodule.c b/submodule.c\nindex 85ca7ea0fb..5752909999 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -32,6 +32,8 @@\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n \n+int submodule_path_config_enabled;\n+\n static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n static int initialized_fetch_ref_tips;\n static struct oid_array ref_tips_before_fetch;\n@@ -2575,7 +2577,7 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \tint ret;\n \n \t/* If extensions.submodulePathConfig is disabled, continue to use the plain path */\n-\tif (!r->repository_format_submodule_path_cfg) {\n+\tif (!r->repository_format_submodule_path_cfg && !submodule_path_config_enabled) {\n \t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n \t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n \t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\ndiff --git a/submodule.h b/submodule.h\nindex b10e16e6c0..c15630bf26 100644\n--- a/submodule.h\n+++ b/submodule.h\n@@ -172,4 +172,6 @@ void absorb_git_dir_into_superproject(const char *path,\n  */\n int get_superproject_working_tree(struct strbuf *buf);\n \n+extern int submodule_path_config_enabled;\n+\n #endif\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 5d52a289f8..2f198bff82 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -135,4 +135,50 @@ test_expect_success 'fetch mixed submodule changes and verify updates' '\n \t)\n '\n \n+test_expect_success 'runtime config extensions.submodulePathConfig on new repo' '\n+\tgit config --global extensions.submodulePathConfig true &&\n+\tgit init -b main runtime-test-new-repo &&\n+\t(\n+\t\tcd runtime-test-new-repo &&\n+\n+\t\tgit init -b main sub &&\n+\t\ttest_commit -C sub sub-initial &&\n+\n+\t\tgit submodule add ./sub sub &&\n+\n+\t\t# Verify that the gitdir config was created correctly\n+\t\tgit config submodule.sub.gitdir > actual &&\n+\t\techo \".git/modules/sub\" > expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'runtime config extensions.submodulePathConfig on existing repo' '\n+\t# create a repo with the extension disabled then enable it\n+\tgit config --global extensions.submodulePathConfig false &&\n+\tgit init -b main runtime-test-existing-repo &&\n+\t(\n+\t\tcd runtime-test-existing-repo &&\n+\n+\t\tgit init -b main sub &&\n+\t\ttest_commit -C sub sub-initial &&\n+\n+\t\tgit submodule add ./sub sub &&\n+\n+\t\t# gitdir should not exist for this repo: it must be migrated\n+\t\ttest_must_fail git config submodule.sub.gitdir\n+\t) &&\n+\tgit config --global extensions.submodulePathConfig true &&\n+\t(\n+\t\tcd runtime-test-existing-repo &&\n+\n+\t\tgit submodule add ./sub sub2 &&\n+\n+\t\t# gitdir should exist after enabling the global config\n+\t\tgit config submodule.sub2.gitdir > actual &&\n+\t\techo \".git/modules/sub2\" > expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532122","messageId":"20251213080817.347922-7-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 06/10] submodule--helper: add gitdir migration command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:12Z","receivedAt":"2025-12-13T08:09:27Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Manually running\n\"git config submodule.<name>.gitdir .git/modules/<name>\"\nfor each submodule can be impractical, so add a migration command to\nsubmodule--helper to automatically create configs for all submodules\nas required by extensions.submodulePathConfig.\n\nThe command calls create_default_gitdir_config() which validates the\ngitdir paths before adding the configs.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |  6 ++-\n builtin/submodule--helper.c                | 58 ++++++++++++++++++++++\n t/t7425-submodule-gitdir-path-extension.sh | 34 +++++++++++++\n 3 files changed, 96 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 929e4e1bf1..63c8727c3b 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -80,8 +80,10 @@ submodulePathConfig:::\n \tThis extension can also be enabled as a global runtime config, with\n \tthe local repository config having precedence (overwrites it).\n \tExisting pre-extension submodules need to be migrated by adding the\n-\tmissing config entries. This is done manually for now, e.g. for each\n-\tsubmodule: \"git config submodule.<name>.gitdir .git/modules/<name>\".\n+\tmissing config entries. This can be done manually, e.g. for each\n+\tsubmodule: \"git config submodule.<name>.gitdir .git/modules/<name>\",\n+\tor via the \"git submodule--helper migrate-gitdir-configs\" command\n+\twhich iterates over all submodules and attempts to migrate them.\n \n worktreeConfig:::\n \tIf enabled, then worktrees will load config settings from the\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 2b5b4f575b..458dc863df 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1270,6 +1270,63 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n \treturn 0;\n }\n \n+static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n+\t\t\t  const char *prefix UNUSED, struct repository *repo)\n+{\n+\tstruct strbuf module_dir = STRBUF_INIT;\n+\tDIR *dir;\n+\tstruct dirent *de;\n+\n+\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n+\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n+\t\t      \"Please enable it for migration to work, for example: \"\n+\t\t      \"git config core.repositoryformatversion 1\"));\n+\n+\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n+\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n+\t\t      \"for migration to work. Please enable it in the root repo: \"\n+\t\t      \"git config extensions.submodulePathConfig true\"));\n+\n+\trepo->repository_format_submodule_path_cfg = 1;\n+\n+\trepo_git_path_append(repo, &module_dir, \"modules/\");\n+\n+\tdir = opendir(module_dir.buf);\n+\tif (!dir)\n+\t\tdie(_(\"could not open '%s'\"), module_dir.buf);\n+\n+\twhile ((de = readdir(dir))) {\n+\t\tstruct strbuf gitdir_path = STRBUF_INIT;\n+\t\tchar *key;\n+\t\tconst char *value;\n+\n+\t\tif (is_dot_or_dotdot(de->d_name))\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_addf(&gitdir_path, \"%s/%s\", module_dir.buf, de->d_name);\n+\t\tif (!is_git_directory(gitdir_path.buf)) {\n+\t\t\tstrbuf_release(&gitdir_path);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tstrbuf_release(&gitdir_path);\n+\n+\t\tkey = xstrfmt(\"submodule.%s.gitdir\", de->d_name);\n+\t\tif (!repo_config_get_string_tmp(repo, key, &value)) {\n+\t\t\t/* Already has a gitdir config, nothing to do. */\n+\t\t\tfree(key);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfree(key);\n+\n+\t\tcreate_default_gitdir_config(de->d_name);\n+\t}\n+\n+\tclosedir(dir);\n+\tstrbuf_release(&module_dir);\n+\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3654,6 +3711,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"migrate-gitdir-configs\", &fn, module_migrate),\n \t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 2f198bff82..b7f0e8cdf4 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -181,4 +181,38 @@ test_expect_success 'runtime config extensions.submodulePathConfig on existing r\n \t)\n '\n \n+test_expect_success 'submodule--helper migrates legacy modules' '\n+\tgit init sm-repo-1 &&\n+\ttest_commit -C sm-repo-1 initial-1 &&\n+\tgit init sm-repo-2 &&\n+\ttest_commit -C sm-repo-2 initial-2 &&\n+\n+\t# ensure the global config is disabled so we can actually test migration\n+\tgit config --global extensions.submodulePathConfig false &&\n+\n+\tgit init -b main migrate-test &&\n+\t(\n+\t\tcd migrate-test &&\n+\n+\t\tgit submodule add ../sm-repo-1 sub1 &&\n+\t\tgit submodule add ../sm-repo-2 sub2 &&\n+\t\ttest_commit add-submodules &&\n+\n+\t\t# gitdir configs should not exist\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\n+\t\tgit submodule--helper migrate-gitdir-configs &&\n+\n+\t\t# gitdir configs must exist after migration\n+\t\tgit config submodule.sub1.gitdir >actual &&\n+\t\techo \".git/modules/sub1\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit config submodule.sub2.gitdir >actual &&\n+\t\techo \".git/modules/sub2\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532123","messageId":"20251213080817.347922-8-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 07/10] builtin/credential-store: move is_rfc3986_unreserved to url.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:13Z","receivedAt":"2025-12-13T08:09:28Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"is_rfc3986_unreserved() was moved to credential-store.c and was made\nstatic by f89854362c (credential-store: move related functions to\ncredential-store file, 2023-06-06) under a correct assumption, at the\ntime, that it was the only place using it.\n\nHowever now we need it to apply URL-encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back as a\npublic function exposed via url.h, instead of the old helper path\n(strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n\nThis function will be used in subsequent commits which do the encoding.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c | 7 +------\n url.c                      | 6 ++++++\n url.h                      | 7 +++++++\n 3 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..bc1453c6b2 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -7,6 +7,7 @@\n #include \"path.h\"\n #include \"string-list.h\"\n #include \"parse-options.h\"\n+#include \"url.h\"\n #include \"write-or-die.h\"\n \n static struct lock_file credential_lock;\n@@ -76,12 +77,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/url.c b/url.c\nindex 282b12495a..adc289229c 100644\n--- a/url.c\n+++ b/url.c\n@@ -3,6 +3,12 @@\n #include \"strbuf.h\"\n #include \"url.h\"\n \n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex 2a27c34277..e644c3c809 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,4 +21,11 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+/*\n+ * The set of unreserved characters as per STD66 (RFC3986) is\n+ * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n+ * components without percent-encoding.\n+ */\n+int is_rfc3986_unreserved(char ch);\n+\n #endif /* URL_H */\n-- \n2.51.2\n\n"},{"id":"532124","messageId":"20251213080817.347922-9-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 08/10] submodule--helper: fix filesystem collisions by encoding gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:14Z","receivedAt":"2025-12-13T08:09:33Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Fix nested filesystem collisions by url-encoding gitdir paths stored\nin submodule.%s.gitdir, when extensions.submodulePathConfig is enabled.\n\nCredit goes to Junio and Patrick for coming up with this design: the\nencoding is only applied when necessary, to newly added submodules.\n\nExisting modules don't need the encoding because git already errors\nout when detecting nested gitdirs before this patch.\n\nThis commit adds the basic url-encoding and some tests. Next commits\nextend the encode -> validate -> retry loop to fix more conflicts.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 12 +++++\n submodule.c                                | 42 +++++++++++++++-\n t/t7425-submodule-gitdir-path-extension.sh | 57 ++++++++++++++++++++++\n 3 files changed, 110 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 458dc863df..2a8d3a9d92 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -34,6 +34,7 @@\n #include \"list-objects-filter-options.h\"\n #include \"wildmatch.h\"\n #include \"strbuf.h\"\n+#include \"url.h\"\n \n #define OPT_QUIET (1 << 0)\n #define OPT_CACHED (1 << 1)\n@@ -470,12 +471,23 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t    !submodule_path_config_enabled)\n \t\treturn;\n \n+\t/* Case 1: try the plain module name */\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n \tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n \t\tstrbuf_release(&gitdir_path);\n \t\treturn;\n \t}\n \n+\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n+\t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\n \t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\ndiff --git a/submodule.c b/submodule.c\nindex 5752909999..bf6db1f2b9 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -31,6 +31,7 @@\n #include \"commit-reach.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"url.h\"\n \n int submodule_path_config_enabled;\n \n@@ -2247,12 +2248,43 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+/*\n+ * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n+ * This does not print errors like the non-encoded version, because encoding is supposed\n+ * to mitigate / fix all these.\n+ */\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+{\n+\tconst char *modules_marker = \"/modules/\";\n+\tchar *p = git_dir, *last_submodule_name = NULL;\n+\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n+\t\t    \"extensions.submodulePathConfig enabled.\");\n+\n+\t/* Find the last submodule name in the gitdir path (modules can be nested). */\n+\twhile ((p = strstr(p, modules_marker))) {\n+\t\tlast_submodule_name = p + strlen(modules_marker);\n+\t\tp++;\n+\t}\n+\n+\t/* Prevent the use of '/' in encoded names */\n+\tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+static int validate_submodule_legacy_git_dir(char *git_dir, const char *submodule_name)\n {\n \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n \tchar *p;\n \tint ret = 0;\n \n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tBUG(\"validate_submodule_git_dir() must be called with \"\n+\t\t    \"extensions.submodulePathConfig disabled.\");\n+\n \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n \t    strcmp(p, submodule_name))\n \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n@@ -2288,6 +2320,14 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \treturn 0;\n }\n \n+int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+{\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\treturn validate_submodule_legacy_git_dir(git_dir, submodule_name);\n+\n+\treturn validate_submodule_encoded_git_dir(git_dir, submodule_name);\n+}\n+\n int validate_submodule_path(const char *path)\n {\n \tchar *p = xstrdup(path);\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex b7f0e8cdf4..7c4f87b79e 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -215,4 +215,61 @@ test_expect_success 'submodule--helper migrates legacy modules' '\n \t)\n '\n \n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of encoded sibling submodules must not be nested' '\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules nested clone_nested &&\n+\n+\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n+\tgit -C clone_nested config submodule.hippo.gitdir > actual &&\n+\ttest_grep \"\\.git/modules/hippo$\" actual &&\n+\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks &&\n+\tgit -C clone_nested config submodule.hippo/hooks.gitdir > actual &&\n+\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules --jobs=2 nested clone_parallel &&\n+\n+\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n+\tgit -C clone_nested config submodule.hippo.gitdir > actual &&\n+\ttest_grep \"\\.git/modules/hippo$\" actual &&\n+\n+\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks &&\n+\tgit -C clone_nested config submodule.hippo/hooks.gitdir > actual &&\n+\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n+'\n+\n+test_expect_success 'disabling extensions.submodulePathConfig prevents nested submodules' '\n+\t(\n+\t\tcd clone_nested &&\n+\t\t# disable extension and verify failure\n+\t\tgit config extensions.submodulePathConfig false &&\n+\t\ttest_must_fail git submodule add ./thing2 hippo/foobar &&\n+\t\t# re-enable extension and verify it works\n+\t\tgit config extensions.submodulePathConfig true &&\n+\t\tgit submodule add ./thing2 hippo/foobar\n+\t)\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532125","messageId":"20251213080817.347922-10-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 09/10] submodule: fix case-folding gitdir filesystem collisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:15Z","receivedAt":"2025-12-13T08:09:37Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new check when extension.submodulePathConfig is enabled, to\ndetect and prevent case-folding filesystem colisions. When this\nnew check is triggered, a stricter casefolding aware URI encoding\nis used to percent-encode uppercase characters.\n\nBy using this check/retry mechanism the uppercase encoding is\nonly applied when necessary, so case-sensitive filesystems are\nnot affected.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 26 ++++++++++-\n submodule.c                                | 53 +++++++++++++++++++++-\n t/t7425-submodule-gitdir-path-extension.sh | 35 ++++++++++++++\n url.c                                      |  7 +++\n url.h                                      |  7 +++\n 5 files changed, 126 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 2a8d3a9d92..5851d49b28 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -478,7 +478,7 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\treturn;\n \t}\n \n-\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\t/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n \tstrbuf_reset(&gitdir_path);\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n \tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n@@ -487,6 +487,30 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\treturn;\n \t}\n \n+\t/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\treturn;\n+\n+\t/* Case 2.3: Try some derived gitdir names, see if one sticks */\n+\tfor (char c = '0'; c <= '9'; c++) {\n+\t\tstrbuf_reset(&gitdir_path);\n+\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n+\t\tstrbuf_addch(&gitdir_path, c);\n+\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\t\treturn;\n+\n+\t\tstrbuf_reset(&gitdir_path);\n+\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n+\t\tstrbuf_addch(&gitdir_path, c);\n+\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\t\treturn;\n+\t}\n+\n \t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\ndiff --git a/submodule.c b/submodule.c\nindex bf6db1f2b9..ae131a56b2 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2248,15 +2248,58 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n+static int check_casefolding_conflict(const char *git_dir,\n+\t\t\t\t      const char *submodule_name,\n+\t\t\t\t      const bool suffixes_match)\n+{\n+\tchar *p, *modules_dir = xstrdup(git_dir);\n+\tstruct dirent *de;\n+\tDIR *dir = NULL;\n+\tint ret = 0;\n+\n+\tif ((p = find_last_dir_sep(modules_dir)))\n+\t\t*p = '\\0';\n+\n+\t/* No conflict is possible if modules_dir doesn't exist (first clone) */\n+\tif (!is_directory(modules_dir))\n+\t\tgoto cleanup;\n+\n+\tdir = opendir(modules_dir);\n+\tif (!dir) {\n+\t\tret = -1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/* Check for another directory under .git/modules that differs only in case. */\n+\twhile ((de = readdir(dir))) {\n+\t\tif (!strcmp(de->d_name, \".\") || !strcmp(de->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tif ((suffixes_match || is_git_directory(git_dir)) &&\n+\t\t    !strcasecmp(de->d_name, submodule_name) &&\n+\t\t    strcmp(de->d_name, submodule_name)) {\n+\t\t\tret = -1; /* collision found */\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+cleanup:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tfree(modules_dir);\n+\treturn ret;\n+}\n+\n /*\n  * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n  * This does not print errors like the non-encoded version, because encoding is supposed\n  * to mitigate / fix all these.\n  */\n-static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name)\n {\n \tconst char *modules_marker = \"/modules/\";\n \tchar *p = git_dir, *last_submodule_name = NULL;\n+\tint config_ignorecase = 0;\n \n \tif (!the_repository->repository_format_submodule_path_cfg)\n \t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n@@ -2272,6 +2315,14 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n \t\treturn -1;\n \n+\t/* Prevent conflicts on case-folding filesystems */\n+\trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n+\tif (ignore_case || config_ignorecase) {\n+\t\tbool suffixes_match = !strcmp(last_submodule_name, submodule_name);\n+\t\treturn check_casefolding_conflict(git_dir, submodule_name,\n+\t\t\t\t\t\t  suffixes_match);\n+\t}\n+\n \treturn 0;\n }\n \ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 7c4f87b79e..a3c6c0c6b9 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -272,4 +272,39 @@ test_expect_success 'disabling extensions.submodulePathConfig prevents nested su\n \t)\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are correctly encoded' '\n+\tgit clone -c extensions.submodulePathConfig=true main cloned-folding &&\n+\t(\n+\t\tcd cloned-folding &&\n+\n+\t\t# conflict: the \"folding\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"folding\" &&\n+\t\ttest_commit lowercase &&\n+\t\tgit submodule add ../new-sub \"FoldinG\" &&\n+\t\ttest_commit uppercase &&\n+\n+\t\t# conflict: the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"FOO\" &&\n+\t\ttest_commit uppercase-foo &&\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\ttest_commit lowercase-foo &&\n+\n+\t\t# create a multi conflict between foobar, fooBar and foo%42ar\n+\t\t# the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"foobar\" &&\n+\t\ttest_commit lowercase-foobar &&\n+\t\tgit submodule add ../new-sub \"foo%42ar\" &&\n+\t\ttest_commit encoded-foo%42ar &&\n+\t\tgit submodule add ../new-sub \"fooBar\" &&\n+\t\ttest_commit mixed-fooBar\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-folding \"folding\" \"modules/folding\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FoldinG\" \"modules/%46oldin%47\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FOO\" \"modules/FOO\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foobar\" \"modules/foobar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo%42ar\" \"modules/foo%42ar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n+'\n+\n test_done\ndiff --git a/url.c b/url.c\nindex adc289229c..3ca5987e90 100644\n--- a/url.c\n+++ b/url.c\n@@ -9,6 +9,13 @@ int is_rfc3986_unreserved(char ch)\n \t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n }\n \n+int is_casefolding_rfc3986_unreserved(char c)\n+{\n+\treturn (c >= 'a' && c <= 'z') ||\n+\t       (c >= '0' && c <= '9') ||\n+\t       c == '-' || c == '.' || c == '_' || c == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex e644c3c809..cd9140e994 100644\n--- a/url.h\n+++ b/url.h\n@@ -28,4 +28,11 @@ void str_end_url_with_slash(const char *url, char **dest);\n  */\n int is_rfc3986_unreserved(char ch);\n \n+/*\n+ * This is a variant of is_rfc3986_unreserved() that treats uppercase\n+ * letters as \"reserved\". This forces them to be percent-encoded, allowing\n+ * 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.\n+ */\n+int is_casefolding_rfc3986_unreserved(char c);\n+\n #endif /* URL_H */\n-- \n2.51.2\n\n"},{"id":"532126","messageId":"20251213080817.347922-11-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"[PATCH v6 10/10] submodule: hash the submodule name for the gitdir path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-13T08:08:16Z","receivedAt":"2025-12-13T08:09:41Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"If none of the previous plain-text / encoding / derivation steps work\nand case 2.4 is reached, then try a hash of the submodule name to see\nif that can be a valid gitdir before giving up and throwing an error.\n\nThis is a \"last resort\" type of measure to avoid conflicts since it\nloses the human readability of the gitdir path. This logic will be\nreached in rare cases, as can be seen in the test we added.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 19 +++++++\n t/t7425-submodule-gitdir-path-extension.sh | 59 ++++++++++++++++++++++\n 2 files changed, 78 insertions(+)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 5851d49b28..3cbacadbe8 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -465,6 +465,10 @@ static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n static void create_default_gitdir_config(const char *submodule_name)\n {\n \tstruct strbuf gitdir_path = STRBUF_INIT;\n+\tstruct git_hash_ctx ctx;\n+\tchar hex_name_hash[GIT_MAX_HEXSZ + 1], header[128];\n+\tunsigned char raw_name_hash[GIT_MAX_RAWSZ];\n+\tint header_len;\n \n \t/* The config is set only when extensions.submodulePathConfig is enabled */\n \tif (!the_repository->repository_format_submodule_path_cfg &&\n@@ -511,6 +515,21 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\t\treturn;\n \t}\n \n+\t/* Case 2.4: If all the above failed, try a hash of the name as a last resort */\n+\theader_len = snprintf(header, sizeof(header), \"blob %zu\", strlen(submodule_name));\n+\tthe_hash_algo->init_fn(&ctx);\n+\tthe_hash_algo->update_fn(&ctx, header, header_len);\n+\tthe_hash_algo->update_fn(&ctx, \"\\0\", 1);\n+\tthe_hash_algo->update_fn(&ctx, submodule_name, strlen(submodule_name));\n+\tthe_hash_algo->final_fn(raw_name_hash, &ctx);\n+\thash_to_hex_algop_r(hex_name_hash, raw_name_hash, the_hash_algo);\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", hex_name_hash);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n \t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex a3c6c0c6b9..1599d28587 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -307,4 +307,63 @@ test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are corre\n \tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a last resort' '\n+\tgit clone -c extensions.submodulePathConfig=true main cloned-hash &&\n+\t(\n+\t\tcd cloned-hash &&\n+\n+\t\t# conflict: add all submodule conflicting variants until we reach the\n+\t\t# final hashing conflict resolution for submodule \"foo\"\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\tgit submodule add ../new-sub \"foo0\" &&\n+\t\tgit submodule add ../new-sub \"foo1\" &&\n+\t\tgit submodule add ../new-sub \"foo2\" &&\n+\t\tgit submodule add ../new-sub \"foo3\" &&\n+\t\tgit submodule add ../new-sub \"foo4\" &&\n+\t\tgit submodule add ../new-sub \"foo5\" &&\n+\t\tgit submodule add ../new-sub \"foo6\" &&\n+\t\tgit submodule add ../new-sub \"foo7\" &&\n+\t\tgit submodule add ../new-sub \"foo8\" &&\n+\t\tgit submodule add ../new-sub \"foo9\" &&\n+\t\tgit submodule add ../new-sub \"%46oo\" &&\n+\t\tgit submodule add ../new-sub \"%46oo0\" &&\n+\t\tgit submodule add ../new-sub \"%46oo1\" &&\n+\t\tgit submodule add ../new-sub \"%46oo2\" &&\n+\t\tgit submodule add ../new-sub \"%46oo3\" &&\n+\t\tgit submodule add ../new-sub \"%46oo4\" &&\n+\t\tgit submodule add ../new-sub \"%46oo5\" &&\n+\t\tgit submodule add ../new-sub \"%46oo6\" &&\n+\t\tgit submodule add ../new-sub \"%46oo7\" &&\n+\t\tgit submodule add ../new-sub \"%46oo8\" &&\n+\t\tgit submodule add ../new-sub \"%46oo9\" &&\n+\t\ttest_commit add-foo-variants &&\n+\t\tgit submodule add ../new-sub \"Foo\" &&\n+\t\ttest_commit add-uppercase-foo\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo\" \"modules/foo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo0\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo1\" \"modules/foo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo2\" \"modules/foo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo3\" \"modules/foo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo4\" \"modules/foo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo5\" \"modules/foo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo6\" \"modules/foo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo7\" \"modules/foo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo8\" \"modules/foo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo9\" \"modules/foo9\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo\" \"modules/%46oo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo0\" \"modules/%46oo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo1\" \"modules/%46oo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo2\" \"modules/%46oo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo3\" \"modules/%46oo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo4\" \"modules/%46oo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo5\" \"modules/%46oo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo6\" \"modules/%46oo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo7\" \"modules/%46oo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo8\" \"modules/%46oo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo9\" \"modules/%46oo9\" &&\n+\thash=$(printf \"Foo\" | git hash-object --stdin) &&\n+\tverify_submodule_gitdir_path cloned-hash \"Foo\" \"modules/${hash}\"\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532134","messageId":"34DD8798-5C69-4092-B6C9-6609E688FBE8@gmail.com","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v6 00/10] Add submodulePathConfig extension and gitdir encoding","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-13T14:03:06Z","receivedAt":"2025-12-13T14:03:17Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 13 déc. 2025 à 03:09, Adrian Ratiu <adrian.ratiu@collabora.com> a écrit :\n> \n> ﻿Hello everyone,\n> \n> For those new to the series, we're implementing a submodule gitdir\n> extension which allows us to have a unified way to determine gitdirs\n> and do things like encode submodule paths to avoid FS conflicts.\n\nHi there, I admit I haven’t followed this series closely. I use submodules quite a bit but haven’t yet peered into the depths of the implementation.\n\nI read over the documentation changes in this series, and it’s not clear to me how or why I would use this new feature (I don’t mean there’s no benefit! Just that I’m having a hard time parsing it out.). By “how” I mean: I can see how to set config and run the migrator; what does that unlock for me to now go and do?\n\nDoes one of the previous cover letters explain how this is useful to submodule users? If so which, and perhaps the docs could also contain a “here’s when/why you might want this extension enabled and what it allows you to do”?\n\nOr maybe this is meant to be not too user-facing, in which case I’m curious who would turn this on and why still :)\n\nAgain, I am mostly curious, so please don’t read this as an attempt to hold the series hostage! :)\n\nBest,\nBen"},{"id":"532195","messageId":"87pl8flnef.fsf@gentoo.mail-host-address-is-not-set","threadId":"63967","inReplyTo":"34DD8798-5C69-4092-B6C9-6609E688FBE8@gmail.com","subject":"Re: [PATCH v6 00/10] Add submodulePathConfig extension and gitdir encoding","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-15T16:28:56Z","receivedAt":"2025-12-15T16:29:21Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Sat, 13 Dec 2025, Ben Knoble <ben.knoble@gmail.com> wrote:\n>> Le 13 déc. 2025 à 03:09, Adrian Ratiu <adrian.ratiu@collabora.com> a écrit :\n>> \n>> ﻿Hello everyone,\n>> \n>> For those new to the series, we're implementing a submodule gitdir\n>> extension which allows us to have a unified way to determine gitdirs\n>> and do things like encode submodule paths to avoid FS conflicts.\n>\n> Hi there, I admit I haven’t followed this series closely. I use submodules quite a bit but haven’t yet peered into the depths of the implementation.\n>\n> I read over the documentation changes in this series, and it’s not clear to me how or why I would use this new feature (I don’t mean there’s no benefit! Just that I’m having a hard time parsing it out.). By “how” I mean: I can see how to set config and run the migrator; what does that unlock for me to now go and do?\n>\n> Does one of the previous cover letters explain how this is useful to submodule users? If so which, and perhaps the docs could also contain a “here’s when/why you might want this extension enabled and what it allows you to do”?\n>\n> Or maybe this is meant to be not too user-facing, in which case I’m curious who would turn this on and why still :)\n>\n> Again, I am mostly curious, so please don’t read this as an attempt to hold the series hostage! :)\n\nIt's perfectly ok to ask, no problem. :)\n\nThis series is for the minority of users who either:\n\n1. Encounter errors like the following in submodule.c:\n   die(_(\"refusing to create/use '%s' in another submodule's \"...)\n\n   These errors can happen due to a number of factors, like\n   case-insensitive filesystems or submodule layouts.\n\n2. Need to specify non-standard gitdir repository paths, different from\n   the currently hardcoded .git/modules/<plain-name> location.\n\n   With this series, the gitdir config becomes the unified way to\n   set/get the gitdir paths, so you can move them around as needed.\n   It also helps other git implementations who don't need to exactly\n   match git's behaviour: the config becomes the standard interface.\n\nIf you are not in one of the two above cases, then there is no reason to\nenable this and it won't affect you.\n\nHope this is clear, maybe we could spell it out better in the\ndocumentation (suggestions welcome btw) or even tell users in the error\nmessages to enable this extension.\n"},{"id":"532225","messageId":"xmqq1pkv6ydn.fsf@gitster.g","threadId":"63967","inReplyTo":"87pl8flnef.fsf@gentoo.mail-host-address-is-not-set","subject":"Re: [PATCH v6 00/10] Add submodulePathConfig extension and gitdir encoding","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-16T00:53:08Z","receivedAt":"2025-12-16T00:53:10Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> 1. Encounter errors like the following in submodule.c:\n>    die(_(\"refusing to create/use '%s' in another submodule's \"...)\n>\n>    These errors can happen due to a number of factors, like\n>    case-insensitive filesystems or submodule layouts.\n>\n> 2. Need to specify non-standard gitdir repository paths, different from\n>    the currently hardcoded .git/modules/<plain-name> location.\n\nUnlike 1. that hints where the need might come from (e.g., the\nmention of case insensitivity), 2. has no hint on why one may want\nto use \"non-standard gitdir\", which is better than nothing but\nprobably still not helpful enough.\n\nPerhaps giving a concrete example or two in the documentation may\nhelp?  \"Imagine you have submodule X at path P on the master branch,\nand then you want to add another submodule Y at path Q on a separate\nbranch that does not have the submodule X yet.  If path P and path Q\noverlaps THIS WAY, then THIS AND THAT BAD THINGS HAPPEN.  This feature\nlets users work this around by DOING THIS AND THAT\".\n\n> If you are not in one of the two above cases, then there is no reason to\n> enable this and it won't affect you.\n>\n> Hope this is clear, maybe we could spell it out better in the\n> documentation (suggestions welcome btw) or even tell users in the error\n> messages to enable this extension.\n\nAbsolutlely.  Developers answering questions only here will waste\nthe brain cycles spent while coming up with the answer, so please do\ndocument what audiences the feature is meant to help.\n"},{"id":"532249","messageId":"aUEh0tqUra-Y_yZd@pks.im","threadId":"63967","inReplyTo":"20251213080817.347922-3-adrian.ratiu@collabora.com","subject":"Re: [PATCH v6 02/10] submodule: always validate gitdirs inside submodule_name_to_gitdir","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-16T09:09:38Z","receivedAt":"2025-12-16T09:09:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 13, 2025 at 10:08:08AM +0200, Adrian Ratiu wrote:\n> Move the ad-hoc validation checks sprinkled across the source tree,\n> after calling submodule_name_to_gitdir() into the function proper,\n> which now always validates the gitdir before returning it.\n> \n> This simplifies the API and helps to:\n> 1. Avoid redundant validation calls after submodule_name_to_gitdir().\n> 2. Avoid the risk of callers forgetting to validate.\n> 3. Ensure gitdir paths provided by users via configs are always valid\n>    (config gitdir paths are added in a subsequent commit).\n> \n> The validation function can still be called as many times as needed\n> outside submodule_name_to_gitdir(), for example we keep two calls\n> which are still required, to avoid parallel clone races by re-running\n> the validation in builtin/submodule-helper.c.\n\nYup, this looks obviously correct now as the sites where we remove calls\nto `validate_submodule_git_dir()` are all sites where we call\n`submodule_name_to_gitdir()` immediately before.\n\nPatrick\n"},{"id":"532250","messageId":"aUEh14H242nm0NcE@pks.im","threadId":"63967","inReplyTo":"20251213080817.347922-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH v6 04/10] submodule: introduce extensions.submodulePathConfig","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-16T09:09:43Z","receivedAt":"2025-12-16T09:09:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 13, 2025 at 10:08:10AM +0200, Adrian Ratiu wrote:\n> diff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\n> index 532456644b..6ce1dcc98b 100644\n> --- a/Documentation/config/extensions.adoc\n> +++ b/Documentation/config/extensions.adoc\n> @@ -73,6 +73,14 @@ relativeWorktrees:::\n>  \trepaired with either the `--relative-paths` option or with the\n>  \t`worktree.useRelativePaths` config set to `true`.\n>  \n> +submodulePathConfig:::\n> +\tIf enabled, the submodule.<name>.gitdir config is the single source of\n> +\ttruth for submodule gitdir paths and is always set for new submodules.\n> +\tGit will error if a module does not have submodule.<name>.gitdir set.\n> +\tExisting pre-extension submodules need to be migrated by adding the\n> +\tmissing config entries. This is done manually for now, e.g. for each\n> +\tsubmodule: \"git config submodule.<name>.gitdir .git/modules/<name>\".\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\n\nYup, makes sense.\n\n> diff --git a/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\n> index 0672d99117..4cf7424cda 100644\n> --- a/Documentation/config/submodule.adoc\n> +++ b/Documentation/config/submodule.adoc\n> @@ -52,6 +52,13 @@ submodule.<name>.active::\n>  \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n>  \tdetails.\n>  \n> +submodule.<name>.gitdir::\n> +\tThis sets the gitdir path for submodule <name>. It only works when\n> +\t`extensions.submodulePathConfig` is enabled, otherwise it does nothing.\n\nThis reads a tiny bit awkward. How about: \"This configuration is only\nrespected when...\"?\n\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index 3bc139ff9c..699ac32004 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -435,6 +435,52 @@ struct init_cb {\n>  };\n>  #define INIT_CB_INIT { 0 }\n>  \n> +static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n> +\t\t\t\t\t     const char *submodule_name)\n> +{\n> +\tconst char *value;\n> +\tchar *key;\n> +\n> +\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n> +\t\treturn -1;\n> +\n> +\t key = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n> +\n> +\t /* Nothing to do if the config already exists. */\n\nNit: additional leading space.\n\n> +static void create_default_gitdir_config(const char *submodule_name)\n> +{\n> +\tstruct strbuf gitdir_path = STRBUF_INIT;\n> +\n> +\t/* The config is set only when extensions.submodulePathConfig is enabled */\n> +\tif (!the_repository->repository_format_submodule_path_cfg)\n> +\t\treturn;\n> +\n> +\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n> +\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n> +\t\tstrbuf_release(&gitdir_path);\n> +\t\treturn;\n> +\t}\n> +\n> +\tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n> +\t      \"Please ensure it is set, for example by running something like: \"\n> +\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n> +\t    submodule_name, submodule_name, submodule_name);\n> +}\n> +\n>  static void init_submodule(const char *path, const char *prefix,\n>  \t\t\t   const char *super_prefix,\n>  \t\t\t   unsigned int flags)\n\nOkay, here we populate the configuration if and only if the repository\nextension is enabled. Makes sense.\n\n> diff --git a/submodule.c b/submodule.c\n> index f645372a18..85ca7ea0fb 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2570,30 +2570,39 @@ int submodule_to_gitdir(struct repository *repo,\n>  void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \t\t\t      const char *submodule_name)\n>  {\n> -\t/*\n> -\t * NEEDSWORK: The current way of mapping a submodule's name to\n> -\t * its location in .git/modules/ has problems with some naming\n> -\t * schemes. For example, if a submodule is named \"foo\" and\n> -\t * another is named \"foo/bar\" (whether present in the same\n> -\t * superproject commit or not - the problem will arise if both\n> -\t * superproject commits have been checked out at any point in\n> -\t * time), or if two submodule names only have different cases in\n> -\t * a case-insensitive filesystem.\n> -\t *\n> -\t * There are several solutions, including encoding the path in\n> -\t * some way, introducing a submodule.<name>.gitdir config in\n> -\t * .git/config (not .gitmodules) that allows overriding what the\n> -\t * gitdir of a submodule would be (and teach Git, upon noticing\n> -\t * a clash, to automatically determine a non-clashing name and\n> -\t * to write such a config), or introducing a\n> -\t * submodule.<name>.gitdir config in .gitmodules that repo\n> -\t * administrators can explicitly set. Nothing has been decided,\n> -\t * so for now, just append the name at the end of the path.\n> -\t */\n> -\trepo_git_path_append(r, buf, \"modules/\");\n> -\tstrbuf_addstr(buf, submodule_name);\n> +\tconst char *gitdir;\n> +\tchar *key;\n> +\tint ret;\n> +\n> +\t/* If extensions.submodulePathConfig is disabled, continue to use the plain path */\n> +\tif (!r->repository_format_submodule_path_cfg) {\n> +\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n> +\t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n> +\t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n> +\t\t\t      \"git dir\"), buf->buf);\n> +\n> +\t\treturn; /* plain gitdir is valid for use */\n> +\t}\n> +\n> +\t/* Extension is enabled: use the gitdir config if it exists */\n> +\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n> +\tret = repo_config_get_string_tmp(r, key, &gitdir);\n> +\tFREE_AND_NULL(key);\n> +\n> +\tif (!ret) {\n> +\t\tstrbuf_addstr(buf, gitdir);\n> +\n> +\t\t/* validate because users might have modified the config */\n> +\t\tif (validate_submodule_git_dir(buf->buf, submodule_name))\n> +\t\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n> +\t\t\t      \"if it is unique or conflicts with another module\"),\n> +\t\t\t    submodule_name, gitdir);\n> +\n> +\t\treturn; /* gitdir from config is valid for use */\n> +\t}\n>  \n> -\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n> -\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n> -\t\t      \"git dir\"), buf->buf);\n> +\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n> +\t      \"Please ensure it is set, for example by running something like: \"\n> +\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n> +\t    submodule_name, submodule_name, submodule_name, submodule_name);\n\nI think the logic would flow a bit more naturally if we didn't have all\nthe early returns. Something like the following untested and uncompiled\ncode:\n\nvoid submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n\t\t\t      const char *submodule_name)\n{\n\tif (!r->repository_format_submodule_path_cfg) {\n\t\t/*\n\t\t * If extensions.submodulePathConfig is disabled,\n\t\t * continue to use the plain path.\n\t\t */\n\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n\t} else {\n\t\tconst char *gitdir;\n\t\tchar *key;\n\n\t\t/* Otherwise, if the extension is enabled, we use the gitdir config. */\n\t\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n\n\t\tif (repo_config_get_string_tmp(r, key, &gitdir)) {\n\t\t\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n\t\t\t      \"Please ensure it is set, for example by running something like: \"\n\t\t\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n\t\t\t    submodule_name, submodule_name, submodule_name, submodule_name);\n\t\t}\n\n\t\tstrbuf_addstr(buf, gitdir);\n\t\tFREE_AND_NULL(key);\n\t}\n\n\tif (validate_submodule_git_dir(buf->buf, submodule_name)) {\n\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n\t\t      \"if it is unique or conflicts with another module\"),\n\t\t    submodule_name, gitdir);\n\t}\n}\n\nI think it would make sense to also hint at the extension in the error\nmessage here so that users know _why_ we expect the key to be set.\n\nPatrick\n"},{"id":"532251","messageId":"aUEh3X7Vy4yhOC4B@pks.im","threadId":"63967","inReplyTo":"20251213080817.347922-6-adrian.ratiu@collabora.com","subject":"Re: [PATCH v6 05/10] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-16T09:09:49Z","receivedAt":"2025-12-16T09:09:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 13, 2025 at 10:08:11AM +0200, Adrian Ratiu wrote:\n> This adds the ability to enable the new extension via a runtime\n> config to avoid having to enable it in each repo configuration.\n\nI think this doesn't quite match what Junio and I suggested.\n\nAs far as I understand, this new runtime option will default-enable the\nrepository extension in _all_ repositories. But this isn't really\nsomething that we should be doing, as it means that Git may now respect\nthe gitconfig even though the extension isn't enabled. Other\nimplementations of Git that don't understand the global configuration\nwill thus start to misbehave in that case.\n\nThe second issue is that the option will cause all existing repositories\nthat have submodules to be broken, as we don't have the \"gitdir\" config\nkey yet.\n\nMy suggestion was thus to have a global configuration that causes both\ngit-init(1) and git-clone(1) to automatically set the repository\nextension for _new_ repositories. This allows developers not worry about\nthis in the future anymore, but it means that they will have to migrate\nexisting repositories, at least if they care about the extension. Which\nI think is a fair tradeoff.\n\nPatrick\n"},{"id":"532252","messageId":"aUEh5fclig8NYmEU@pks.im","threadId":"63967","inReplyTo":"20251213080817.347922-7-adrian.ratiu@collabora.com","subject":"Re: [PATCH v6 06/10] submodule--helper: add gitdir migration command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-16T09:09:57Z","receivedAt":"2025-12-16T09:10:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 13, 2025 at 10:08:12AM +0200, Adrian Ratiu wrote:\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index 2b5b4f575b..458dc863df 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -1270,6 +1270,63 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n>  \treturn 0;\n>  }\n>  \n> +static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n> +\t\t\t  const char *prefix UNUSED, struct repository *repo)\n> +{\n> +\tstruct strbuf module_dir = STRBUF_INIT;\n> +\tDIR *dir;\n> +\tstruct dirent *de;\n> +\n> +\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n> +\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n> +\t\t      \"Please enable it for migration to work, for example: \"\n> +\t\t      \"git config core.repositoryformatversion 1\"));\n> +\n> +\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n> +\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n> +\t\t      \"for migration to work. Please enable it in the root repo: \"\n> +\t\t      \"git config extensions.submodulePathConfig true\"));\n\nI think we should reverse the ordering here and first create the\n\"gitdir\" config entries before we enable the extension itself. This\nensures that a halfway-aborted migration will still leave us in a good\nstate, as the config entries will be ignored until the extension is\nenabled.\n\nPatrick\n"},{"id":"532256","messageId":"874ipqhiaa.fsf@collabora.com","threadId":"63967","inReplyTo":"aUEh14H242nm0NcE@pks.im","subject":"Re: [PATCH v6 04/10] submodule: introduce extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-16T09:45:17Z","receivedAt":"2025-12-16T09:45:41Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 16 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Sat, Dec 13, 2025 at 10:08:10AM +0200, Adrian Ratiu wrote:\n>> diff --git a/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\n>> index 0672d99117..4cf7424cda 100644\n>> --- a/Documentation/config/submodule.adoc\n>> +++ b/Documentation/config/submodule.adoc\n>> @@ -52,6 +52,13 @@ submodule.<name>.active::\n>>  \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n>>  \tdetails.\n>>  \n>> +submodule.<name>.gitdir::\n>> +\tThis sets the gitdir path for submodule <name>. It only works when\n>> +\t`extensions.submodulePathConfig` is enabled, otherwise it does nothing.\n>\n> This reads a tiny bit awkward. How about: \"This configuration is only\n> respected when...\"?\n\nAck, will fix on the next reroll.\n\n>> diff --git a/submodule.c b/submodule.c\n>> index f645372a18..85ca7ea0fb 100644\n>> --- a/submodule.c\n>> +++ b/submodule.c\n>> @@ -2570,30 +2570,39 @@ int submodule_to_gitdir(struct repository *repo,\n>>  void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>>  \t\t\t      const char *submodule_name)\n>>  {\n>> -\t/*\n>> -\t * NEEDSWORK: The current way of mapping a submodule's name to\n>> -\t * its location in .git/modules/ has problems with some naming\n>> -\t * schemes. For example, if a submodule is named \"foo\" and\n>> -\t * another is named \"foo/bar\" (whether present in the same\n>> -\t * superproject commit or not - the problem will arise if both\n>> -\t * superproject commits have been checked out at any point in\n>> -\t * time), or if two submodule names only have different cases in\n>> -\t * a case-insensitive filesystem.\n>> -\t *\n>> -\t * There are several solutions, including encoding the path in\n>> -\t * some way, introducing a submodule.<name>.gitdir config in\n>> -\t * .git/config (not .gitmodules) that allows overriding what the\n>> -\t * gitdir of a submodule would be (and teach Git, upon noticing\n>> -\t * a clash, to automatically determine a non-clashing name and\n>> -\t * to write such a config), or introducing a\n>> -\t * submodule.<name>.gitdir config in .gitmodules that repo\n>> -\t * administrators can explicitly set. Nothing has been decided,\n>> -\t * so for now, just append the name at the end of the path.\n>> -\t */\n>> -\trepo_git_path_append(r, buf, \"modules/\");\n>> -\tstrbuf_addstr(buf, submodule_name);\n>> +\tconst char *gitdir;\n>> +\tchar *key;\n>> +\tint ret;\n>> +\n>> +\t/* If extensions.submodulePathConfig is disabled, continue to use the plain path */\n>> +\tif (!r->repository_format_submodule_path_cfg) {\n>> +\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n>> +\t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n>> +\t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n>> +\t\t\t      \"git dir\"), buf->buf);\n>> +\n>> +\t\treturn; /* plain gitdir is valid for use */\n>> +\t}\n>> +\n>> +\t/* Extension is enabled: use the gitdir config if it exists */\n>> +\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n>> +\tret = repo_config_get_string_tmp(r, key, &gitdir);\n>> +\tFREE_AND_NULL(key);\n>> +\n>> +\tif (!ret) {\n>> +\t\tstrbuf_addstr(buf, gitdir);\n>> +\n>> +\t\t/* validate because users might have modified the config */\n>> +\t\tif (validate_submodule_git_dir(buf->buf, submodule_name))\n>> +\t\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n>> +\t\t\t      \"if it is unique or conflicts with another module\"),\n>> +\t\t\t    submodule_name, gitdir);\n>> +\n>> +\t\treturn; /* gitdir from config is valid for use */\n>> +\t}\n>>  \n>> -\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n>> -\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n>> -\t\t      \"git dir\"), buf->buf);\n>> +\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n>> +\t      \"Please ensure it is set, for example by running something like: \"\n>> +\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n>> +\t    submodule_name, submodule_name, submodule_name, submodule_name);\n>\n> I think the logic would flow a bit more naturally if we didn't have all\n> the early returns. Something like the following untested and uncompiled\n> code:\n>\n> void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n> \t\t\t      const char *submodule_name)\n> {\n> \tif (!r->repository_format_submodule_path_cfg) {\n> \t\t/*\n> \t\t * If extensions.submodulePathConfig is disabled,\n> \t\t * continue to use the plain path.\n> \t\t */\n> \t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n> \t} else {\n> \t\tconst char *gitdir;\n> \t\tchar *key;\n>\n> \t\t/* Otherwise, if the extension is enabled, we use the gitdir config. */\n> \t\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n>\n> \t\tif (repo_config_get_string_tmp(r, key, &gitdir)) {\n> \t\t\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n> \t\t\t      \"Please ensure it is set, for example by running something like: \"\n> \t\t\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n> \t\t\t    submodule_name, submodule_name, submodule_name, submodule_name);\n> \t\t}\n>\n> \t\tstrbuf_addstr(buf, gitdir);\n> \t\tFREE_AND_NULL(key);\n> \t}\n>\n> \tif (validate_submodule_git_dir(buf->buf, submodule_name)) {\n> \t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n> \t\t      \"if it is unique or conflicts with another module\"),\n> \t\t    submodule_name, gitdir);\n> \t}\n> }\n>\n> I think it would make sense to also hint at the extension in the error\n> message here so that users know _why_ we expect the key to be set.\n\nAck, will fix in the next reroll.\n"},{"id":"532257","messageId":"871pkuhhji.fsf@collabora.com","threadId":"63967","inReplyTo":"aUEh3X7Vy4yhOC4B@pks.im","subject":"Re: [PATCH v6 05/10] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-16T10:01:21Z","receivedAt":"2025-12-16T10:01:45Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 16 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Sat, Dec 13, 2025 at 10:08:11AM +0200, Adrian Ratiu wrote:\n>> This adds the ability to enable the new extension via a runtime\n>> config to avoid having to enable it in each repo configuration.\n>\n> I think this doesn't quite match what Junio and I suggested.\n>\n> As far as I understand, this new runtime option will default-enable the\n> repository extension in _all_ repositories. But this isn't really\n> something that we should be doing, as it means that Git may now respect\n> the gitconfig even though the extension isn't enabled. Other\n> implementations of Git that don't understand the global configuration\n> will thus start to misbehave in that case.\n>\n> The second issue is that the option will cause all existing repositories\n> that have submodules to be broken, as we don't have the \"gitdir\" config\n> key yet.\n>\n> My suggestion was thus to have a global configuration that causes both\n> git-init(1) and git-clone(1) to automatically set the repository\n> extension for _new_ repositories. This allows developers not worry about\n> this in the future anymore, but it means that they will have to migrate\n> existing repositories, at least if they care about the extension. Which\n> I think is a fair tradeoff.\n\nRight, I misunderstood what you asked for. Will fix.\n"},{"id":"532258","messageId":"87y0n2g27u.fsf@collabora.com","threadId":"63967","inReplyTo":"aUEh5fclig8NYmEU@pks.im","subject":"Re: [PATCH v6 06/10] submodule--helper: add gitdir migration command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-16T10:17:41Z","receivedAt":"2025-12-16T10:18:06Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 16 Dec 2025, Patrick Steinhardt <ps@pks.im> wrote:\n> On Sat, Dec 13, 2025 at 10:08:12AM +0200, Adrian Ratiu wrote:\n>> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n>> index 2b5b4f575b..458dc863df 100644\n>> --- a/builtin/submodule--helper.c\n>> +++ b/builtin/submodule--helper.c\n>> @@ -1270,6 +1270,63 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n>>  \treturn 0;\n>>  }\n>>  \n>> +static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n>> +\t\t\t  const char *prefix UNUSED, struct repository *repo)\n>> +{\n>> +\tstruct strbuf module_dir = STRBUF_INIT;\n>> +\tDIR *dir;\n>> +\tstruct dirent *de;\n>> +\n>> +\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n>> +\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n>> +\t\t      \"Please enable it for migration to work, for example: \"\n>> +\t\t      \"git config core.repositoryformatversion 1\"));\n>> +\n>> +\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n>> +\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n>> +\t\t      \"for migration to work. Please enable it in the root repo: \"\n>> +\t\t      \"git config extensions.submodulePathConfig true\"));\n>\n> I think we should reverse the ordering here and first create the\n> \"gitdir\" config entries before we enable the extension itself. This\n> ensures that a halfway-aborted migration will still leave us in a good\n> state, as the config entries will be ignored until the extension is\n> enabled.\n\nGood point. Will do.\n"},{"id":"532312","messageId":"dymehf7vjf6winlsezdzbdiboo7d74wezoopwkyc67ckra7oe2@5ruvpsgjpbea","threadId":"63967","inReplyTo":"20251213080817.347922-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v6 00/10] Add submodulePathConfig extension and gitdir encoding","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2025-12-16T23:20:48Z","receivedAt":"2025-12-16T23:20:56Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2025.12.13 10:08, Adrian Ratiu wrote:\n> Hello everyone,\n> \n> For those new to the series, we're implementing a submodule gitdir\n> extension which allows us to have a unified way to determine gitdirs\n> and do things like encode submodule paths to avoid FS conflicts.\n\nI'm afraid I've gotten a bit lost with this series. IIUC, we no longer\ntry to encode submodule gitdirs by default, instead we do this only if\nwe detect a conflict with an existing gitdir. However, in all of my\nlocal testing, I have been unable to produce a conflict that triggers\nthis encoding. Instead, everything hits the error:\n  \"fatal: A git directory for 'nested%2fsub' is found locally with remote(s):\"\nfrom `builtin/submodule--helper.c:3389`.\n\nThis happens regardless of the setting of\n`extensions.submodulepathconfig` in either the repo's local config, or\nmy user config.\n\nMy testing setup has been as follows:\n* Using our locally-built Git with our downstream patch of [1] included:\n  * create a repo \"sub\"\n  * create a repo \"super\"\n  * In \"super\":\n    * mkdir nested\n    * git submodule add ../sub nested/sub\n    * Verify that the submodule's gitdir is .git/modules/nested%2fsub\n* Using a build of git from upstream `next` plus this series:\n  * git config set --global extensions.submodulepathconfig true\n  * git clone --recurse-submodules super super2\n  * create a repo \"nested%2fsub\"\n  * In \"super2\":\n    * git submodule add ../nested%2fsub\n\nAt this point I'd expect the collision detection / encoding to take\neffect, but instead I get the error listed above.\n\n\n[1] https://lore.kernel.org/git/20180807230637.247200-1-bmwill@google.com/\n"},{"id":"532313","messageId":"y7hfbq37mh2a6rnwvycul2e3fhxl7bljkdlukdpl3obgg57u3p@vgdsj2wo5nab","threadId":"63967","inReplyTo":"20251213080817.347922-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH v6 04/10] submodule: introduce extensions.submodulePathConfig","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2025-12-16T23:22:59Z","receivedAt":"2025-12-16T23:23:06Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2025.12.13 10:08, Adrian Ratiu wrote:\n> The idea of this extension is to abstract away the submodule gitdir\n> path implementation: everyone is expected to use the config and not\n> worry about how the path is computed internally, either in git or\n> other implementations.\n> \n> With this extension enabled, the submodule.<name>.gitdir repo config\n> becomes the single source of truth for all submodule gitdir paths.\n> \n> The submodule.<name>.gitdir config is added automatically for all new\n> submodules when this extension is enabled.\n> \n> Git will throw an error if the extension is enabled and a config is\n> missing, advising users how to migrate. Migration is manual for now.\n\nThis part doesn't seem accurate in my testing. When cloning a project\nwith `--recurse-submodules` and with the extension enabled globally, the\nresulting .git/config does not include gitdir configs for any of the\ncloned submodules, yet no error occurs.\n"},{"id":"532324","messageId":"87ecotefb8.fsf@gentoo.mail-host-address-is-not-set","threadId":"63967","inReplyTo":"y7hfbq37mh2a6rnwvycul2e3fhxl7bljkdlukdpl3obgg57u3p@vgdsj2wo5nab","subject":"Re: [PATCH v6 04/10] submodule: introduce extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-17T07:30:03Z","receivedAt":"2025-12-17T07:30:44Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 16 Dec 2025, Josh Steadmon <steadmon@google.com> wrote:\n> On 2025.12.13 10:08, Adrian Ratiu wrote:\n>> The idea of this extension is to abstract away the submodule gitdir\n>> path implementation: everyone is expected to use the config and not\n>> worry about how the path is computed internally, either in git or\n>> other implementations.\n>> \n>> With this extension enabled, the submodule.<name>.gitdir repo config\n>> becomes the single source of truth for all submodule gitdir paths.\n>> \n>> The submodule.<name>.gitdir config is added automatically for all new\n>> submodules when this extension is enabled.\n>> \n>> Git will throw an error if the extension is enabled and a config is\n>> missing, advising users how to migrate. Migration is manual for now.\n>\n> This part doesn't seem accurate in my testing. When cloning a project\n> with `--recurse-submodules` and with the extension enabled globally, the\n> resulting .git/config does not include gitdir configs for any of the\n> cloned submodules, yet no error occurs.\n\nPatrick already pointed out in the other patch that I misunderstood how\nthe global config is supposed to work.\n\nWhat you point out here is a side-effect of that. :)\n\nI will fix the global config in v7. indeed it should throw an error.\n\nI will also add four test combinations for cloning w/o\n--recurse-submodules and global config on/off.\n\nI also missed these cases in the tests I added.\n\nMany thanks,\nAdrian\n"},{"id":"532326","messageId":"87bjjxed42.fsf@collabora.com","threadId":"63967","inReplyTo":"dymehf7vjf6winlsezdzbdiboo7d74wezoopwkyc67ckra7oe2@5ruvpsgjpbea","subject":"Re: [PATCH v6 00/10] Add submodulePathConfig extension and gitdir encoding","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-17T08:17:33Z","receivedAt":"2025-12-17T08:17:58Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 16 Dec 2025, Josh Steadmon <steadmon@google.com> wrote:\n> On 2025.12.13 10:08, Adrian Ratiu wrote:\n>> Hello everyone,\n>> \n>> For those new to the series, we're implementing a submodule gitdir\n>> extension which allows us to have a unified way to determine gitdirs\n>> and do things like encode submodule paths to avoid FS conflicts.\n>\n> I'm afraid I've gotten a bit lost with this series. IIUC, we no longer\n> try to encode submodule gitdirs by default, instead we do this only if\n> we detect a conflict with an existing gitdir. However, in all of my\n> local testing, I have been unable to produce a conflict that triggers\n> this encoding. Instead, everything hits the error:\n>   \"fatal: A git directory for 'nested%2fsub' is found locally with remote(s):\"\n> from `builtin/submodule--helper.c:3389`.\n>\n> This happens regardless of the setting of\n> `extensions.submodulepathconfig` in either the repo's local config, or\n> my user config.\n>\n> My testing setup has been as follows:\n> * Using our locally-built Git with our downstream patch of [1] included:\n>   * create a repo \"sub\"\n>   * create a repo \"super\"\n>   * In \"super\":\n>     * mkdir nested\n>     * git submodule add ../sub nested/sub\n>     * Verify that the submodule's gitdir is .git/modules/nested%2fsub\n> * Using a build of git from upstream `next` plus this series:\n>   * git config set --global extensions.submodulepathconfig true\n>   * git clone --recurse-submodules super super2\n>   * create a repo \"nested%2fsub\"\n>   * In \"super2\":\n>     * git submodule add ../nested%2fsub\n>\n> At this point I'd expect the collision detection / encoding to take\n> effect, but instead I get the error listed above.\n\nThis is a good test case, thanks, I will add something very similar to\nt7425-submodule-gitdir-path-extension.sh in v7.\n\nPlease wait for v7, where I will fix the global config, then you can\nretry the steps above.\n\nJust one important thing to look out for:\n\nExisting submodules will _not_ get their submodule.<name>.gitdir\nautomatically created when enabling the extension, so you will get\nerrors like \"gitdir does not exist\" after enabling the extension.\n\nThose are expected and I assume you already got errors like those.\n\nThe gitdirs can be created manually, for example:\ngit config submodule.<name>.gitdir .git/modules/<name>\n\nOr they can all be created automatically:\ngit submodule--helper migrate-gitdir-configs\n\nWhen running the migration command in a repo containing submodules, it\nwill also enable the extension in the super repo config, in addition to\ncreating all the submodule gitdirs configs, so running just that single\ncommand should be enough.\n\nHope this all makes sense and happy to answer any questions,\nAdrian\n"},{"id":"532409","messageId":"351D4D02-AF90-4209-85D6-6C3C80C99C8A@gmail.com","threadId":"63967","inReplyTo":"87pl8flnef.fsf@gentoo.mail-host-address-is-not-set","subject":"Re: [PATCH v6 00/10] Add submodulePathConfig extension and gitdir encoding","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-18T03:43:36Z","receivedAt":"2025-12-18T03:43:48Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 15 déc. 2025 à 11:29, Adrian Ratiu <adrian.ratiu@collabora.com> a écrit :\n> \n> ﻿On Sat, 13 Dec 2025, Ben Knoble <ben.knoble@gmail.com> wrote:\n>>>> Le 13 déc. 2025 à 03:09, Adrian Ratiu <adrian.ratiu@collabora.com> a écrit :\n>>> \n>>> ﻿Hello everyone,\n>>> \n>>> For those new to the series, we're implementing a submodule gitdir\n>>> extension which allows us to have a unified way to determine gitdirs\n>>> and do things like encode submodule paths to avoid FS conflicts.\n>> \n>> Hi there, I admit I haven’t followed this series closely. I use submodules quite a bit but haven’t yet peered into the depths of the implementation.\n>> \n>> I read over the documentation changes in this series, and it’s not clear to me how or why I would use this new feature (I don’t mean there’s no benefit! Just that I’m having a hard time parsing it out.). By “how” I mean: I can see how to set config and run the migrator; what does that unlock for me to now go and do?\n>> \n>> Does one of the previous cover letters explain how this is useful to submodule users? If so which, and perhaps the docs could also contain a “here’s when/why you might want this extension enabled and what it allows you to do”?\n>> \n>> Or maybe this is meant to be not too user-facing, in which case I’m curious who would turn this on and why still :)\n>> \n>> Again, I am mostly curious, so please don’t read this as an attempt to hold the series hostage! :)\n> \n> It's perfectly ok to ask, no problem. :)\n> \n> This series is for the minority of users who either:\n> \n> 1. Encounter errors like the following in submodule.c:\n>   die(_(\"refusing to create/use '%s' in another submodule's \"...)\n> \n>   These errors can happen due to a number of factors, like\n>   case-insensitive filesystems or submodule layouts.\n> \n> 2. Need to specify non-standard gitdir repository paths, different from\n>   the currently hardcoded .git/modules/<plain-name> location.\n> \n>   With this series, the gitdir config becomes the unified way to\n>   set/get the gitdir paths, so you can move them around as needed.\n>   It also helps other git implementations who don't need to exactly\n>   match git's behaviour: the config becomes the standard interface.\n> \n> If you are not in one of the two above cases, then there is no reason to\n> enable this and it won't affect you.\n> \n> Hope this is clear, maybe we could spell it out better in the\n> documentation (suggestions welcome btw) or even tell users in the error\n> messages to enable this extension.\n\nThanks! That helped, and I am not in either case ;) I agree with Junio’s downthread points about docs."},{"id":"532581","messageId":"20251220101528.1227487-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 01/11] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:18Z","receivedAt":"2025-12-20T10:16:05Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded modules gitdir path leading to test failures.\n\nCall the submodule_name_to_gitdir() helper instead, which was invented\nexactly for this purpose and is already used by all the other locations\nwhich work on gitdirs.\n\nAlso narrow the scope of the submod_gitdir_path variable which is not\nused anymore in the updated \"else\" branch.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 13 +++++++------\n 1 file changed, 7 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 35f6cf735e..13b5e4ed68 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3183,13 +3183,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n \n static int add_submodule(const struct add_data *add_data)\n {\n-\tchar *submod_gitdir_path;\n \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n \tint ret = -1;\n \n \t/* perhaps the path already exists and is already a git repo, else clone it */\n \tif (is_directory(add_data->sm_path)) {\n+\t\tchar *submod_gitdir_path;\n \t\tstruct strbuf sm_path = STRBUF_INIT;\n \t\tstrbuf_addstr(&sm_path, add_data->sm_path);\n \t\tsubmod_gitdir_path = xstrfmt(\"%s/.git\", add_data->sm_path);\n@@ -3203,10 +3203,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3215,8 +3216,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3234,7 +3235,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.51.2\n\n"},{"id":"532582","messageId":"20251220101528.1227487-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 02/11] submodule: always validate gitdirs inside submodule_name_to_gitdir","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:19Z","receivedAt":"2025-12-20T10:16:08Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Move the ad-hoc validation checks sprinkled across the source tree,\nafter calling submodule_name_to_gitdir() into the function proper,\nwhich now always validates the gitdir before returning it.\n\nThis simplifies the API and helps to:\n1. Avoid redundant validation calls after submodule_name_to_gitdir().\n2. Avoid the risk of callers forgetting to validate.\n3. Ensure gitdir paths provided by users via configs are always valid\n   (config gitdir paths are added in a subsequent commit).\n\nThe validation function can still be called as many times as needed\noutside submodule_name_to_gitdir(), for example we keep two calls\nwhich are still required, to avoid parallel clone races by re-running\nthe validation in builtin/submodule-helper.c.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c |  4 ----\n submodule.c                 | 12 ++++--------\n 2 files changed, 4 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 13b5e4ed68..f1fc098614 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1699,10 +1699,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n \t\t\t\t\t\t    clone_data->path);\n \n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\n \tif (!file_exists(sm_gitdir)) {\n \t\tif (clone_data->require_init && !stat(clone_data_path, &st) &&\n \t\t    !is_empty_dir(clone_data_path))\ndiff --git a/submodule.c b/submodule.c\nindex 40a5c6fb9d..f645372a18 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2166,11 +2166,6 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n \t\t\t\t\t\t sub->name);\n-\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n-\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n-\t\t\t\t      \"submodule's git dir\"),\n-\t\t\t\t    gitdir.buf);\n \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n \t\t\tstrbuf_release(&gitdir);\n \n@@ -2349,9 +2344,6 @@ static void relocate_single_git_dir_into_superproject(const char *path,\n \t\tdie(_(\"could not lookup name for submodule '%s'\"), path);\n \n \tsubmodule_name_to_gitdir(&new_gitdir, the_repository, sub->name);\n-\tif (validate_submodule_git_dir(new_gitdir.buf, sub->name) < 0)\n-\t\tdie(_(\"refusing to move '%s' into an existing git dir\"),\n-\t\t    real_old_git_dir);\n \tif (safe_create_leading_directories_const(the_repository, new_gitdir.buf) < 0)\n \t\tdie(_(\"could not create directory '%s'\"), new_gitdir.buf);\n \treal_new_git_dir = real_pathdup(new_gitdir.buf, 1);\n@@ -2600,4 +2592,8 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t */\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n+\n+\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n+\t\t      \"git dir\"), buf->buf);\n }\n-- \n2.51.2\n\n"},{"id":"532583","messageId":"20251220101528.1227487-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 03/11] builtin/submodule--helper: add gitdir command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:20Z","receivedAt":"2025-12-20T10:16:13Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This exposes the gitdir name computed by submodule_name_to_gitdir()\ninternally, to make it easier for users and tests to interact with it.\n\nNext commit will add a gitdir configuration, so this helper can also be\nused to easily query that config or validate any gitdir path the user\nsets (submodule_name_to_git_dir now runs the validation logic, since\nour previous commit).\n\nBased-on-patch-by: Brandon Williams <bwilliams.eng@gmail.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex f1fc098614..3bc139ff9c 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1204,6 +1204,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3583,6 +3599,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\n-- \n2.51.2\n\n"},{"id":"532584","messageId":"20251220101528.1227487-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 04/11] submodule: introduce extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:21Z","receivedAt":"2025-12-20T10:16:20Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"The idea of this extension is to abstract away the submodule gitdir\npath implementation: everyone is expected to use the config and not\nworry about how the path is computed internally, either in git or\nother implementations.\n\nWith this extension enabled, the submodule.<name>.gitdir repo config\nbecomes the single source of truth for all submodule gitdir paths.\n\nThe submodule.<name>.gitdir config is added automatically for all new\nsubmodules when this extension is enabled.\n\nGit will throw an error if the extension is enabled and a config is\nmissing, advising users how to migrate. Migration is manual for now.\n\nE.g. to add a missing config entry for an existing \"foo\" module:\ngit config submodule.foo.gitdir .git/modules/foo\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |  23 ++++\n Documentation/config/submodule.adoc        |   7 ++\n builtin/submodule--helper.c                |  54 +++++++-\n repository.c                               |   1 +\n repository.h                               |   1 +\n setup.c                                    |   7 ++\n setup.h                                    |   1 +\n submodule.c                                |  60 +++++----\n t/lib-verify-submodule-gitdir-path.sh      |  24 ++++\n t/meson.build                              |   1 +\n t/t7425-submodule-gitdir-path-extension.sh | 138 +++++++++++++++++++++\n t/t9902-completion.sh                      |   1 +\n 12 files changed, 289 insertions(+), 29 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-gitdir-path-extension.sh\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 532456644b..e15b93f2fb 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -73,6 +73,29 @@ relativeWorktrees:::\n \trepaired with either the `--relative-paths` option or with the\n \t`worktree.useRelativePaths` config set to `true`.\n \n+submodulePathConfig:::\n+\tThis extension is for the minority of users who:\n++\n+--\n+* Encounter errors like\t`refusing to create ... in another submodule's git dir`\n+  due to a number of reasons, like case-insensitive filesystem conflicts when\n+  creating modules named `foo` and `Foo`.\n+* Require more flexible submodule layouts, for example due to nested names like\n+  `foo`, `foo/bar` and `foo/baz` not supported by the default gitdir mechanism\n+  which uses `.git/modules/<plain-name>` locations, causing further conflicts.\n+--\n++\n+When `extensions.submodulePathConfig` is enabled, the `submodule.<name>.gitdir`\n+config becomes the single source of truth for all submodule gitdir paths and is\n+automatically set for all new submodules both during clone and init operations.\n++\n+Git will error out if a module does not have a corresponding\n+`submodule.<name>.gitdir` set.\n++\n+Existing (pre-extension) submodules need to be migrated by adding the missing\n+config entries. This is done manually for now, e.g. for each submodule:\n+`git config submodule.<name>.gitdir .git/modules/<name>`.\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/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 0672d99117..9c260a69f6 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -52,6 +52,13 @@ submodule.<name>.active::\n \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n \tdetails.\n \n+submodule.<name>.gitdir::\n+\tThis sets the gitdir path for submodule <name>. This configuration is\n+\trespected when `extensions.submodulePathConfig` is enabled, otherwise it\n+\thas no effect. When enabled, this config becomes the single source of\n+\ttruth for submodule gitdir paths and git will error if it is missing.\n+\tSee linkgit:git-config[1] for details.\n+\n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\n \tsubmodule's path to determine if the submodule is of interest to git\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 3bc139ff9c..f8cae345a5 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -435,6 +435,48 @@ struct init_cb {\n };\n #define INIT_CB_INIT { 0 }\n \n+static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n+\t\t\t\t\t     const char *submodule_name)\n+{\n+\tconst char *value;\n+\tchar *key;\n+\n+\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n+\t\treturn -1;\n+\n+\t key = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\n+\t/* Nothing to do if the config already exists. */\n+\tif (!repo_config_get_string_tmp(the_repository, key, &value)) {\n+\t\tfree(key);\n+\t\treturn 0;\n+\t}\n+\n+\tif (repo_config_set_gently(the_repository, key, gitdir_path->buf)) {\n+\t\tfree(key);\n+\t\treturn -1;\n+\t}\n+\n+\tfree(key);\n+\treturn 0;\n+}\n+\n+static void create_default_gitdir_config(const char *submodule_name)\n+{\n+\tstruct strbuf gitdir_path = STRBUF_INIT;\n+\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n+\tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n+\t      \"Please ensure it is set, for example by running something like: \"\n+\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n+\t    submodule_name, submodule_name, submodule_name);\n+}\n+\n static void init_submodule(const char *path, const char *prefix,\n \t\t\t   const char *super_prefix,\n \t\t\t   unsigned int flags)\n@@ -511,6 +553,10 @@ static void init_submodule(const char *path, const char *prefix,\n \t\tif (repo_config_set_gently(the_repository, sb.buf, upd))\n \t\t\tdie(_(\"Failed to register update mode for submodule path '%s'\"), displaypath);\n \t}\n+\n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tcreate_default_gitdir_config(sub->name);\n+\n \tstrbuf_release(&sb);\n \tfree(displaypath);\n \tfree(url);\n@@ -1801,8 +1847,9 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n \t\tunlink(head);\n \t\tfree(head);\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n+\t\tdie(_(\"refusing to create/use '%s' in another submodule's git dir. \"\n+\t\t      \"Enabling extensions.submodulePathConfig should fix this.\"),\n+\t\t    sm_gitdir);\n \t}\n \n \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n@@ -3574,6 +3621,9 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \tadd_data.progress = !!progress;\n \tadd_data.dissociate = !!dissociate;\n \n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tcreate_default_gitdir_config(add_data.sm_name);\n+\n \tif (add_submodule(&add_data))\n \t\tgoto cleanup;\n \tconfigure_added_submodule(&add_data);\ndiff --git a/repository.c b/repository.c\nindex 863f24411b..6cb1247f4b 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -281,6 +281,7 @@ int repo_init(struct repository *repo,\n \trepo->repository_format_worktree_config = format.worktree_config;\n \trepo->repository_format_relative_worktrees = format.relative_worktrees;\n \trepo->repository_format_precious_objects = format.precious_objects;\n+\trepo->repository_format_submodule_path_cfg = format.submodule_path_cfg;\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 6063c4b846..7141237f97 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -165,6 +165,7 @@ struct repository {\n \tint repository_format_worktree_config;\n \tint repository_format_relative_worktrees;\n \tint repository_format_precious_objects;\n+\tint repository_format_submodule_path_cfg;\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 3a6a048620..428427d689 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -686,6 +686,9 @@ static enum extension_result handle_extension(const char *var,\n \t} else if (!strcmp(ext, \"relativeworktrees\")) {\n \t\tdata->relative_worktrees = git_config_bool(var, value);\n \t\treturn EXTENSION_OK;\n+\t} else if (!strcmp(ext, \"submodulepathconfig\")) {\n+\t\tdata->submodule_path_cfg = git_config_bool(var, value);\n+\t\treturn EXTENSION_OK;\n \t}\n \treturn EXTENSION_UNKNOWN;\n }\n@@ -1947,6 +1950,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\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\tthe_repository->repository_format_submodule_path_cfg =\n+\t\t\t\trepo_fmt.submodule_path_cfg;\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@@ -2045,6 +2050,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_submodule_path_cfg =\n+\t\tfmt->submodule_path_cfg;\n \tthe_repository->repository_format_relative_worktrees =\n \t\tfmt->relative_worktrees;\n \tthe_repository->repository_format_partial_clone =\ndiff --git a/setup.h b/setup.h\nindex d55dcc6608..0738dec244 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -167,6 +167,7 @@ struct repository_format {\n \tchar *partial_clone; /* value of extensions.partialclone */\n \tint worktree_config;\n \tint relative_worktrees;\n+\tint submodule_path_cfg;\n \tint is_bare;\n \tint hash_algo;\n \tint compat_hash_algo;\ndiff --git a/submodule.c b/submodule.c\nindex f645372a18..e3692009cd 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2158,8 +2158,9 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\t\tif (validate_submodule_git_dir(git_dir,\n \t\t\t\t\t\t\t       sub->name) < 0)\n \t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n-\t\t\t\t\t      \"another submodule's git dir\"),\n-\t\t\t\t\t    git_dir);\n+\t\t\t\t\t      \"another submodule's git dir. \"\n+\t\t\t\t\t      \"Enabling extensions.submodulePathConfig \"\n+\t\t\t\t\t      \"should fix this.\"), git_dir);\n \t\t\t\tfree(git_dir);\n \t\t\t}\n \t\t} else {\n@@ -2570,30 +2571,35 @@ int submodule_to_gitdir(struct repository *repo,\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n-\t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n-\t */\n-\trepo_git_path_append(r, buf, \"modules/\");\n-\tstrbuf_addstr(buf, submodule_name);\n+\tif (!r->repository_format_submodule_path_cfg) {\n+\t\t/*\n+\t\t * If extensions.submodulePathConfig is disabled,\n+\t\t * continue to use the plain path.\n+\t\t */\n+\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n+\t} else {\n+\t\tconst char *gitdir;\n+\t\tchar *key;\n+\t\tint ret;\n+\n+\t\t/* Otherwise the extension is enabled, so use the gitdir config. */\n+\t\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\t\tret = repo_config_get_string_tmp(r, key, &gitdir);\n+\t\tFREE_AND_NULL(key);\n+\n+\t\tif (ret)\n+\t\t\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n+\t\t\t      \"Please ensure it is set, for example by running something like: \"\n+\t\t\t      \"'git config submodule.%s.gitdir .git/modules/%s'. For details \"\n+\t\t\t      \"see the extensions.submodulePathConfig documentation.\"),\n+\t\t\t    submodule_name, submodule_name, submodule_name, submodule_name);\n+\n+\t\tstrbuf_addstr(buf, gitdir);\n+\t}\n \n-\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), buf->buf);\n+\t/* validate because users might have modified the config */\n+\tif (validate_submodule_git_dir(buf->buf, submodule_name))\n+\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n+\t\t      \"if it is unique or conflicts with another module\"),\n+\t\t    submodule_name, buf->buf);\n }\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..62794df976\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,24 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n+\n+# This does not check filesystem existence. That is done in submodule.c via the\n+# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n+# might or might not exist (e.g. when adding a new submodule), so this only\n+# checks the expected configuration path, which might be overridden by the user.\n+\n+verify_submodule_gitdir_path() {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\t# Compute expected absolute path\n+\t\texpected=\"$(git rev-parse --git-common-dir)/$path\" &&\n+\t\texpected=\"$(test-tool path-utils real_path \"$expected\")\" &&\n+\t\t# Compute actual absolute path\n+\t\tactual=\"$(git submodule--helper gitdir \"$name\")\" &&\n+\t\tactual=\"$(test-tool path-utils real_path \"$actual\")\" &&\n+\t\techo \"$expected\" >expect &&\n+\t\techo \"$actual\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..0b1de3251a 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -887,6 +887,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-gitdir-path-extension.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nnew file mode 100755\nindex 0000000000..5d52a289f8\n--- /dev/null\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -0,0 +1,138 @@\n+#!/bin/sh\n+\n+test_description='submodulePathConfig extension works as expected'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+       git config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed extension submodules' '\n+\tgit init -b main legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init -b main new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init -b main main &&\n+\t(\n+\t\tcd main &&\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\t# trigger the \"die_path_inside_submodule\" check\n+\t\ttest_must_fail git submodule add ../new-sub \"legacy/nested\" &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\tgit submodule add ../new-sub \"New Sub\" &&\n+\t\ttest_commit new &&\n+\n+\t\t# retrigger the \"die_path_inside_submodule\" check with encoding\n+\t\ttest_must_fail git submodule add ../new-sub \"New Sub/nested2\"\n+       )\n+'\n+\n+test_expect_success 'verify new submodule gitdir config' '\n+\tgit -C main config submodule.\"New Sub\".gitdir > actual &&\n+\techo \".git/modules/New Sub\" > expect &&\n+\ttest_cmp expect actual &&\n+\tverify_submodule_gitdir_path main \"New Sub\" \"modules/New Sub\"\n+'\n+\n+test_expect_success 'manual add and verify legacy submodule gitdir config' '\n+\t# the legacy module should not contain a gitdir config, because it\n+\t# was added before the extension was enabled. Add and test it.\n+\ttest_must_fail git -C main config submodule.legacy.gitdir &&\n+\tgit -C main config submodule.legacy.gitdir .git/modules/legacy &&\n+\tgit -C main config submodule.legacy.gitdir > actual &&\n+\techo \".git/modules/legacy\" > expect &&\n+\ttest_cmp expect actual &&\n+\tverify_submodule_gitdir_path main \"legacy\" \"modules/legacy\"\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned-non-extension &&\n+\t(\n+\t\tcd cloned-non-extension &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/\"New Sub\" &&\n+\n+\t\ttest_must_fail git config submodule.legacy.gitdir &&\n+\t\ttest_must_fail git config submodule.\"New Sub\".gitdir &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t) &&\n+\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules main cloned-extension &&\n+\t(\n+\t\tcd cloned-extension &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\tgit config submodule.legacy.gitdir &&\n+\t\tgit config submodule.\"New Sub\".gitdir &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to encoded submodules' '\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C main config receive.denyCurrentBranch updateInstead &&\n+\t(\n+\t\tcd cloned-extension &&\n+\n+\t\tgit -C legacy switch --track -C main origin/main  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C \"New Sub\" switch --track -C main origin/main &&\n+\t\ttest_commit -C \"New Sub\" second-commit &&\n+\t\tgit -C \"New Sub\" push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C main origin/main  &&\n+\t\tgit add legacy \"New Sub\" &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_done\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 964e1f1569..ffb9c8b522 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n \tsubmodule.sub.fetchRecurseSubmodules Z\n \tsubmodule.sub.ignore Z\n \tsubmodule.sub.active Z\n+\tsubmodule.sub.gitdir Z\n \tEOF\n '\n \n-- \n2.51.2\n\n"},{"id":"532585","messageId":"20251220101528.1227487-6-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 05/11] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:22Z","receivedAt":"2025-12-20T10:16:21Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new config `init.autoSetupSubmodulePathConfig` which allows\nenabling `extensions.submodulePathConfig` for new submodules by\ndefault (those created via git init or clone).\n\nImportant: setting init.autoSetupSubmodulePathConfig = true does\nnot globally enable `extensions.submodulePathConfig`. Existing\nrepositories will still have the extension disabled and will\nrequire migration (for example via git submodule--helper command\nadded in the next commit).\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |   4 +\n Documentation/config/init.adoc             |   6 +\n setup.c                                    |  12 +-\n t/t7425-submodule-gitdir-path-extension.sh | 125 +++++++++++++++++++++\n 4 files changed, 146 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex e15b93f2fb..0968ac3d5c 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -95,6 +95,10 @@ Git will error out if a module does not have a corresponding\n Existing (pre-extension) submodules need to be migrated by adding the missing\n config entries. This is done manually for now, e.g. for each submodule:\n `git config submodule.<name>.gitdir .git/modules/<name>`.\n++\n+The extension can be enabled automatically for new repositories by setting\n+`init.autoSetupSubmodulePathConfig` to `true`, for example by running\n+`git config --global init.autoSetupSubmodulePathConfig true`.\n \n worktreeConfig:::\n \tIf enabled, then worktrees will load config settings from the\ndiff --git a/Documentation/config/init.adoc b/Documentation/config/init.adoc\nindex e45b2a8121..293a2ddbdf 100644\n--- a/Documentation/config/init.adoc\n+++ b/Documentation/config/init.adoc\n@@ -18,3 +18,9 @@ endif::[]\n \tSee `--ref-format=` in linkgit:git-init[1]. Both the command line\n \toption and the `GIT_DEFAULT_REF_FORMAT` environment variable take\n \tprecedence over this config.\n+\n+init.autoSetupSubmodulePathConfig::\n+\tA boolean that specifies if `git init` and `git clone` should\n+\tautomatically set `extensions.submodulePathConfig` to `true`. This\n+\tallows all new repositories to automatically use the submodule path\n+\textension. Defaults to `false` when unset.\ndiff --git a/setup.c b/setup.c\nindex 428427d689..3e05fe7c58 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2661,7 +2661,7 @@ int init_db(const char *git_dir, const char *real_git_dir,\n \t    const char *initial_branch,\n \t    int init_shared_repository, unsigned int flags)\n {\n-\tint reinit;\n+\tint reinit, auto_setup_submodule_path_config = 0;\n \tint exist_ok = flags & INIT_DB_EXIST_OK;\n \tchar *original_git_dir = real_pathdup(git_dir, 1);\n \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n@@ -2712,6 +2712,16 @@ int init_db(const char *git_dir, const char *real_git_dir,\n \t\t\t\t\t  initial_branch, flags & INIT_DB_QUIET);\n \tcreate_object_directory();\n \n+\trepo_config_get_bool(the_repository, \"init.autoSetupSubmodulePathConfig\",\n+\t\t\t     &auto_setup_submodule_path_config);\n+\tif (auto_setup_submodule_path_config) {\n+\t\tint version = 0;\n+\t\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &version);\n+\t\tif (version < 1)\n+\t\t\trepo_config_set(the_repository, \"core.repositoryformatversion\", \"1\");\n+\t\trepo_config_set(the_repository, \"extensions.submodulepathconfig\", \"true\");\n+\t}\n+\n \tif (repo_settings_get_shared_repository(the_repository)) {\n \t\tchar buf[10];\n \t\t/* We do not spell \"group\" and such, so that\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 5d52a289f8..06ee1ff86b 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -135,4 +135,129 @@ test_expect_success 'fetch mixed submodule changes and verify updates' '\n \t)\n '\n \n+test_expect_success '`git init` respects init.autoSetupSubmodulePathConfig' '\n+\tgit config --global init.autoSetupSubmodulePathConfig true &&\n+\tgit init repo-init &&\n+\tgit -C repo-init config extensions.submodulePathConfig > actual &&\n+\techo true > expect &&\n+\ttest_cmp expect actual &&\n+\t# create a submodule and check gitdir\n+\t(\n+\t\tcd repo-init &&\n+\t\tgit init -b main sub &&\n+\t\ttest_commit -C sub sub-initial &&\n+\t\tgit submodule add ./sub sub &&\n+\t\tgit config submodule.sub.gitdir > actual &&\n+\t\techo \".git/modules/sub\" > expect &&\n+\t\ttest_cmp expect actual\n+\t) &&\n+\tgit config --global --unset init.autoSetupSubmodulePathConfig\n+'\n+\n+test_expect_success '`git init` does not set extension by default' '\n+\tgit init upstream &&\n+\ttest_commit -C upstream initial &&\n+\ttest_must_fail git -C upstream config extensions.submodulePathConfig &&\n+\t# create a pair of submodules and check gitdir is not created\n+\tgit init -b main sub &&\n+\ttest_commit -C sub sub-initial &&\n+\t(\n+\t\tcd upstream &&\n+\t\tgit submodule add ../sub sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\tgit submodule add ../sub sub2 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\tgit commit -m \"Add submodules\"\n+\t)\n+'\n+\n+test_expect_success '`git clone` does not set extension by default' '\n+\ttest_when_finished \"rm -rf repo-clone-no-ext\" &&\n+\tgit clone upstream repo-clone-no-ext &&\n+\t(\n+\t\tcd repo-clone-no-ext &&\n+\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\t\ttest_path_is_missing .git/modules/sub1 &&\n+\t\ttest_path_is_missing .git/modules/sub2 &&\n+\n+\t\t# create a submodule and check gitdir is not created\n+\t\tgit submodule add ../sub sub3 &&\n+\t\ttest_must_fail git config submodule.sub3.gitdir\n+\t)\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` does not set extension by default' '\n+\ttest_when_finished \"rm -rf repo-clone-no-ext\" &&\n+\tgit clone --recurse-submodules upstream repo-clone-no-ext &&\n+\t(\n+\t\tcd repo-clone-no-ext &&\n+\n+\t\t# verify that that submodules do not have gitdir set\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\n+\t\t# create another submodule and check that gitdir is not created\n+\t\tgit submodule add ../sub sub3 &&\n+\t\ttest_path_is_dir .git/modules/sub3 &&\n+\t\ttest_must_fail git config submodule.sub3.gitdir\n+\t)\n+\n+'\n+\n+test_expect_success '`git clone` respects init.autoSetupSubmodulePathConfig' '\n+\ttest_when_finished \"rm -rf repo-clone\" &&\n+\tgit config --global init.autoSetupSubmodulePathConfig true &&\n+\tgit clone upstream repo-clone &&\n+\t(\n+\t\tcd repo-clone &&\n+\n+\t\t# verify new repo extension is inherited from global config\n+\t\tgit config extensions.submodulePathConfig > actual &&\n+\t\techo true > expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# new submodule has a gitdir config\n+\t\tgit submodule add ../sub sub &&\n+\t\ttest_path_is_dir .git/modules/sub &&\n+\t\tgit config submodule.sub.gitdir > actual &&\n+\t\techo \".git/modules/sub\" > expect &&\n+\t\ttest_cmp expect actual\n+\t) &&\n+\tgit config --global --unset init.autoSetupSubmodulePathConfig\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` respects init.autoSetupSubmodulePathConfig' '\n+\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n+\tgit config --global init.autoSetupSubmodulePathConfig true &&\n+\tgit clone  --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# verify new repo extension is inherited from global config\n+\t\tgit config extensions.submodulePathConfig > actual &&\n+\t\techo true > expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# previous submodules should exist\n+\t\tgit config submodule.sub1.gitdir &&\n+\t\tgit config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\n+\t\t# create another submodule and check that gitdir is created\n+\t\tgit submodule add ../sub new-sub &&\n+\t\ttest_path_is_dir .git/modules/new-sub &&\n+\t\tgit config submodule.new-sub.gitdir > actual &&\n+\t\techo \".git/modules/new-sub\" > expect &&\n+\t\ttest_cmp expect actual\n+\t) &&\n+\tgit config --global --unset init.autoSetupSubmodulePathConfig\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532586","messageId":"20251220101528.1227487-7-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 06/11] submodule--helper: add gitdir migration command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:23Z","receivedAt":"2025-12-20T10:16:26Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Manually running\n\"git config submodule.<name>.gitdir .git/modules/<name>\"\nfor each submodule can be impractical, so add a migration command to\nsubmodule--helper to automatically create configs for all submodules\nas required by extensions.submodulePathConfig.\n\nThe command calls create_default_gitdir_config() which validates the\ngitdir paths before adding the configs.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |  6 +-\n builtin/submodule--helper.c                | 58 +++++++++++++++++++\n t/t7425-submodule-gitdir-path-extension.sh | 67 ++++++++++++++++++++++\n 3 files changed, 129 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 0968ac3d5c..62dc112874 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -93,8 +93,10 @@ Git will error out if a module does not have a corresponding\n `submodule.<name>.gitdir` set.\n +\n Existing (pre-extension) submodules need to be migrated by adding the missing\n-config entries. This is done manually for now, e.g. for each submodule:\n-`git config submodule.<name>.gitdir .git/modules/<name>`.\n+config entries. This can be done manually, e.g. for each submodule:\n+`git config submodule.<name>.gitdir .git/modules/<name>`, or via the\n+`git submodule--helper migrate-gitdir-configs` command which iterates over all\n+submodules and attempts to migrate them.\n +\n The extension can be enabled automatically for new repositories by setting\n `init.autoSetupSubmodulePathConfig` to `true`, for example by running\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex f8cae345a5..5a6436f18f 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1266,6 +1266,63 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n \treturn 0;\n }\n \n+static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n+\t\t\t  const char *prefix UNUSED, struct repository *repo)\n+{\n+\tstruct strbuf module_dir = STRBUF_INIT;\n+\tDIR *dir;\n+\tstruct dirent *de;\n+\n+\trepo_git_path_append(repo, &module_dir, \"modules/\");\n+\n+\tdir = opendir(module_dir.buf);\n+\tif (!dir)\n+\t\tdie(_(\"could not open '%s'\"), module_dir.buf);\n+\n+\twhile ((de = readdir(dir))) {\n+\t\tstruct strbuf gitdir_path = STRBUF_INIT;\n+\t\tchar *key;\n+\t\tconst char *value;\n+\n+\t\tif (is_dot_or_dotdot(de->d_name))\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_addf(&gitdir_path, \"%s/%s\", module_dir.buf, de->d_name);\n+\t\tif (!is_git_directory(gitdir_path.buf)) {\n+\t\t\tstrbuf_release(&gitdir_path);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tstrbuf_release(&gitdir_path);\n+\n+\t\tkey = xstrfmt(\"submodule.%s.gitdir\", de->d_name);\n+\t\tif (!repo_config_get_string_tmp(repo, key, &value)) {\n+\t\t\t/* Already has a gitdir config, nothing to do. */\n+\t\t\tfree(key);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfree(key);\n+\n+\t\tcreate_default_gitdir_config(de->d_name);\n+\t}\n+\n+\tclosedir(dir);\n+\tstrbuf_release(&module_dir);\n+\n+\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n+\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n+\t\t      \"Please enable it for migration to work, for example: \"\n+\t\t      \"git config core.repositoryformatversion 1\"));\n+\n+\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n+\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n+\t\t      \"for migration to work. Please enable it in the root repo: \"\n+\t\t      \"git config extensions.submodulePathConfig true\"));\n+\n+\trepo->repository_format_submodule_path_cfg = 1;\n+\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3649,6 +3706,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"migrate-gitdir-configs\", &fn, module_migrate),\n \t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 06ee1ff86b..6ca9f13a59 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -260,4 +260,71 @@ test_expect_success '`git clone --recurse-submodules` respects init.autoSetupSub\n \tgit config --global --unset init.autoSetupSubmodulePathConfig\n '\n \n+test_expect_success 'submodule--helper migrates legacy modules' '\n+\t(\n+\t\tcd upstream &&\n+\n+\t\t# previous submodules exist and were not migrated yet\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\n+\t\t# run migration\n+\t\tgit submodule--helper migrate-gitdir-configs &&\n+\n+\t\t# test that migration worked\n+\t\tgit config submodule.sub1.gitdir >actual &&\n+\t\techo \".git/modules/sub1\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit config submodule.sub2.gitdir >actual &&\n+\t\techo \".git/modules/sub2\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# repository extension is enabled after migration\n+\t\tgit config extensions.submodulePathConfig > actual &&\n+\t\techo \"true\" > expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` works after migration' '\n+\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n+\n+\t# test with extension disabled after the upstream repo was migrated\n+\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# init.autoSetupSubmodulePathConfig was disabled before clone, so\n+\t\t# the repo extension config should also be off, the migration ignored\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\n+\t\t# modules should look like there was no migration done\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2\n+\t) &&\n+\trm -rf repo-clone-recursive &&\n+\n+\t# enable the extension, then retry the clone\n+\tgit config --global init.autoSetupSubmodulePathConfig true &&\n+\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# repository extension is enabled\n+\t\tgit config extensions.submodulePathConfig > actual &&\n+\t\techo \"true\" > expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# gitdir configs exist for submodules\n+\t\tgit config submodule.sub1.gitdir &&\n+\t\tgit config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2\n+\t)\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532587","messageId":"20251220101528.1227487-8-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 07/11] builtin/credential-store: move is_rfc3986_unreserved to url.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:24Z","receivedAt":"2025-12-20T10:16:30Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"is_rfc3986_unreserved() was moved to credential-store.c and was made\nstatic by f89854362c (credential-store: move related functions to\ncredential-store file, 2023-06-06) under a correct assumption, at the\ntime, that it was the only place using it.\n\nHowever now we need it to apply URL-encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back as a\npublic function exposed via url.h, instead of the old helper path\n(strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n\nThis function will be used in subsequent commits which do the encoding.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c | 7 +------\n url.c                      | 6 ++++++\n url.h                      | 7 +++++++\n 3 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..bc1453c6b2 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -7,6 +7,7 @@\n #include \"path.h\"\n #include \"string-list.h\"\n #include \"parse-options.h\"\n+#include \"url.h\"\n #include \"write-or-die.h\"\n \n static struct lock_file credential_lock;\n@@ -76,12 +77,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/url.c b/url.c\nindex 282b12495a..adc289229c 100644\n--- a/url.c\n+++ b/url.c\n@@ -3,6 +3,12 @@\n #include \"strbuf.h\"\n #include \"url.h\"\n \n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex 2a27c34277..e644c3c809 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,4 +21,11 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+/*\n+ * The set of unreserved characters as per STD66 (RFC3986) is\n+ * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n+ * components without percent-encoding.\n+ */\n+int is_rfc3986_unreserved(char ch);\n+\n #endif /* URL_H */\n-- \n2.51.2\n\n"},{"id":"532588","messageId":"20251220101528.1227487-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:17Z","receivedAt":"2025-12-20T10:16:30Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello everyone,\n\nFor those new to the series, we're implementing a submodule gitdir\nextension which allows us to have a unified way to determine gitdirs\nand do things like encode submodule paths to avoid FS conflicts.\n\nv7 addresses all feedback received in v6, containing just iterative\nimprovements to code, tests and documentation without any big design\nchanges this time.\n\nPatches 1-6 implement the basic mechanisms of the new extension.\nPatches 7-11 improve filesystem conflict detection and resolution.\n\nAs always, this is based on the latest master branch, I've checkd\nfor conflicts with next/seen, pushed to Github [1] and succesfully\nran the CI [2].\n\n1: https://github.com/10ne1/git/tree/dev/aratiu/encoding-v7\n2: https://github.com/10ne1/git/actions/runs/20384003122\n\nChanges in v7:\n* Simplified submodule_name_to_gitdir() code structure. Logically it still is\n  the same like in v6, just easier to read. (Patrick)\n* Reworked the global config to enable the extension only for new repos\n  during clone and init operations (Patrick, Junio)\n* Improved validation of existing submodules with encoded names (Josh, Emily)\n* Added more tests for init, clone w/o --recurse-submodules and a conflict (Josh)\n* Migration command creates gitdir configs before enabling the extension (Patrick)\n* Reworded and reformatted the extension doc (Junio, Ben)\n* Reworded submodule.<name>.gitdir documentation for clarity (Patrick)\n* Added references to extensions.submodulePathConfig in error msgs (Junio, Patrick)\n* Minor whitespace fixes and others nits (Patrick)\n\nRange-diff v6 -> v7:\n 1:  a6024a7569 =  1:  a6024a7569 submodule--helper: use submodule_name_to_gitdir in add_submodule\n 2:  59058180eb =  2:  59058180eb submodule: always validate gitdirs inside submodule_name_to_gitdir\n 3:  b685f4f9ff =  3:  b685f4f9ff builtin/submodule--helper: add gitdir command\n 4:  0247c28cbc !  4:  a5d6db00ac submodule: introduce extensions.submodulePathConfig\n    @@ Documentation/config/extensions.adoc: relativeWorktrees:::\n      \t`worktree.useRelativePaths` config set to `true`.\n      \n     +submodulePathConfig:::\n    -+\tIf enabled, the submodule.<name>.gitdir config is the single source of\n    -+\ttruth for submodule gitdir paths and is always set for new submodules.\n    -+\tGit will error if a module does not have submodule.<name>.gitdir set.\n    -+\tExisting pre-extension submodules need to be migrated by adding the\n    -+\tmissing config entries. This is done manually for now, e.g. for each\n    -+\tsubmodule: \"git config submodule.<name>.gitdir .git/modules/<name>\".\n    ++\tThis extension is for the minority of users who:\n    +++\n    ++--\n    ++* Encounter errors like\t`refusing to create ... in another submodule's git dir`\n    ++  due to a number of reasons, like case-insensitive filesystem conflicts when\n    ++  creating modules named `foo` and `Foo`.\n    ++* Require more flexible submodule layouts, for example due to nested names like\n    ++  `foo`, `foo/bar` and `foo/baz` not supported by the default gitdir mechanism\n    ++  which uses `.git/modules/<plain-name>` locations, causing further conflicts.\n    ++--\n    +++\n    ++When `extensions.submodulePathConfig` is enabled, the `submodule.<name>.gitdir`\n    ++config becomes the single source of truth for all submodule gitdir paths and is\n    ++automatically set for all new submodules both during clone and init operations.\n    +++\n    ++Git will error out if a module does not have a corresponding\n    ++`submodule.<name>.gitdir` set.\n    +++\n    ++Existing (pre-extension) submodules need to be migrated by adding the missing\n    ++config entries. This is done manually for now, e.g. for each submodule:\n    ++`git config submodule.<name>.gitdir .git/modules/<name>`.\n     +\n      worktreeConfig:::\n      \tIf enabled, then worktrees will load config settings from the\n    @@ Documentation/config/submodule.adoc: submodule.<name>.active::\n      \tdetails.\n      \n     +submodule.<name>.gitdir::\n    -+\tThis sets the gitdir path for submodule <name>. It only works when\n    -+\t`extensions.submodulePathConfig` is enabled, otherwise it does nothing.\n    -+\tWhen the extension is enabled, this config is the single source of truth\n    -+\tfor submodule gitdir paths and git will throw an error if it is missing.\n    ++\tThis sets the gitdir path for submodule <name>. This configuration is\n    ++\trespected when `extensions.submodulePathConfig` is enabled, otherwise it\n    ++\thas no effect. When enabled, this config becomes the single source of\n    ++\ttruth for submodule gitdir paths and git will error if it is missing.\n     +\tSee linkgit:git-config[1] for details.\n     +\n      submodule.active::\n    @@ builtin/submodule--helper.c: struct init_cb {\n     +\n     +\t key = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n     +\n    -+\t /* Nothing to do if the config already exists. */\n    ++\t/* Nothing to do if the config already exists. */\n     +\tif (!repo_config_get_string_tmp(the_repository, key, &value)) {\n     +\t\tfree(key);\n     +\t\treturn 0;\n    @@ builtin/submodule--helper.c: struct init_cb {\n     +{\n     +\tstruct strbuf gitdir_path = STRBUF_INIT;\n     +\n    -+\t/* The config is set only when extensions.submodulePathConfig is enabled */\n    -+\tif (!the_repository->repository_format_submodule_path_cfg)\n    -+\t\treturn;\n    -+\n     +\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n     +\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n     +\t\tstrbuf_release(&gitdir_path);\n    @@ builtin/submodule--helper.c: static void init_submodule(const char *path, const\n      \t\t\tdie(_(\"Failed to register update mode for submodule path '%s'\"), displaypath);\n      \t}\n     +\n    -+\tcreate_default_gitdir_config(sub->name);\n    ++\tif (the_repository->repository_format_submodule_path_cfg)\n    ++\t\tcreate_default_gitdir_config(sub->name);\n     +\n      \tstrbuf_release(&sb);\n      \tfree(displaypath);\n      \tfree(url);\n    +@@ builtin/submodule--helper.c: static int clone_submodule(const struct module_clone_data *clone_data,\n    + \t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n    + \t\tunlink(head);\n    + \t\tfree(head);\n    +-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n    +-\t\t      \"git dir\"), sm_gitdir);\n    ++\t\tdie(_(\"refusing to create/use '%s' in another submodule's git dir. \"\n    ++\t\t      \"Enabling extensions.submodulePathConfig should fix this.\"),\n    ++\t\t    sm_gitdir);\n    + \t}\n    + \n    + \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n     @@ builtin/submodule--helper.c: static int module_add(int argc, const char **argv, const char *prefix,\n      \tadd_data.progress = !!progress;\n      \tadd_data.dissociate = !!dissociate;\n      \n    -+\tcreate_default_gitdir_config(add_data.sm_name);\n    ++\tif (the_repository->repository_format_submodule_path_cfg)\n    ++\t\tcreate_default_gitdir_config(add_data.sm_name);\n     +\n      \tif (add_submodule(&add_data))\n      \t\tgoto cleanup;\n    @@ setup.h: struct repository_format {\n      \tint compat_hash_algo;\n     \n      ## submodule.c ##\n    +@@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n    + \t\t\t\tif (validate_submodule_git_dir(git_dir,\n    + \t\t\t\t\t\t\t       sub->name) < 0)\n    + \t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n    +-\t\t\t\t\t      \"another submodule's git dir\"),\n    +-\t\t\t\t\t    git_dir);\n    ++\t\t\t\t\t      \"another submodule's git dir. \"\n    ++\t\t\t\t\t      \"Enabling extensions.submodulePathConfig \"\n    ++\t\t\t\t\t      \"should fix this.\"), git_dir);\n    + \t\t\t\tfree(git_dir);\n    + \t\t\t}\n    + \t\t} else {\n     @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n      void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n      \t\t\t      const char *submodule_name)\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     -\t */\n     -\trepo_git_path_append(r, buf, \"modules/\");\n     -\tstrbuf_addstr(buf, submodule_name);\n    -+\tconst char *gitdir;\n    -+\tchar *key;\n    -+\tint ret;\n    -+\n    -+\t/* If extensions.submodulePathConfig is disabled, continue to use the plain path */\n     +\tif (!r->repository_format_submodule_path_cfg) {\n    ++\t\t/*\n    ++\t\t * If extensions.submodulePathConfig is disabled,\n    ++\t\t * continue to use the plain path.\n    ++\t\t */\n     +\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n    -+\t\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n    -+\t\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n    -+\t\t\t      \"git dir\"), buf->buf);\n    ++\t} else {\n    ++\t\tconst char *gitdir;\n    ++\t\tchar *key;\n    ++\t\tint ret;\n    ++\n    ++\t\t/* Otherwise the extension is enabled, so use the gitdir config. */\n    ++\t\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n    ++\t\tret = repo_config_get_string_tmp(r, key, &gitdir);\n    ++\t\tFREE_AND_NULL(key);\n    ++\n    ++\t\tif (ret)\n    ++\t\t\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n    ++\t\t\t      \"Please ensure it is set, for example by running something like: \"\n    ++\t\t\t      \"'git config submodule.%s.gitdir .git/modules/%s'. For details \"\n    ++\t\t\t      \"see the extensions.submodulePathConfig documentation.\"),\n    ++\t\t\t    submodule_name, submodule_name, submodule_name, submodule_name);\n     +\n    -+\t\treturn; /* plain gitdir is valid for use */\n    -+\t}\n    -+\n    -+\t/* Extension is enabled: use the gitdir config if it exists */\n    -+\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n    -+\tret = repo_config_get_string_tmp(r, key, &gitdir);\n    -+\tFREE_AND_NULL(key);\n    -+\n    -+\tif (!ret) {\n     +\t\tstrbuf_addstr(buf, gitdir);\n    -+\n    -+\t\t/* validate because users might have modified the config */\n    -+\t\tif (validate_submodule_git_dir(buf->buf, submodule_name))\n    -+\t\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n    -+\t\t\t      \"if it is unique or conflicts with another module\"),\n    -+\t\t\t    submodule_name, gitdir);\n    -+\n    -+\t\treturn; /* gitdir from config is valid for use */\n     +\t}\n      \n     -\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n     -\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n     -\t\t      \"git dir\"), buf->buf);\n    -+\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n    -+\t      \"Please ensure it is set, for example by running something like: \"\n    -+\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n    -+\t    submodule_name, submodule_name, submodule_name, submodule_name);\n    ++\t/* validate because users might have modified the config */\n    ++\tif (validate_submodule_git_dir(buf->buf, submodule_name))\n    ++\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n    ++\t\t      \"if it is unique or conflicts with another module\"),\n    ++\t\t    submodule_name, buf->buf);\n      }\n     \n      ## t/lib-verify-submodule-gitdir-path.sh (new) ##\n 5:  486dd2c45c <  -:  ---------- submodule: allow runtime enabling extensions.submodulePathConfig\n -:  ---------- >  5:  6ee88ee00a submodule: allow runtime enabling extensions.submodulePathConfig\n 6:  9d5e050701 !  6:  7f93a5cee0 submodule--helper: add gitdir migration command\n    @@ Commit message\n         Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n     \n      ## Documentation/config/extensions.adoc ##\n    -@@ Documentation/config/extensions.adoc: submodulePathConfig:::\n    - \tThis extension can also be enabled as a global runtime config, with\n    - \tthe local repository config having precedence (overwrites it).\n    - \tExisting pre-extension submodules need to be migrated by adding the\n    --\tmissing config entries. This is done manually for now, e.g. for each\n    --\tsubmodule: \"git config submodule.<name>.gitdir .git/modules/<name>\".\n    -+\tmissing config entries. This can be done manually, e.g. for each\n    -+\tsubmodule: \"git config submodule.<name>.gitdir .git/modules/<name>\",\n    -+\tor via the \"git submodule--helper migrate-gitdir-configs\" command\n    -+\twhich iterates over all submodules and attempts to migrate them.\n    - \n    - worktreeConfig:::\n    - \tIf enabled, then worktrees will load config settings from the\n    +@@ Documentation/config/extensions.adoc: Git will error out if a module does not have a corresponding\n    + `submodule.<name>.gitdir` set.\n    + +\n    + Existing (pre-extension) submodules need to be migrated by adding the missing\n    +-config entries. This is done manually for now, e.g. for each submodule:\n    +-`git config submodule.<name>.gitdir .git/modules/<name>`.\n    ++config entries. This can be done manually, e.g. for each submodule:\n    ++`git config submodule.<name>.gitdir .git/modules/<name>`, or via the\n    ++`git submodule--helper migrate-gitdir-configs` command which iterates over all\n    ++submodules and attempts to migrate them.\n    + +\n    + The extension can be enabled automatically for new repositories by setting\n    + `init.autoSetupSubmodulePathConfig` to `true`, for example by running\n     \n      ## builtin/submodule--helper.c ##\n     @@ builtin/submodule--helper.c: static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n    @@ builtin/submodule--helper.c: static int module_gitdir(int argc, const char **arg\n     +\tDIR *dir;\n     +\tstruct dirent *de;\n     +\n    -+\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n    -+\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n    -+\t\t      \"Please enable it for migration to work, for example: \"\n    -+\t\t      \"git config core.repositoryformatversion 1\"));\n    -+\n    -+\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n    -+\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n    -+\t\t      \"for migration to work. Please enable it in the root repo: \"\n    -+\t\t      \"git config extensions.submodulePathConfig true\"));\n    -+\n    -+\trepo->repository_format_submodule_path_cfg = 1;\n    -+\n     +\trepo_git_path_append(repo, &module_dir, \"modules/\");\n     +\n     +\tdir = opendir(module_dir.buf);\n    @@ builtin/submodule--helper.c: static int module_gitdir(int argc, const char **arg\n     +\tclosedir(dir);\n     +\tstrbuf_release(&module_dir);\n     +\n    ++\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n    ++\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n    ++\t\t      \"Please enable it for migration to work, for example: \"\n    ++\t\t      \"git config core.repositoryformatversion 1\"));\n    ++\n    ++\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n    ++\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n    ++\t\t      \"for migration to work. Please enable it in the root repo: \"\n    ++\t\t      \"git config extensions.submodulePathConfig true\"));\n    ++\n    ++\trepo->repository_format_submodule_path_cfg = 1;\n    ++\n     +\treturn 0;\n     +}\n     +\n    @@ builtin/submodule--helper.c: int cmd_submodule__helper(int argc,\n      \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n     \n      ## t/t7425-submodule-gitdir-path-extension.sh ##\n    -@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'runtime config extensions.submodulePathConfig on existing r\n    - \t)\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` respects init.autoSetupSub\n    + \tgit config --global --unset init.autoSetupSubmodulePathConfig\n      '\n      \n     +test_expect_success 'submodule--helper migrates legacy modules' '\n    -+\tgit init sm-repo-1 &&\n    -+\ttest_commit -C sm-repo-1 initial-1 &&\n    -+\tgit init sm-repo-2 &&\n    -+\ttest_commit -C sm-repo-2 initial-2 &&\n    -+\n    -+\t# ensure the global config is disabled so we can actually test migration\n    -+\tgit config --global extensions.submodulePathConfig false &&\n    -+\n    -+\tgit init -b main migrate-test &&\n     +\t(\n    -+\t\tcd migrate-test &&\n    -+\n    -+\t\tgit submodule add ../sm-repo-1 sub1 &&\n    -+\t\tgit submodule add ../sm-repo-2 sub2 &&\n    -+\t\ttest_commit add-submodules &&\n    ++\t\tcd upstream &&\n     +\n    -+\t\t# gitdir configs should not exist\n    ++\t\t# previous submodules exist and were not migrated yet\n     +\t\ttest_must_fail git config submodule.sub1.gitdir &&\n     +\t\ttest_must_fail git config submodule.sub2.gitdir &&\n    ++\t\ttest_path_is_dir .git/modules/sub1 &&\n    ++\t\ttest_path_is_dir .git/modules/sub2 &&\n     +\n    ++\t\t# run migration\n     +\t\tgit submodule--helper migrate-gitdir-configs &&\n     +\n    -+\t\t# gitdir configs must exist after migration\n    ++\t\t# test that migration worked\n     +\t\tgit config submodule.sub1.gitdir >actual &&\n     +\t\techo \".git/modules/sub1\" >expect &&\n     +\t\ttest_cmp expect actual &&\n    -+\n     +\t\tgit config submodule.sub2.gitdir >actual &&\n     +\t\techo \".git/modules/sub2\" >expect &&\n    ++\t\ttest_cmp expect actual &&\n    ++\n    ++\t\t# repository extension is enabled after migration\n    ++\t\tgit config extensions.submodulePathConfig > actual &&\n    ++\t\techo \"true\" > expect &&\n     +\t\ttest_cmp expect actual\n     +\t)\n     +'\n    ++\n    ++test_expect_success '`git clone --recurse-submodules` works after migration' '\n    ++\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n    ++\n    ++\t# test with extension disabled after the upstream repo was migrated\n    ++\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n    ++\t(\n    ++\t\tcd repo-clone-recursive &&\n    ++\n    ++\t\t# init.autoSetupSubmodulePathConfig was disabled before clone, so\n    ++\t\t# the repo extension config should also be off, the migration ignored\n    ++\t\ttest_must_fail git config extensions.submodulePathConfig &&\n    ++\n    ++\t\t# modules should look like there was no migration done\n    ++\t\ttest_must_fail git config submodule.sub1.gitdir &&\n    ++\t\ttest_must_fail git config submodule.sub2.gitdir &&\n    ++\t\ttest_path_is_dir .git/modules/sub1 &&\n    ++\t\ttest_path_is_dir .git/modules/sub2\n    ++\t) &&\n    ++\trm -rf repo-clone-recursive &&\n    ++\n    ++\t# enable the extension, then retry the clone\n    ++\tgit config --global init.autoSetupSubmodulePathConfig true &&\n    ++\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n    ++\t(\n    ++\t\tcd repo-clone-recursive &&\n    ++\n    ++\t\t# repository extension is enabled\n    ++\t\tgit config extensions.submodulePathConfig > actual &&\n    ++\t\techo \"true\" > expect &&\n    ++\t\ttest_cmp expect actual &&\n    ++\n    ++\t\t# gitdir configs exist for submodules\n    ++\t\tgit config submodule.sub1.gitdir &&\n    ++\t\tgit config submodule.sub2.gitdir &&\n    ++\t\ttest_path_is_dir .git/modules/sub1 &&\n    ++\t\ttest_path_is_dir .git/modules/sub2\n    ++\t)\n    ++'\n     +\n      test_done\n 7:  a164370edc =  7:  2e55521bbf builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n 8:  94f785817a !  8:  7fd920aa70 submodule--helper: fix filesystem collisions by encoding gitdir paths\n    @@ builtin/submodule--helper.c\n      #define OPT_QUIET (1 << 0)\n      #define OPT_CACHED (1 << 1)\n     @@ builtin/submodule--helper.c: static void create_default_gitdir_config(const char *submodule_name)\n    - \t    !submodule_path_config_enabled)\n    - \t\treturn;\n    + {\n    + \tstruct strbuf gitdir_path = STRBUF_INIT;\n      \n     +\t/* Case 1: try the plain module name */\n      \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n    @@ submodule.c\n      #include \"setup.h\"\n     +#include \"url.h\"\n      \n    - int submodule_path_config_enabled;\n    - \n    + static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n    + static int initialized_fetch_ref_tips;\n     @@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n      \treturn ret;\n      }\n    @@ submodule.c: int validate_submodule_git_dir(char *git_dir, const char *submodule\n      \tchar *p = xstrdup(path);\n     \n      ## t/t7425-submodule-gitdir-path-extension.sh ##\n    -@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'submodule--helper migrates legacy modules' '\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` works after migration' '\n      \t)\n      '\n      \n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'submodule--help\n     +\t(\n     +\t\tcd clone_nested &&\n     +\t\t# disable extension and verify failure\n    -+\t\tgit config extensions.submodulePathConfig false &&\n    ++\t\tgit config --replace-all extensions.submodulePathConfig false &&\n     +\t\ttest_must_fail git submodule add ./thing2 hippo/foobar &&\n     +\t\t# re-enable extension and verify it works\n    -+\t\tgit config extensions.submodulePathConfig true &&\n    ++\t\tgit config --replace-all extensions.submodulePathConfig true &&\n     +\t\tgit submodule add ./thing2 hippo/foobar\n     +\t)\n     +'\n 9:  0061979221 =  9:  e21656aad4 submodule: fix case-folding gitdir filesystem collisions\n10:  6d71434f39 ! 10:  9e3da1c585 submodule: hash the submodule name for the gitdir path\n    @@ builtin/submodule--helper.c: static int validate_and_set_submodule_gitdir(struct\n     +\tunsigned char raw_name_hash[GIT_MAX_RAWSZ];\n     +\tint header_len;\n      \n    - \t/* The config is set only when extensions.submodulePathConfig is enabled */\n    - \tif (!the_repository->repository_format_submodule_path_cfg &&\n    + \t/* Case 1: try the plain module name */\n    + \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n     @@ builtin/submodule--helper.c: static void create_default_gitdir_config(const char *submodule_name)\n      \t\t\treturn;\n      \t}\n -:  ---------- > 11:  56bca88799 submodule: detect conflicts with existing gitdir configs\n\nAdrian Ratiu (11):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  submodule: always validate gitdirs inside submodule_name_to_gitdir\n  builtin/submodule--helper: add gitdir command\n  submodule: introduce extensions.submodulePathConfig\n  submodule: allow runtime enabling extensions.submodulePathConfig\n  submodule--helper: add gitdir migration command\n  builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n  submodule--helper: fix filesystem collisions by encoding gitdir paths\n  submodule: fix case-folding gitdir filesystem collisions\n  submodule: hash the submodule name for the gitdir path\n  submodule: detect conflicts with existing gitdir configs\n\n Documentation/config/extensions.adoc       |  29 ++\n Documentation/config/init.adoc             |   6 +\n Documentation/config/submodule.adoc        |   7 +\n builtin/credential-store.c                 |   7 +-\n builtin/submodule--helper.c                | 201 +++++++-\n repository.c                               |   1 +\n repository.h                               |   1 +\n setup.c                                    |  19 +-\n setup.h                                    |   1 +\n submodule.c                                | 219 +++++++--\n t/lib-verify-submodule-gitdir-path.sh      |  24 +\n t/meson.build                              |   1 +\n t/t7425-submodule-gitdir-path-extension.sh | 508 +++++++++++++++++++++\n t/t9902-completion.sh                      |   1 +\n url.c                                      |  13 +\n url.h                                      |  14 +\n 16 files changed, 1000 insertions(+), 52 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-gitdir-path-extension.sh\n\n-- \n2.51.2\n\n"},{"id":"532589","messageId":"20251220101528.1227487-9-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 08/11] submodule--helper: fix filesystem collisions by encoding gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:25Z","receivedAt":"2025-12-20T10:16:34Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Fix nested filesystem collisions by url-encoding gitdir paths stored\nin submodule.%s.gitdir, when extensions.submodulePathConfig is enabled.\n\nCredit goes to Junio and Patrick for coming up with this design: the\nencoding is only applied when necessary, to newly added submodules.\n\nExisting modules don't need the encoding because git already errors\nout when detecting nested gitdirs before this patch.\n\nThis commit adds the basic url-encoding and some tests. Next commits\nextend the encode -> validate -> retry loop to fix more conflicts.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 12 +++++\n submodule.c                                | 42 +++++++++++++++-\n t/t7425-submodule-gitdir-path-extension.sh | 57 ++++++++++++++++++++++\n 3 files changed, 110 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 5a6436f18f..81ba95d11c 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -34,6 +34,7 @@\n #include \"list-objects-filter-options.h\"\n #include \"wildmatch.h\"\n #include \"strbuf.h\"\n+#include \"url.h\"\n \n #define OPT_QUIET (1 << 0)\n #define OPT_CACHED (1 << 1)\n@@ -465,12 +466,23 @@ static void create_default_gitdir_config(const char *submodule_name)\n {\n \tstruct strbuf gitdir_path = STRBUF_INIT;\n \n+\t/* Case 1: try the plain module name */\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n \tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n \t\tstrbuf_release(&gitdir_path);\n \t\treturn;\n \t}\n \n+\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n+\t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\n \t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\ndiff --git a/submodule.c b/submodule.c\nindex e3692009cd..cf32872ec7 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -31,6 +31,7 @@\n #include \"commit-reach.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"url.h\"\n \n static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n static int initialized_fetch_ref_tips;\n@@ -2246,12 +2247,43 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+/*\n+ * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n+ * This does not print errors like the non-encoded version, because encoding is supposed\n+ * to mitigate / fix all these.\n+ */\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+{\n+\tconst char *modules_marker = \"/modules/\";\n+\tchar *p = git_dir, *last_submodule_name = NULL;\n+\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n+\t\t    \"extensions.submodulePathConfig enabled.\");\n+\n+\t/* Find the last submodule name in the gitdir path (modules can be nested). */\n+\twhile ((p = strstr(p, modules_marker))) {\n+\t\tlast_submodule_name = p + strlen(modules_marker);\n+\t\tp++;\n+\t}\n+\n+\t/* Prevent the use of '/' in encoded names */\n+\tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+static int validate_submodule_legacy_git_dir(char *git_dir, const char *submodule_name)\n {\n \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n \tchar *p;\n \tint ret = 0;\n \n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tBUG(\"validate_submodule_git_dir() must be called with \"\n+\t\t    \"extensions.submodulePathConfig disabled.\");\n+\n \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n \t    strcmp(p, submodule_name))\n \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n@@ -2287,6 +2319,14 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \treturn 0;\n }\n \n+int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+{\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\treturn validate_submodule_legacy_git_dir(git_dir, submodule_name);\n+\n+\treturn validate_submodule_encoded_git_dir(git_dir, submodule_name);\n+}\n+\n int validate_submodule_path(const char *path)\n {\n \tchar *p = xstrdup(path);\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 6ca9f13a59..dbe18f2925 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -327,4 +327,61 @@ test_expect_success '`git clone --recurse-submodules` works after migration' '\n \t)\n '\n \n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of encoded sibling submodules must not be nested' '\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules nested clone_nested &&\n+\n+\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n+\tgit -C clone_nested config submodule.hippo.gitdir > actual &&\n+\ttest_grep \"\\.git/modules/hippo$\" actual &&\n+\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks &&\n+\tgit -C clone_nested config submodule.hippo/hooks.gitdir > actual &&\n+\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules --jobs=2 nested clone_parallel &&\n+\n+\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n+\tgit -C clone_nested config submodule.hippo.gitdir > actual &&\n+\ttest_grep \"\\.git/modules/hippo$\" actual &&\n+\n+\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks &&\n+\tgit -C clone_nested config submodule.hippo/hooks.gitdir > actual &&\n+\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n+'\n+\n+test_expect_success 'disabling extensions.submodulePathConfig prevents nested submodules' '\n+\t(\n+\t\tcd clone_nested &&\n+\t\t# disable extension and verify failure\n+\t\tgit config --replace-all extensions.submodulePathConfig false &&\n+\t\ttest_must_fail git submodule add ./thing2 hippo/foobar &&\n+\t\t# re-enable extension and verify it works\n+\t\tgit config --replace-all extensions.submodulePathConfig true &&\n+\t\tgit submodule add ./thing2 hippo/foobar\n+\t)\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532590","messageId":"20251220101528.1227487-10-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 09/11] submodule: fix case-folding gitdir filesystem collisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:26Z","receivedAt":"2025-12-20T10:16:39Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new check when extension.submodulePathConfig is enabled, to\ndetect and prevent case-folding filesystem colisions. When this\nnew check is triggered, a stricter casefolding aware URI encoding\nis used to percent-encode uppercase characters.\n\nBy using this check/retry mechanism the uppercase encoding is\nonly applied when necessary, so case-sensitive filesystems are\nnot affected.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 26 ++++++++++-\n submodule.c                                | 53 +++++++++++++++++++++-\n t/t7425-submodule-gitdir-path-extension.sh | 35 ++++++++++++++\n url.c                                      |  7 +++\n url.h                                      |  7 +++\n 5 files changed, 126 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 81ba95d11c..d601306882 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -473,7 +473,7 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\treturn;\n \t}\n \n-\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\t/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n \tstrbuf_reset(&gitdir_path);\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n \tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n@@ -482,6 +482,30 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\treturn;\n \t}\n \n+\t/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\treturn;\n+\n+\t/* Case 2.3: Try some derived gitdir names, see if one sticks */\n+\tfor (char c = '0'; c <= '9'; c++) {\n+\t\tstrbuf_reset(&gitdir_path);\n+\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n+\t\tstrbuf_addch(&gitdir_path, c);\n+\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\t\treturn;\n+\n+\t\tstrbuf_reset(&gitdir_path);\n+\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n+\t\tstrbuf_addch(&gitdir_path, c);\n+\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\t\treturn;\n+\t}\n+\n \t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\ndiff --git a/submodule.c b/submodule.c\nindex cf32872ec7..834c794b7d 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2247,15 +2247,58 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n+static int check_casefolding_conflict(const char *git_dir,\n+\t\t\t\t      const char *submodule_name,\n+\t\t\t\t      const bool suffixes_match)\n+{\n+\tchar *p, *modules_dir = xstrdup(git_dir);\n+\tstruct dirent *de;\n+\tDIR *dir = NULL;\n+\tint ret = 0;\n+\n+\tif ((p = find_last_dir_sep(modules_dir)))\n+\t\t*p = '\\0';\n+\n+\t/* No conflict is possible if modules_dir doesn't exist (first clone) */\n+\tif (!is_directory(modules_dir))\n+\t\tgoto cleanup;\n+\n+\tdir = opendir(modules_dir);\n+\tif (!dir) {\n+\t\tret = -1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/* Check for another directory under .git/modules that differs only in case. */\n+\twhile ((de = readdir(dir))) {\n+\t\tif (!strcmp(de->d_name, \".\") || !strcmp(de->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tif ((suffixes_match || is_git_directory(git_dir)) &&\n+\t\t    !strcasecmp(de->d_name, submodule_name) &&\n+\t\t    strcmp(de->d_name, submodule_name)) {\n+\t\t\tret = -1; /* collision found */\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+cleanup:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tfree(modules_dir);\n+\treturn ret;\n+}\n+\n /*\n  * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n  * This does not print errors like the non-encoded version, because encoding is supposed\n  * to mitigate / fix all these.\n  */\n-static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name)\n {\n \tconst char *modules_marker = \"/modules/\";\n \tchar *p = git_dir, *last_submodule_name = NULL;\n+\tint config_ignorecase = 0;\n \n \tif (!the_repository->repository_format_submodule_path_cfg)\n \t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n@@ -2271,6 +2314,14 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n \t\treturn -1;\n \n+\t/* Prevent conflicts on case-folding filesystems */\n+\trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n+\tif (ignore_case || config_ignorecase) {\n+\t\tbool suffixes_match = !strcmp(last_submodule_name, submodule_name);\n+\t\treturn check_casefolding_conflict(git_dir, submodule_name,\n+\t\t\t\t\t\t  suffixes_match);\n+\t}\n+\n \treturn 0;\n }\n \ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex dbe18f2925..eb9c80787c 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -384,4 +384,39 @@ test_expect_success 'disabling extensions.submodulePathConfig prevents nested su\n \t)\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are correctly encoded' '\n+\tgit clone -c extensions.submodulePathConfig=true main cloned-folding &&\n+\t(\n+\t\tcd cloned-folding &&\n+\n+\t\t# conflict: the \"folding\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"folding\" &&\n+\t\ttest_commit lowercase &&\n+\t\tgit submodule add ../new-sub \"FoldinG\" &&\n+\t\ttest_commit uppercase &&\n+\n+\t\t# conflict: the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"FOO\" &&\n+\t\ttest_commit uppercase-foo &&\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\ttest_commit lowercase-foo &&\n+\n+\t\t# create a multi conflict between foobar, fooBar and foo%42ar\n+\t\t# the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"foobar\" &&\n+\t\ttest_commit lowercase-foobar &&\n+\t\tgit submodule add ../new-sub \"foo%42ar\" &&\n+\t\ttest_commit encoded-foo%42ar &&\n+\t\tgit submodule add ../new-sub \"fooBar\" &&\n+\t\ttest_commit mixed-fooBar\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-folding \"folding\" \"modules/folding\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FoldinG\" \"modules/%46oldin%47\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FOO\" \"modules/FOO\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foobar\" \"modules/foobar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo%42ar\" \"modules/foo%42ar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n+'\n+\n test_done\ndiff --git a/url.c b/url.c\nindex adc289229c..3ca5987e90 100644\n--- a/url.c\n+++ b/url.c\n@@ -9,6 +9,13 @@ int is_rfc3986_unreserved(char ch)\n \t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n }\n \n+int is_casefolding_rfc3986_unreserved(char c)\n+{\n+\treturn (c >= 'a' && c <= 'z') ||\n+\t       (c >= '0' && c <= '9') ||\n+\t       c == '-' || c == '.' || c == '_' || c == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex e644c3c809..cd9140e994 100644\n--- a/url.h\n+++ b/url.h\n@@ -28,4 +28,11 @@ void str_end_url_with_slash(const char *url, char **dest);\n  */\n int is_rfc3986_unreserved(char ch);\n \n+/*\n+ * This is a variant of is_rfc3986_unreserved() that treats uppercase\n+ * letters as \"reserved\". This forces them to be percent-encoded, allowing\n+ * 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.\n+ */\n+int is_casefolding_rfc3986_unreserved(char c);\n+\n #endif /* URL_H */\n-- \n2.51.2\n\n"},{"id":"532591","messageId":"20251220101528.1227487-11-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 10/11] submodule: hash the submodule name for the gitdir path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:27Z","receivedAt":"2025-12-20T10:16:44Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"If none of the previous plain-text / encoding / derivation steps work\nand case 2.4 is reached, then try a hash of the submodule name to see\nif that can be a valid gitdir before giving up and throwing an error.\n\nThis is a \"last resort\" type of measure to avoid conflicts since it\nloses the human readability of the gitdir path. This logic will be\nreached in rare cases, as can be seen in the test we added.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 19 +++++++\n t/t7425-submodule-gitdir-path-extension.sh | 59 ++++++++++++++++++++++\n 2 files changed, 78 insertions(+)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex d601306882..0c5563dc3f 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -465,6 +465,10 @@ static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n static void create_default_gitdir_config(const char *submodule_name)\n {\n \tstruct strbuf gitdir_path = STRBUF_INIT;\n+\tstruct git_hash_ctx ctx;\n+\tchar hex_name_hash[GIT_MAX_HEXSZ + 1], header[128];\n+\tunsigned char raw_name_hash[GIT_MAX_RAWSZ];\n+\tint header_len;\n \n \t/* Case 1: try the plain module name */\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n@@ -506,6 +510,21 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\t\treturn;\n \t}\n \n+\t/* Case 2.4: If all the above failed, try a hash of the name as a last resort */\n+\theader_len = snprintf(header, sizeof(header), \"blob %zu\", strlen(submodule_name));\n+\tthe_hash_algo->init_fn(&ctx);\n+\tthe_hash_algo->update_fn(&ctx, header, header_len);\n+\tthe_hash_algo->update_fn(&ctx, \"\\0\", 1);\n+\tthe_hash_algo->update_fn(&ctx, submodule_name, strlen(submodule_name));\n+\tthe_hash_algo->final_fn(raw_name_hash, &ctx);\n+\thash_to_hex_algop_r(hex_name_hash, raw_name_hash, the_hash_algo);\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", hex_name_hash);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n \t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex eb9c80787c..5fcfc29363 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -419,4 +419,63 @@ test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are corre\n \tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a last resort' '\n+\tgit clone -c extensions.submodulePathConfig=true main cloned-hash &&\n+\t(\n+\t\tcd cloned-hash &&\n+\n+\t\t# conflict: add all submodule conflicting variants until we reach the\n+\t\t# final hashing conflict resolution for submodule \"foo\"\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\tgit submodule add ../new-sub \"foo0\" &&\n+\t\tgit submodule add ../new-sub \"foo1\" &&\n+\t\tgit submodule add ../new-sub \"foo2\" &&\n+\t\tgit submodule add ../new-sub \"foo3\" &&\n+\t\tgit submodule add ../new-sub \"foo4\" &&\n+\t\tgit submodule add ../new-sub \"foo5\" &&\n+\t\tgit submodule add ../new-sub \"foo6\" &&\n+\t\tgit submodule add ../new-sub \"foo7\" &&\n+\t\tgit submodule add ../new-sub \"foo8\" &&\n+\t\tgit submodule add ../new-sub \"foo9\" &&\n+\t\tgit submodule add ../new-sub \"%46oo\" &&\n+\t\tgit submodule add ../new-sub \"%46oo0\" &&\n+\t\tgit submodule add ../new-sub \"%46oo1\" &&\n+\t\tgit submodule add ../new-sub \"%46oo2\" &&\n+\t\tgit submodule add ../new-sub \"%46oo3\" &&\n+\t\tgit submodule add ../new-sub \"%46oo4\" &&\n+\t\tgit submodule add ../new-sub \"%46oo5\" &&\n+\t\tgit submodule add ../new-sub \"%46oo6\" &&\n+\t\tgit submodule add ../new-sub \"%46oo7\" &&\n+\t\tgit submodule add ../new-sub \"%46oo8\" &&\n+\t\tgit submodule add ../new-sub \"%46oo9\" &&\n+\t\ttest_commit add-foo-variants &&\n+\t\tgit submodule add ../new-sub \"Foo\" &&\n+\t\ttest_commit add-uppercase-foo\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo\" \"modules/foo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo0\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo1\" \"modules/foo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo2\" \"modules/foo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo3\" \"modules/foo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo4\" \"modules/foo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo5\" \"modules/foo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo6\" \"modules/foo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo7\" \"modules/foo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo8\" \"modules/foo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo9\" \"modules/foo9\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo\" \"modules/%46oo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo0\" \"modules/%46oo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo1\" \"modules/%46oo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo2\" \"modules/%46oo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo3\" \"modules/%46oo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo4\" \"modules/%46oo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo5\" \"modules/%46oo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo6\" \"modules/%46oo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo7\" \"modules/%46oo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo8\" \"modules/%46oo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo9\" \"modules/%46oo9\" &&\n+\thash=$(printf \"Foo\" | git hash-object --stdin) &&\n+\tverify_submodule_gitdir_path cloned-hash \"Foo\" \"modules/${hash}\"\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532592","messageId":"20251220101528.1227487-12-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"[PATCH v7 11/11] submodule: detect conflicts with existing gitdir configs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-20T10:15:28Z","receivedAt":"2025-12-20T10:16:47Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Credit goes to Emily and Josh for testing and noticing a corner-case\nwhich caused conflicts with existing gitdir configs to silently pass\nvalidation, then fail later in add_submodule() with a cryptic error:\n\nfatal: A git directory for 'nested%2fsub' is found locally with remote(s):\n  origin\t/.../trash directory.t7425-submodule-gitdir-path-extension/sub\n\nThis change ensures the validation step checks existing gitdirs for\nconflicts. We only have to do this for submodules having gitdirs,\nbecause those without submodule.%s.gitdir need to be migrated and\nwill throw an error earlier in the submodule codepath.\n\nQuoting Josh:\n My testing setup has been as follows:\n * Using our locally-built Git with our downstream patch of [1] included:\n   * create a repo \"sub\"\n   * create a repo \"super\"\n   * In \"super\":\n     * mkdir nested\n     * git submodule add ../sub nested/sub\n     * Verify that the submodule's gitdir is .git/modules/nested%2fsub\n * Using a build of git from upstream `next` plus this series:\n   * git config set --global extensions.submodulepathconfig true\n   * git clone --recurse-submodules super super2\n   * create a repo \"nested%2fsub\"\n   * In \"super2\":\n     * git submodule add ../nested%2fsub\n\nAt this point I'd expect the collision detection / encoding to take\neffect, but instead I get the error listed above.\nEnd quote\n\nSuggested-by: Josh Steadmon <steadmon@google.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n submodule.c                                | 60 ++++++++++++++++++++++\n t/t7425-submodule-gitdir-path-extension.sh | 27 ++++++++++\n 2 files changed, 87 insertions(+)\n\ndiff --git a/submodule.c b/submodule.c\nindex 834c794b7d..d778e8eca7 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2289,6 +2289,61 @@ static int check_casefolding_conflict(const char *git_dir,\n \treturn ret;\n }\n \n+struct submodule_from_gitdir_cb {\n+\tconst char *gitdir;\n+\tconst char *submodule_name;\n+\tbool conflict_found;\n+};\n+\n+static int find_conflict_by_gitdir_cb(const char *var, const char *value,\n+\t\t\t\t      const struct config_context *ctx UNUSED, void *data)\n+{\n+\tstruct submodule_from_gitdir_cb *cb = data;\n+\tconst char *submodule_name_start;\n+\tsize_t submodule_name_len;\n+\tconst char *suffix = \".gitdir\";\n+\tsize_t suffix_len = strlen(suffix);\n+\n+\tif (!skip_prefix(var, \"submodule.\", &submodule_name_start))\n+\t\treturn 0;\n+\n+\t/* Check if submodule_name_start ends with \".gitdir\" */\n+\tsubmodule_name_len = strlen(submodule_name_start);\n+\tif (submodule_name_len < suffix_len ||\n+\t    strcmp(submodule_name_start + submodule_name_len - suffix_len, suffix) != 0)\n+\t\treturn 0; /* Does not end with \".gitdir\" */\n+\n+\tsubmodule_name_len -= suffix_len;\n+\n+\t/*\n+\t * A conflict happens if:\n+\t * 1. The submodule names are different and\n+\t * 2. The gitdir paths resolve to the same absolute path\n+\t */\n+\tif (value && strncmp(cb->submodule_name, submodule_name_start, submodule_name_len)) {\n+\t\tchar *abs_path_cb = absolute_pathdup(cb->gitdir);\n+\t\tchar *abs_path_value = absolute_pathdup(value);\n+\n+\t\tcb->conflict_found = !strcmp(abs_path_cb, abs_path_value);\n+\n+\t\tfree(abs_path_cb);\n+\t\tfree(abs_path_value);\n+\t}\n+\n+\treturn cb->conflict_found;\n+}\n+static bool submodule_conflicts_with_existing(const char *gitdir, const char *submodule_name)\n+{\n+\tstruct submodule_from_gitdir_cb cb = { 0 };\n+\tcb.submodule_name = submodule_name;\n+\tcb.gitdir = gitdir;\n+\n+\t/* Find conflicts with existing repo gitdir configs */\n+\trepo_config(the_repository, find_conflict_by_gitdir_cb, &cb);\n+\n+\treturn cb.conflict_found;\n+}\n+\n /*\n  * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n  * This does not print errors like the non-encoded version, because encoding is supposed\n@@ -2314,6 +2369,11 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n \t\treturn -1;\n \n+\t/* Prevent conflicts with existing submodule gitdirs */\n+\tif (is_git_directory(git_dir) &&\n+\t    submodule_conflicts_with_existing(git_dir, submodule_name))\n+\t\t\treturn -1;\n+\n \t/* Prevent conflicts on case-folding filesystems */\n \trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n \tif (ignore_case || config_ignorecase) {\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 5fcfc29363..5f221c507a 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -478,4 +478,31 @@ test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a\n \tverify_submodule_gitdir_path cloned-hash \"Foo\" \"modules/${hash}\"\n '\n \n+test_expect_success 'submodule gitdir conflicts with previously encoded name (local config)' '\n+\tgit init -b main super_with_encoded &&\n+\t(\n+\t\tcd super_with_encoded &&\n+\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\t# Add a submodule with a nested path\n+\t\tgit submodule add --name \"nested/sub\" ../sub nested/sub &&\n+\t\ttest_commit add-encoded-gitdir &&\n+\n+\t\tverify_submodule_gitdir_path . \"nested/sub\" \"modules/nested%2fsub\" &&\n+\t\ttest_path_is_dir \".git/modules/nested%2fsub\"\n+\t) &&\n+\n+\t# create a submodule that will conflict with the encoded gitdir name:\n+\t# the existing gitdir is \".git/modules/nested%2fsub\", which is used\n+\t# by \"nested/sub\", so the new submod will get another (non-conflicting)\n+\t# name: \"nested%252fsub\".\n+\t(\n+\t\tcd super_with_encoded &&\n+\t\tgit submodule add ../sub \"nested%2fsub\" &&\n+\t\tverify_submodule_gitdir_path . \"nested%2fsub\" \"modules/nested%252fsub\" &&\n+\t\ttest_path_is_dir \".git/modules/nested%252fsub\"\n+\t)\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"532604","messageId":"xmqqbjjstv71.fsf@gitster.g","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v7 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-21T02:39:14Z","receivedAt":"2025-12-21T02:39:16Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> As always, this is based on the latest master branch, I've checkd\n> for conflicts with next/seen, pushed to Github [1] and succesfully\n> ran the CI [2].\n\nOK.  I've queued it on a bit older tip of master (namely, where all\nthe previous iterations have bene queued on top---this will make it\nmuch easier to compare the iterative changes) and they seem to apply\ncleanly.\n\nWill replace and queue, but due to family reasons, I may not\nimmediately have time to look at this or any other topics much of\nthis week.\n\nThanks, and happy holidays.\n"},{"id":"532605","messageId":"xmqqy0mwsedz.fsf@gitster.g","threadId":"63967","inReplyTo":"20251220101528.1227487-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH v7 04/11] submodule: introduce extensions.submodulePathConfig","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-21T03:27:36Z","receivedAt":"2025-12-21T03:27:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\nThis comment is not about the primary contents of this series, but\nI notice that the whitespace immediately after \"errors like\" is not\na SP but a HT here.\n\n> +* Encounter errors like\t`refusing to create ... in another submodule's git dir`\n   123456701234567012345670\n\nI have already alluded to it as a #leftoverbit in a different topic,\nbut we probably want to have a new whitespace error class to detect\na HT in the middle of a sentence that should have been a SP.\n\nPerhaps the rule would be something like a HT that is at the column\nthat is at (tab-width - 1) modulo tab-width (default 8, but the\nusual attribute applies), that is surrounded by non-whitespace\ncharacters on both sides.\n\nI may be counting off-by-one, though ;-) The quoted problematic line\nhas, labeling the leftmost column as 1, the HT at the 24th column.\n\n cf. https://lore.kernel.org/git/xmqq5xa76z0o.fsf@gitster.g/\n"},{"id":"532657","messageId":"87y0mtwcbc.fsf@gentoo.mail-host-address-is-not-set","threadId":"63967","inReplyTo":"xmqqy0mwsedz.fsf@gitster.g","subject":"Re: [PATCH v7 04/11] submodule: introduce extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2025-12-23T13:35:35Z","receivedAt":"2025-12-23T13:35:59Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Sun, 21 Dec 2025, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n> This comment is not about the primary contents of this series, but\n> I notice that the whitespace immediately after \"errors like\" is not\n> a SP but a HT here.\n>\n>> +* Encounter errors like\t`refusing to create ... in another submodule's git dir`\n>    123456701234567012345670\n>\n> I have already alluded to it as a #leftoverbit in a different topic,\n> but we probably want to have a new whitespace error class to detect\n> a HT in the middle of a sentence that should have been a SP.\n>\n> Perhaps the rule would be something like a HT that is at the column\n> that is at (tab-width - 1) modulo tab-width (default 8, but the\n> usual attribute applies), that is surrounded by non-whitespace\n> characters on both sides.\n>\n> I may be counting off-by-one, though ;-) The quoted problematic line\n> has, labeling the leftmost column as 1, the HT at the 24th column.\n>\n>  cf. https://lore.kernel.org/git/xmqq5xa76z0o.fsf@gitster.g/\n\nI think the algorithm you pointed out is sound, seems to work, so I sent\na separate patch for it:\n\nhttps://public-inbox.org/git/20251223132756.604036-1-adrian.ratiu@collabora.com/T/#u\n\nWill fix the whitespace in this series on the next re-roll.\n\nThanks!\n"},{"id":"533105","messageId":"aVy46Y9GIMlFCzIq@pks.im","threadId":"63967","inReplyTo":"20251220101528.1227487-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v7 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T07:25:29Z","receivedAt":"2026-01-06T07:25:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 20, 2025 at 12:15:17PM +0200, Adrian Ratiu wrote:\n> Changes in v7:\n> * Simplified submodule_name_to_gitdir() code structure. Logically it still is\n>   the same like in v6, just easier to read. (Patrick)\n> * Reworked the global config to enable the extension only for new repos\n>   during clone and init operations (Patrick, Junio)\n> * Improved validation of existing submodules with encoded names (Josh, Emily)\n> * Added more tests for init, clone w/o --recurse-submodules and a conflict (Josh)\n> * Migration command creates gitdir configs before enabling the extension (Patrick)\n> * Reworded and reformatted the extension doc (Junio, Ben)\n> * Reworded submodule.<name>.gitdir documentation for clarity (Patrick)\n> * Added references to extensions.submodulePathConfig in error msgs (Junio, Patrick)\n> * Minor whitespace fixes and others nits (Patrick)\n\nI've had another look at this now. Concept-wise I'm now happy with this\nseries, so what remains is mostly just nits. I think that this series\nshould become ready in one or two iterations. Thanks!\n\nPatrick\n"},{"id":"533106","messageId":"aVy4-LZ7Lz_tuqdp@pks.im","threadId":"63967","inReplyTo":"20251220101528.1227487-5-adrian.ratiu@collabora.com","subject":"Re: [PATCH v7 04/11] submodule: introduce extensions.submodulePathConfig","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T07:25:44Z","receivedAt":"2026-01-06T07:25:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 20, 2025 at 12:15:21PM +0200, Adrian Ratiu wrote:\n> diff --git a/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\n> index 0672d99117..9c260a69f6 100644\n> --- a/Documentation/config/submodule.adoc\n> +++ b/Documentation/config/submodule.adoc\n> @@ -52,6 +52,13 @@ submodule.<name>.active::\n>  \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n>  \tdetails.\n>  \n> +submodule.<name>.gitdir::\n> +\tThis sets the gitdir path for submodule <name>. This configuration is\n> +\trespected when `extensions.submodulePathConfig` is enabled, otherwise it\n> +\thas no effect. When enabled, this config becomes the single source of\n> +\ttruth for submodule gitdir paths and git will error if it is missing.\n\nTiny nit: s/git/Git/\n\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index 3bc139ff9c..f8cae345a5 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -435,6 +435,48 @@ struct init_cb {\n>  };\n>  #define INIT_CB_INIT { 0 }\n>  \n> +static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n> +\t\t\t\t\t     const char *submodule_name)\n> +{\n> +\tconst char *value;\n> +\tchar *key;\n> +\n> +\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n> +\t\treturn -1;\n> +\n> +\t key = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n\nTiny nit: extra space before `key`.\n\n> diff --git a/submodule.c b/submodule.c\n> index f645372a18..e3692009cd 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2570,30 +2571,35 @@ int submodule_to_gitdir(struct repository *repo,\n>  void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n>  \t\t\t      const char *submodule_name)\n>  {\n> -\t/*\n> -\t * NEEDSWORK: The current way of mapping a submodule's name to\n> -\t * its location in .git/modules/ has problems with some naming\n> -\t * schemes. For example, if a submodule is named \"foo\" and\n> -\t * another is named \"foo/bar\" (whether present in the same\n> -\t * superproject commit or not - the problem will arise if both\n> -\t * superproject commits have been checked out at any point in\n> -\t * time), or if two submodule names only have different cases in\n> -\t * a case-insensitive filesystem.\n> -\t *\n> -\t * There are several solutions, including encoding the path in\n> -\t * some way, introducing a submodule.<name>.gitdir config in\n> -\t * .git/config (not .gitmodules) that allows overriding what the\n> -\t * gitdir of a submodule would be (and teach Git, upon noticing\n> -\t * a clash, to automatically determine a non-clashing name and\n> -\t * to write such a config), or introducing a\n> -\t * submodule.<name>.gitdir config in .gitmodules that repo\n> -\t * administrators can explicitly set. Nothing has been decided,\n> -\t * so for now, just append the name at the end of the path.\n> -\t */\n> -\trepo_git_path_append(r, buf, \"modules/\");\n> -\tstrbuf_addstr(buf, submodule_name);\n> +\tif (!r->repository_format_submodule_path_cfg) {\n> +\t\t/*\n> +\t\t * If extensions.submodulePathConfig is disabled,\n> +\t\t * continue to use the plain path.\n> +\t\t */\n> +\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n> +\t} else {\n> +\t\tconst char *gitdir;\n> +\t\tchar *key;\n> +\t\tint ret;\n> +\n> +\t\t/* Otherwise the extension is enabled, so use the gitdir config. */\n> +\t\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n> +\t\tret = repo_config_get_string_tmp(r, key, &gitdir);\n> +\t\tFREE_AND_NULL(key);\n> +\n> +\t\tif (ret)\n> +\t\t\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n> +\t\t\t      \"Please ensure it is set, for example by running something like: \"\n> +\t\t\t      \"'git config submodule.%s.gitdir .git/modules/%s'. For details \"\n> +\t\t\t      \"see the extensions.submodulePathConfig documentation.\"),\n> +\t\t\t    submodule_name, submodule_name, submodule_name, submodule_name);\n> +\n> +\t\tstrbuf_addstr(buf, gitdir);\n> +\t}\n>  \n> -\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n> -\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n> -\t\t      \"git dir\"), buf->buf);\n> +\t/* validate because users might have modified the config */\n> +\tif (validate_submodule_git_dir(buf->buf, submodule_name))\n> +\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n> +\t\t      \"if it is unique or conflicts with another module\"),\n> +\t\t    submodule_name, buf->buf);\n>  }\n\nNit: this error message may be misleading, as it is also used in the\ncase where the submodule path was derived from its name. I think the\noriginal message should be retained, maybe followed by a call to\n`advice()` that users may wish to enable the extension to fix this in\ncase it's not enabled already.\n\n> diff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\n> new file mode 100644\n> index 0000000000..62794df976\n> --- /dev/null\n> +++ b/t/lib-verify-submodule-gitdir-path.sh\n> @@ -0,0 +1,24 @@\n> +# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n> +\n> +# This does not check filesystem existence. That is done in submodule.c via the\n> +# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n> +# might or might not exist (e.g. when adding a new submodule), so this only\n> +# checks the expected configuration path, which might be overridden by the user.\n> +\n> +verify_submodule_gitdir_path() {\n\nNit: there should be a space between function name and `()`.\n\n> diff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\n> new file mode 100755\n> index 0000000000..5d52a289f8\n> --- /dev/null\n> +++ b/t/t7425-submodule-gitdir-path-extension.sh\n> @@ -0,0 +1,138 @@\n> +#!/bin/sh\n> +\n> +test_description='submodulePathConfig extension works as expected'\n\nI think I didn't spot any test that verifies the actual config values\nthat get written when the repository extension is enabled. Specifially,\nwhat I think we ought to test there is that the generated submodule path\nis relative to the repository and not an absolute path.\n\n> +\n> +. ./test-lib.sh\n> +. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n> +\n> +test_expect_success 'setup: allow file protocol' '\n> +       git config --global protocol.file.allow always\n> +'\n> +\n> +test_expect_success 'create repo with mixed extension submodules' '\n> +\tgit init -b main legacy-sub &&\n> +\ttest_commit -C legacy-sub legacy-initial &&\n> +\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n> +\n> +\tgit init -b main new-sub &&\n> +\ttest_commit -C new-sub new-initial &&\n> +\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n> +\n> +\tgit init -b main main &&\n> +\t(\n> +\t\tcd main &&\n> +\t\tgit submodule add ../legacy-sub legacy &&\n> +\t\ttest_commit legacy-sub &&\n> +\n> +\t\t# trigger the \"die_path_inside_submodule\" check\n> +\t\ttest_must_fail git submodule add ../new-sub \"legacy/nested\" &&\n> +\n> +\t\tgit config core.repositoryformatversion 1 &&\n> +\t\tgit config extensions.submodulePathConfig true &&\n> +\n> +\t\tgit submodule add ../new-sub \"New Sub\" &&\n> +\t\ttest_commit new &&\n> +\n> +\t\t# retrigger the \"die_path_inside_submodule\" check with encoding\n> +\t\ttest_must_fail git submodule add ../new-sub \"New Sub/nested2\"\n> +       )\n> +'\n> +\n> +test_expect_success 'verify new submodule gitdir config' '\n> +\tgit -C main config submodule.\"New Sub\".gitdir > actual &&\n> +\techo \".git/modules/New Sub\" > expect &&\n\nNit: we don't typically have a space between \">\" and the target file.\nAlso true in other test cases.\n\nPatrick\n"},{"id":"533107","messageId":"aVy4_vtbuYlyppXT@pks.im","threadId":"63967","inReplyTo":"20251220101528.1227487-6-adrian.ratiu@collabora.com","subject":"Re: [PATCH v7 05/11] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T07:25:50Z","receivedAt":"2026-01-06T07:25:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 20, 2025 at 12:15:22PM +0200, Adrian Ratiu wrote:\n> diff --git a/Documentation/config/init.adoc b/Documentation/config/init.adoc\n> index e45b2a8121..293a2ddbdf 100644\n> --- a/Documentation/config/init.adoc\n> +++ b/Documentation/config/init.adoc\n> @@ -18,3 +18,9 @@ endif::[]\n>  \tSee `--ref-format=` in linkgit:git-init[1]. Both the command line\n>  \toption and the `GIT_DEFAULT_REF_FORMAT` environment variable take\n>  \tprecedence over this config.\n> +\n> +init.autoSetupSubmodulePathConfig::\n> +\tA boolean that specifies if `git init` and `git clone` should\n> +\tautomatically set `extensions.submodulePathConfig` to `true`. This\n> +\tallows all new repositories to automatically use the submodule path\n> +\textension. Defaults to `false` when unset.\n\nNit: I would have called this `init.defaultSubmodulePathConfig` so that\nit fits in better with the other configuration we have that impatcs how\nwe set up repository extensions.\n\n> diff --git a/setup.c b/setup.c\n> index 428427d689..3e05fe7c58 100644\n> --- a/setup.c\n> +++ b/setup.c\n> @@ -2712,6 +2712,16 @@ int init_db(const char *git_dir, const char *real_git_dir,\n>  \t\t\t\t\t  initial_branch, flags & INIT_DB_QUIET);\n>  \tcreate_object_directory();\n>  \n> +\trepo_config_get_bool(the_repository, \"init.autoSetupSubmodulePathConfig\",\n> +\t\t\t     &auto_setup_submodule_path_config);\n> +\tif (auto_setup_submodule_path_config) {\n> +\t\tint version = 0;\n> +\t\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &version);\n> +\t\tif (version < 1)\n> +\t\t\trepo_config_set(the_repository, \"core.repositoryformatversion\", \"1\");\n> +\t\trepo_config_set(the_repository, \"extensions.submodulepathconfig\", \"true\");\n> +\t}\n> +\n>  \tif (repo_settings_get_shared_repository(the_repository)) {\n>  \t\tchar buf[10];\n>  \t\t/* We do not spell \"group\" and such, so that\n\nI think that this logic woudl be better located in\n`initialize_repository_version()`, which is also where we set up all the\nother extensions. Feel free to disregard though in case there's a good\nreason you don't do it there.\n\nPatrick\n"},{"id":"533108","messageId":"aVy5Ap7wX4aW-UL4@pks.im","threadId":"63967","inReplyTo":"20251220101528.1227487-7-adrian.ratiu@collabora.com","subject":"Re: [PATCH v7 06/11] submodule--helper: add gitdir migration command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T07:25:54Z","receivedAt":"2026-01-06T07:26:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 20, 2025 at 12:15:23PM +0200, Adrian Ratiu wrote:\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index f8cae345a5..5a6436f18f 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -1266,6 +1266,63 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n>  \treturn 0;\n>  }\n>  \n> +static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n> +\t\t\t  const char *prefix UNUSED, struct repository *repo)\n> +{\n> +\tstruct strbuf module_dir = STRBUF_INIT;\n> +\tDIR *dir;\n> +\tstruct dirent *de;\n> +\n> +\trepo_git_path_append(repo, &module_dir, \"modules/\");\n> +\n> +\tdir = opendir(module_dir.buf);\n> +\tif (!dir)\n> +\t\tdie(_(\"could not open '%s'\"), module_dir.buf);\n> +\n> +\twhile ((de = readdir(dir))) {\n> +\t\tstruct strbuf gitdir_path = STRBUF_INIT;\n> +\t\tchar *key;\n> +\t\tconst char *value;\n> +\n> +\t\tif (is_dot_or_dotdot(de->d_name))\n> +\t\t\tcontinue;\n> +\n> +\t\tstrbuf_addf(&gitdir_path, \"%s/%s\", module_dir.buf, de->d_name);\n> +\t\tif (!is_git_directory(gitdir_path.buf)) {\n> +\t\t\tstrbuf_release(&gitdir_path);\n> +\t\t\tcontinue;\n> +\t\t}\n> +\t\tstrbuf_release(&gitdir_path);\n> +\n> +\t\tkey = xstrfmt(\"submodule.%s.gitdir\", de->d_name);\n> +\t\tif (!repo_config_get_string_tmp(repo, key, &value)) {\n> +\t\t\t/* Already has a gitdir config, nothing to do. */\n> +\t\t\tfree(key);\n> +\t\t\tcontinue;\n> +\t\t}\n> +\t\tfree(key);\n> +\n> +\t\tcreate_default_gitdir_config(de->d_name);\n> +\t}\n> +\n> +\tclosedir(dir);\n> +\tstrbuf_release(&module_dir);\n> +\n> +\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n> +\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n> +\t\t      \"Please enable it for migration to work, for example: \"\n> +\t\t      \"git config core.repositoryformatversion 1\"));\n\nWe should probably be careful here to not override the repository format\nversion in case it's already greater than 0. We don't have version 2\nyet, but if we ever do this would otherwise need to be changed.\n\n> diff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\n> index 06ee1ff86b..6ca9f13a59 100755\n> --- a/t/t7425-submodule-gitdir-path-extension.sh\n> +++ b/t/t7425-submodule-gitdir-path-extension.sh\n> @@ -260,4 +260,71 @@ test_expect_success '`git clone --recurse-submodules` respects init.autoSetupSub\n>  \tgit config --global --unset init.autoSetupSubmodulePathConfig\n>  '\n>  \n> +test_expect_success 'submodule--helper migrates legacy modules' '\n> +\t(\n> +\t\tcd upstream &&\n> +\n> +\t\t# previous submodules exist and were not migrated yet\n> +\t\ttest_must_fail git config submodule.sub1.gitdir &&\n> +\t\ttest_must_fail git config submodule.sub2.gitdir &&\n> +\t\ttest_path_is_dir .git/modules/sub1 &&\n> +\t\ttest_path_is_dir .git/modules/sub2 &&\n> +\n> +\t\t# run migration\n> +\t\tgit submodule--helper migrate-gitdir-configs &&\n> +\n> +\t\t# test that migration worked\n> +\t\tgit config submodule.sub1.gitdir >actual &&\n> +\t\techo \".git/modules/sub1\" >expect &&\n> +\t\ttest_cmp expect actual &&\n> +\t\tgit config submodule.sub2.gitdir >actual &&\n> +\t\techo \".git/modules/sub2\" >expect &&\n> +\t\ttest_cmp expect actual &&\n> +\n> +\t\t# repository extension is enabled after migration\n> +\t\tgit config extensions.submodulePathConfig > actual &&\n> +\t\techo \"true\" > expect &&\n\nStyle nit: redirection operator strikes again :) Probably makes sense to\nscan through all commits for this style issue.\n\nPatrick\n"},{"id":"533109","messageId":"aVy5DAgijOJKA4dZ@pks.im","threadId":"63967","inReplyTo":"20251220101528.1227487-12-adrian.ratiu@collabora.com","subject":"Re: [PATCH v7 11/11] submodule: detect conflicts with existing gitdir configs","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T07:26:04Z","receivedAt":"2026-01-06T07:26:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 20, 2025 at 12:15:28PM +0200, Adrian Ratiu wrote:\n> diff --git a/submodule.c b/submodule.c\n> index 834c794b7d..d778e8eca7 100644\n> --- a/submodule.c\n> +++ b/submodule.c\n> @@ -2289,6 +2289,61 @@ static int check_casefolding_conflict(const char *git_dir,\n>  \treturn ret;\n>  }\n>  \n> +struct submodule_from_gitdir_cb {\n> +\tconst char *gitdir;\n> +\tconst char *submodule_name;\n> +\tbool conflict_found;\n> +};\n> +\n> +static int find_conflict_by_gitdir_cb(const char *var, const char *value,\n> +\t\t\t\t      const struct config_context *ctx UNUSED, void *data)\n> +{\n> +\tstruct submodule_from_gitdir_cb *cb = data;\n> +\tconst char *submodule_name_start;\n> +\tsize_t submodule_name_len;\n> +\tconst char *suffix = \".gitdir\";\n> +\tsize_t suffix_len = strlen(suffix);\n> +\n> +\tif (!skip_prefix(var, \"submodule.\", &submodule_name_start))\n> +\t\treturn 0;\n> +\n> +\t/* Check if submodule_name_start ends with \".gitdir\" */\n> +\tsubmodule_name_len = strlen(submodule_name_start);\n> +\tif (submodule_name_len < suffix_len ||\n> +\t    strcmp(submodule_name_start + submodule_name_len - suffix_len, suffix) != 0)\n> +\t\treturn 0; /* Does not end with \".gitdir\" */\n> +\n> +\tsubmodule_name_len -= suffix_len;\n> +\n> +\t/*\n> +\t * A conflict happens if:\n> +\t * 1. The submodule names are different and\n> +\t * 2. The gitdir paths resolve to the same absolute path\n> +\t */\n> +\tif (value && strncmp(cb->submodule_name, submodule_name_start, submodule_name_len)) {\n> +\t\tchar *abs_path_cb = absolute_pathdup(cb->gitdir);\n> +\t\tchar *abs_path_value = absolute_pathdup(value);\n> +\n> +\t\tcb->conflict_found = !strcmp(abs_path_cb, abs_path_value);\n> +\n> +\t\tfree(abs_path_cb);\n> +\t\tfree(abs_path_value);\n> +\t}\n> +\n> +\treturn cb->conflict_found;\n> +}\n\nNit: empty line missing between these two functions.\n\nPatrick\n"},{"id":"533228","messageId":"877bttidul.fsf@collabora.com","threadId":"63967","inReplyTo":"aVy4-LZ7Lz_tuqdp@pks.im","subject":"Re: [PATCH v7 04/11] submodule: introduce extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T16:31:30Z","receivedAt":"2026-01-07T16:32:01Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 06 Jan 2026, Patrick Steinhardt <ps@pks.im> wrote:\n>> diff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\n>> new file mode 100755\n>> index 0000000000..5d52a289f8\n>> --- /dev/null\n>> +++ b/t/t7425-submodule-gitdir-path-extension.sh\n>> @@ -0,0 +1,138 @@\n>> +#!/bin/sh\n>> +\n>> +test_description='submodulePathConfig extension works as expected'\n>\n> I think I didn't spot any test that verifies the actual config values\n> that get written when the repository extension is enabled. Specifially,\n> what I think we ought to test there is that the generated submodule path\n> is relative to the repository and not an absolute path.\n>\n\nYes, I agree, it's a good idea to add a test which verifies the gitdir\nconfig value to be relative to the repository regardless if the URL\nbeing added is absolute or not.\n\nThis might actually be an oversight in the current code, which the test\nwill uncover and I will fix, if necessary.\n\nWill do this in v8 and also fix all the nits you pointed out.\n\nThanks again,\nAdrian\n"},{"id":"533229","messageId":"874ioxidg4.fsf@collabora.com","threadId":"63967","inReplyTo":"aVy4_vtbuYlyppXT@pks.im","subject":"Re: [PATCH v7 05/11] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T16:40:11Z","receivedAt":"2026-01-07T16:40:36Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 06 Jan 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Sat, Dec 20, 2025 at 12:15:22PM +0200, Adrian Ratiu wrote:\n>> diff --git a/Documentation/config/init.adoc b/Documentation/config/init.adoc\n>> index e45b2a8121..293a2ddbdf 100644\n>> --- a/Documentation/config/init.adoc\n>> +++ b/Documentation/config/init.adoc\n>> @@ -18,3 +18,9 @@ endif::[]\n>>  \tSee `--ref-format=` in linkgit:git-init[1]. Both the command line\n>>  \toption and the `GIT_DEFAULT_REF_FORMAT` environment variable take\n>>  \tprecedence over this config.\n>> +\n>> +init.autoSetupSubmodulePathConfig::\n>> +\tA boolean that specifies if `git init` and `git clone` should\n>> +\tautomatically set `extensions.submodulePathConfig` to `true`. This\n>> +\tallows all new repositories to automatically use the submodule path\n>> +\textension. Defaults to `false` when unset.\n>\n> Nit: I would have called this `init.defaultSubmodulePathConfig` so that\n> it fits in better with the other configuration we have that impatcs how\n> we set up repository extensions.\n\nI'll rename it in v8.\n\n>\n>> diff --git a/setup.c b/setup.c\n>> index 428427d689..3e05fe7c58 100644\n>> --- a/setup.c\n>> +++ b/setup.c\n>> @@ -2712,6 +2712,16 @@ int init_db(const char *git_dir, const char *real_git_dir,\n>>  \t\t\t\t\t  initial_branch, flags & INIT_DB_QUIET);\n>>  \tcreate_object_directory();\n>>  \n>> +\trepo_config_get_bool(the_repository, \"init.autoSetupSubmodulePathConfig\",\n>> +\t\t\t     &auto_setup_submodule_path_config);\n>> +\tif (auto_setup_submodule_path_config) {\n>> +\t\tint version = 0;\n>> +\t\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &version);\n>> +\t\tif (version < 1)\n>> +\t\t\trepo_config_set(the_repository, \"core.repositoryformatversion\", \"1\");\n>> +\t\trepo_config_set(the_repository, \"extensions.submodulepathconfig\", \"true\");\n>> +\t}\n>> +\n>>  \tif (repo_settings_get_shared_repository(the_repository)) {\n>>  \t\tchar buf[10];\n>>  \t\t/* We do not spell \"group\" and such, so that\n>\n> I think that this logic woudl be better located in\n> `initialize_repository_version()`, which is also where we set up all the\n> other extensions. Feel free to disregard though in case there's a good\n> reason you don't do it there.\n\nNo specific reason from the top of my mind. I think we can move it\nwithout issues. Will do it in v8 as well.\n\nThanks again,\nAdrian\n"},{"id":"533230","messageId":"871pk1idcm.fsf@collabora.com","threadId":"63967","inReplyTo":"aVy5Ap7wX4aW-UL4@pks.im","subject":"Re: [PATCH v7 06/11] submodule--helper: add gitdir migration command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T16:42:17Z","receivedAt":"2026-01-07T16:42:41Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Tue, 06 Jan 2026, Patrick Steinhardt <ps@pks.im> wrote:\n> On Sat, Dec 20, 2025 at 12:15:23PM +0200, Adrian Ratiu wrote:\n>> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n>> index f8cae345a5..5a6436f18f 100644\n>> --- a/builtin/submodule--helper.c\n>> +++ b/builtin/submodule--helper.c\n>> @@ -1266,6 +1266,63 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n>>  \treturn 0;\n>>  }\n>>  \n>> +static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n>> +\t\t\t  const char *prefix UNUSED, struct repository *repo)\n>> +{\n>> +\tstruct strbuf module_dir = STRBUF_INIT;\n>> +\tDIR *dir;\n>> +\tstruct dirent *de;\n>> +\n>> +\trepo_git_path_append(repo, &module_dir, \"modules/\");\n>> +\n>> +\tdir = opendir(module_dir.buf);\n>> +\tif (!dir)\n>> +\t\tdie(_(\"could not open '%s'\"), module_dir.buf);\n>> +\n>> +\twhile ((de = readdir(dir))) {\n>> +\t\tstruct strbuf gitdir_path = STRBUF_INIT;\n>> +\t\tchar *key;\n>> +\t\tconst char *value;\n>> +\n>> +\t\tif (is_dot_or_dotdot(de->d_name))\n>> +\t\t\tcontinue;\n>> +\n>> +\t\tstrbuf_addf(&gitdir_path, \"%s/%s\", module_dir.buf, de->d_name);\n>> +\t\tif (!is_git_directory(gitdir_path.buf)) {\n>> +\t\t\tstrbuf_release(&gitdir_path);\n>> +\t\t\tcontinue;\n>> +\t\t}\n>> +\t\tstrbuf_release(&gitdir_path);\n>> +\n>> +\t\tkey = xstrfmt(\"submodule.%s.gitdir\", de->d_name);\n>> +\t\tif (!repo_config_get_string_tmp(repo, key, &value)) {\n>> +\t\t\t/* Already has a gitdir config, nothing to do. */\n>> +\t\t\tfree(key);\n>> +\t\t\tcontinue;\n>> +\t\t}\n>> +\t\tfree(key);\n>> +\n>> +\t\tcreate_default_gitdir_config(de->d_name);\n>> +\t}\n>> +\n>> +\tclosedir(dir);\n>> +\tstrbuf_release(&module_dir);\n>> +\n>> +\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n>> +\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n>> +\t\t      \"Please enable it for migration to work, for example: \"\n>> +\t\t      \"git config core.repositoryformatversion 1\"));\n>\n> We should probably be careful here to not override the repository format\n> version in case it's already greater than 0. We don't have version 2\n> yet, but if we ever do this would otherwise need to be changed.\n\nAck, it's best to future proof this. Will do in v8.\n\n>> diff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\n>> index 06ee1ff86b..6ca9f13a59 100755\n>> --- a/t/t7425-submodule-gitdir-path-extension.sh\n>> +++ b/t/t7425-submodule-gitdir-path-extension.sh\n>> @@ -260,4 +260,71 @@ test_expect_success '`git clone --recurse-submodules` respects init.autoSetupSub\n>>  \tgit config --global --unset init.autoSetupSubmodulePathConfig\n>>  '\n>>  \n>> +test_expect_success 'submodule--helper migrates legacy modules' '\n>> +\t(\n>> +\t\tcd upstream &&\n>> +\n>> +\t\t# previous submodules exist and were not migrated yet\n>> +\t\ttest_must_fail git config submodule.sub1.gitdir &&\n>> +\t\ttest_must_fail git config submodule.sub2.gitdir &&\n>> +\t\ttest_path_is_dir .git/modules/sub1 &&\n>> +\t\ttest_path_is_dir .git/modules/sub2 &&\n>> +\n>> +\t\t# run migration\n>> +\t\tgit submodule--helper migrate-gitdir-configs &&\n>> +\n>> +\t\t# test that migration worked\n>> +\t\tgit config submodule.sub1.gitdir >actual &&\n>> +\t\techo \".git/modules/sub1\" >expect &&\n>> +\t\ttest_cmp expect actual &&\n>> +\t\tgit config submodule.sub2.gitdir >actual &&\n>> +\t\techo \".git/modules/sub2\" >expect &&\n>> +\t\ttest_cmp expect actual &&\n>> +\n>> +\t\t# repository extension is enabled after migration\n>> +\t\tgit config extensions.submodulePathConfig > actual &&\n>> +\t\techo \"true\" > expect &&\n>\n> Style nit: redirection operator strikes again :) Probably makes sense to\n> scan through all commits for this style issue.\n\nAck, will do.\n"},{"id":"533245","messageId":"20260107230145.517562-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:34Z","receivedAt":"2026-01-07T23:02:53Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello everyone,\n\nFor those new to the series, we're implementing a submodule gitdir\nextension which allows us to have a unified way to determine gitdirs\nand do things like encode submodule paths to avoid FS conflicts.\n\nv8 addresses nits and small code improvements, no more design changes\nas the series is quieting down. Most of the range-diff is whitespace\nfixes this time. :)\n\nPatches 1-6 implement the basic mechanisms of the new extension.\nPatches 7-11 improve filesystem conflict detection and resolution.\n\nAs always, this is based on the latest master branch, I've checkd\nfor conflicts with next/seen, pushed to Github [1] and succesfully\nran the CI [2].\n\n1: https://github.com/10ne1/git/tree/dev/aratiu/encoding-v8\n2: https://github.com/10ne1/git/actions/runs/20798198092\n\nChanges in v8:\n* Added a new test to ensure gitdir config path is relative (Patrick)\n* Improved gitdir validation error message and added advice (Patrick)\n* Renamed init.autoSetupSubmodulePathConfig to init.defaultSubmodulePathConfig\n  and moved its init logic to initialize_repository_version() (Patrick)\n* repositoryformatversion is only set to 1 if it's 0, so it doesn't\n  overwrite potential future higher versions (Patrick)\n* Fixed global init.defaultSubmodulePathConfig leak between tests (Adrian)\n* Whitespace and other minor fixes (Junio, Patrick)\n\nRange-diff between v7 -> v8:\n 1:  87bdd023f8 =  1:  5aae0df74b submodule--helper: use submodule_name_to_gitdir in add_submodule\n 2:  8ea022eb6f =  2:  041d921487 submodule: always validate gitdirs inside submodule_name_to_gitdir\n 3:  2092faf443 =  3:  12ff77be2d builtin/submodule--helper: add gitdir command\n 4:  b72e8d3610 !  4:  faaea085d2 submodule: introduce extensions.submodulePathConfig\n    @@ Documentation/config/extensions.adoc: relativeWorktrees:::\n     +\tThis extension is for the minority of users who:\n     ++\n     +--\n    -+* Encounter errors like\t`refusing to create ... in another submodule's git dir`\n    ++* Encounter errors like `refusing to create ... in another submodule's git dir`\n     +  due to a number of reasons, like case-insensitive filesystem conflicts when\n     +  creating modules named `foo` and `Foo`.\n     +* Require more flexible submodule layouts, for example due to nested names like\n    @@ Documentation/config/submodule.adoc: submodule.<name>.active::\n     +\tThis sets the gitdir path for submodule <name>. This configuration is\n     +\trespected when `extensions.submodulePathConfig` is enabled, otherwise it\n     +\thas no effect. When enabled, this config becomes the single source of\n    -+\ttruth for submodule gitdir paths and git will error if it is missing.\n    ++\ttruth for submodule gitdir paths and Git will error if it is missing.\n     +\tSee linkgit:git-config[1] for details.\n     +\n      submodule.active::\n    @@ builtin/submodule--helper.c: struct init_cb {\n     +\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n     +\t\treturn -1;\n     +\n    -+\t key = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n    ++\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n     +\n     +\t/* Nothing to do if the config already exists. */\n     +\tif (!repo_config_get_string_tmp(the_repository, key, &value)) {\n    @@ setup.h: struct repository_format {\n      \tint compat_hash_algo;\n     \n      ## submodule.c ##\n    +@@\n    + #include \"commit-reach.h\"\n    + #include \"read-cache-ll.h\"\n    + #include \"setup.h\"\n    ++#include \"advice.h\"\n    + \n    + static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n    + static int initialized_fetch_ref_tips;\n     @@ submodule.c: int submodule_move_head(const char *path, const char *super_prefix,\n      \t\t\t\tif (validate_submodule_git_dir(git_dir,\n      \t\t\t\t\t\t\t       sub->name) < 0)\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     +\t\tconst char *gitdir;\n     +\t\tchar *key;\n     +\t\tint ret;\n    -+\n    + \n    +-\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n     +\t\t/* Otherwise the extension is enabled, so use the gitdir config. */\n     +\t\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n     +\t\tret = repo_config_get_string_tmp(r, key, &gitdir);\n    @@ submodule.c: int submodule_to_gitdir(struct repository *repo,\n     +\n     +\t\tstrbuf_addstr(buf, gitdir);\n     +\t}\n    - \n    --\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n    --\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n    --\t\t      \"git dir\"), buf->buf);\n    ++\n     +\t/* validate because users might have modified the config */\n    -+\tif (validate_submodule_git_dir(buf->buf, submodule_name))\n    -+\t\tdie(_(\"invalid 'submodule.%s.gitdir' config: '%s' please check \"\n    -+\t\t      \"if it is unique or conflicts with another module\"),\n    -+\t\t    submodule_name, buf->buf);\n    ++\tif (validate_submodule_git_dir(buf->buf, submodule_name)) {\n    ++\t\tadvise(_(\"enabling extensions.submodulePathConfig might fix the \"\n    ++\t\t\t \"following error, if it's not already enabled.\"));\n    + \t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n    +-\t\t      \"git dir\"), buf->buf);\n    ++\t\t      \" git dir.\"), buf->buf);\n    ++\t}\n      }\n     \n      ## t/lib-verify-submodule-gitdir-path.sh (new) ##\n    @@ t/lib-verify-submodule-gitdir-path.sh (new)\n     +# might or might not exist (e.g. when adding a new submodule), so this only\n     +# checks the expected configuration path, which might be overridden by the user.\n     +\n    -+verify_submodule_gitdir_path() {\n    ++verify_submodule_gitdir_path () {\n     +\trepo=\"$1\" &&\n     +\tname=\"$2\" &&\n     +\tpath=\"$3\" &&\n    @@ t/t7425-submodule-gitdir-path-extension.sh (new)\n     +'\n     +\n     +test_expect_success 'verify new submodule gitdir config' '\n    -+\tgit -C main config submodule.\"New Sub\".gitdir > actual &&\n    -+\techo \".git/modules/New Sub\" > expect &&\n    ++\tgit -C main config submodule.\"New Sub\".gitdir >actual &&\n    ++\techo \".git/modules/New Sub\" >expect &&\n     +\ttest_cmp expect actual &&\n     +\tverify_submodule_gitdir_path main \"New Sub\" \"modules/New Sub\"\n     +'\n    @@ t/t7425-submodule-gitdir-path-extension.sh (new)\n     +\t# was added before the extension was enabled. Add and test it.\n     +\ttest_must_fail git -C main config submodule.legacy.gitdir &&\n     +\tgit -C main config submodule.legacy.gitdir .git/modules/legacy &&\n    -+\tgit -C main config submodule.legacy.gitdir > actual &&\n    -+\techo \".git/modules/legacy\" > expect &&\n    ++\tgit -C main config submodule.legacy.gitdir >actual &&\n    ++\techo \".git/modules/legacy\" >expect &&\n     +\ttest_cmp expect actual &&\n     +\tverify_submodule_gitdir_path main \"legacy\" \"modules/legacy\"\n     +'\n     +\n    ++test_expect_success 'gitdir config path is relative for both absolute and relative urls' '\n    ++\ttest_when_finished \"rm -rf relative-cfg-path-test\" &&\n    ++\tgit init -b main relative-cfg-path-test &&\n    ++\t(\n    ++\t\tcd relative-cfg-path-test &&\n    ++\t\tgit config core.repositoryformatversion 1 &&\n    ++\t\tgit config extensions.submodulePathConfig true &&\n    ++\n    ++\t\t# Test with absolute URL\n    ++\t\tgit submodule add \"$TRASH_DIRECTORY/new-sub\" sub-abs &&\n    ++\t\tgit config submodule.sub-abs.gitdir >actual &&\n    ++\t\techo \".git/modules/sub-abs\" >expect &&\n    ++\t\ttest_cmp expect actual &&\n    ++\n    ++\t\t# Test with relative URL\n    ++\t\tgit submodule add ../new-sub sub-rel &&\n    ++\t\tgit config submodule.sub-rel.gitdir >actual &&\n    ++\t\techo \".git/modules/sub-rel\" >expect &&\n    ++\t\ttest_cmp expect actual\n    ++\t)\n    ++'\n    ++\n     +test_expect_success 'clone from repo with both legacy and new-style submodules' '\n     +\tgit clone --recurse-submodules main cloned-non-extension &&\n     +\t(\n 5:  8017e1ca16 !  5:  3a65c86a38 submodule: allow runtime enabling extensions.submodulePathConfig\n    @@ Metadata\n      ## Commit message ##\n         submodule: allow runtime enabling extensions.submodulePathConfig\n     \n    -    Add a new config `init.autoSetupSubmodulePathConfig` which allows\n    +    Add a new config `init.defaultSubmodulePathConfig` which allows\n         enabling `extensions.submodulePathConfig` for new submodules by\n         default (those created via git init or clone).\n     \n    -    Important: setting init.autoSetupSubmodulePathConfig = true does\n    +    Important: setting init.defaultSubmodulePathConfig = true does\n         not globally enable `extensions.submodulePathConfig`. Existing\n         repositories will still have the extension disabled and will\n         require migration (for example via git submodule--helper command\n    @@ Documentation/config/extensions.adoc: Git will error out if a module does not ha\n      `git config submodule.<name>.gitdir .git/modules/<name>`.\n     ++\n     +The extension can be enabled automatically for new repositories by setting\n    -+`init.autoSetupSubmodulePathConfig` to `true`, for example by running\n    -+`git config --global init.autoSetupSubmodulePathConfig true`.\n    ++`init.defaultSubmodulePathConfig` to `true`, for example by running\n    ++`git config --global init.defaultSubmodulePathConfig true`.\n      \n      worktreeConfig:::\n      \tIf enabled, then worktrees will load config settings from the\n    @@ Documentation/config/init.adoc: endif::[]\n      \toption and the `GIT_DEFAULT_REF_FORMAT` environment variable take\n      \tprecedence over this config.\n     +\n    -+init.autoSetupSubmodulePathConfig::\n    ++init.defaultSubmodulePathConfig::\n     +\tA boolean that specifies if `git init` and `git clone` should\n     +\tautomatically set `extensions.submodulePathConfig` to `true`. This\n     +\tallows all new repositories to automatically use the submodule path\n     +\textension. Defaults to `false` when unset.\n     \n      ## setup.c ##\n    -@@ setup.c: int init_db(const char *git_dir, const char *real_git_dir,\n    - \t    const char *initial_branch,\n    - \t    int init_shared_repository, unsigned int flags)\n    +@@ setup.c: void initialize_repository_version(int hash_algo,\n      {\n    --\tint reinit;\n    -+\tint reinit, auto_setup_submodule_path_config = 0;\n    - \tint exist_ok = flags & INIT_DB_EXIST_OK;\n    - \tchar *original_git_dir = real_pathdup(git_dir, 1);\n    - \tstruct repository_format repo_fmt = REPOSITORY_FORMAT_INIT;\n    -@@ setup.c: int init_db(const char *git_dir, const char *real_git_dir,\n    - \t\t\t\t\t  initial_branch, flags & INIT_DB_QUIET);\n    - \tcreate_object_directory();\n    + \tstruct strbuf repo_version = STRBUF_INIT;\n    + \tint target_version = GIT_REPO_VERSION;\n    ++\tint default_submodule_path_config = 0;\n      \n    -+\trepo_config_get_bool(the_repository, \"init.autoSetupSubmodulePathConfig\",\n    -+\t\t\t     &auto_setup_submodule_path_config);\n    -+\tif (auto_setup_submodule_path_config) {\n    -+\t\tint version = 0;\n    -+\t\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &version);\n    -+\t\tif (version < 1)\n    -+\t\t\trepo_config_set(the_repository, \"core.repositoryformatversion\", \"1\");\n    + \t/*\n    + \t * Note that we initialize the repository version to 1 when the ref\n    +@@ setup.c: void initialize_repository_version(int hash_algo,\n    + \t\tclear_repository_format(&repo_fmt);\n    + \t}\n    + \n    ++\trepo_config_get_bool(the_repository, \"init.defaultSubmodulePathConfig\",\n    ++\t\t\t     &default_submodule_path_config);\n    ++\tif (default_submodule_path_config) {\n    ++\t\t/* extensions.submodulepathconfig requires at least version 1 */\n    ++\t\tif (target_version == 0)\n    ++\t\t\ttarget_version = 1;\n     +\t\trepo_config_set(the_repository, \"extensions.submodulepathconfig\", \"true\");\n     +\t}\n     +\n    - \tif (repo_settings_get_shared_repository(the_repository)) {\n    - \t\tchar buf[10];\n    - \t\t/* We do not spell \"group\" and such, so that\n    + \tstrbuf_addf(&repo_version, \"%d\", target_version);\n    + \trepo_config_set(the_repository, \"core.repositoryformatversion\", repo_version.buf);\n    + \n     \n      ## t/t7425-submodule-gitdir-path-extension.sh ##\n     @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed submodule changes and verify updates' '\n      \t)\n      '\n      \n    -+test_expect_success '`git init` respects init.autoSetupSubmodulePathConfig' '\n    -+\tgit config --global init.autoSetupSubmodulePathConfig true &&\n    ++test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n    ++\tgit config --global init.defaultSubmodulePathConfig true &&\n     +\tgit init repo-init &&\n     +\tgit -C repo-init config extensions.submodulePathConfig > actual &&\n     +\techo true > expect &&\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed sub\n     +\t\techo \".git/modules/sub\" > expect &&\n     +\t\ttest_cmp expect actual\n     +\t) &&\n    -+\tgit config --global --unset init.autoSetupSubmodulePathConfig\n    ++\tgit config --global --unset init.defaultSubmodulePathConfig\n     +'\n     +\n     +test_expect_success '`git init` does not set extension by default' '\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed sub\n     +\n     +'\n     +\n    -+test_expect_success '`git clone` respects init.autoSetupSubmodulePathConfig' '\n    ++test_expect_success '`git clone` respects init.defaultSubmodulePathConfig' '\n     +\ttest_when_finished \"rm -rf repo-clone\" &&\n    -+\tgit config --global init.autoSetupSubmodulePathConfig true &&\n    ++\tgit config --global init.defaultSubmodulePathConfig true &&\n     +\tgit clone upstream repo-clone &&\n     +\t(\n     +\t\tcd repo-clone &&\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed sub\n     +\t\techo \".git/modules/sub\" > expect &&\n     +\t\ttest_cmp expect actual\n     +\t) &&\n    -+\tgit config --global --unset init.autoSetupSubmodulePathConfig\n    ++\tgit config --global --unset init.defaultSubmodulePathConfig\n     +'\n     +\n    -+test_expect_success '`git clone --recurse-submodules` respects init.autoSetupSubmodulePathConfig' '\n    ++test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmodulePathConfig' '\n     +\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n    -+\tgit config --global init.autoSetupSubmodulePathConfig true &&\n    ++\tgit config --global init.defaultSubmodulePathConfig true &&\n     +\tgit clone  --recurse-submodules upstream repo-clone-recursive &&\n     +\t(\n     +\t\tcd repo-clone-recursive &&\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed sub\n     +\t\techo \".git/modules/new-sub\" > expect &&\n     +\t\ttest_cmp expect actual\n     +\t) &&\n    -+\tgit config --global --unset init.autoSetupSubmodulePathConfig\n    ++\tgit config --global --unset init.defaultSubmodulePathConfig\n     +'\n     +\n      test_done\n 6:  ce96f85456 !  6:  c62db6b32f submodule--helper: add gitdir migration command\n    @@ Documentation/config/extensions.adoc: Git will error out if a module does not ha\n     +submodules and attempts to migrate them.\n      +\n      The extension can be enabled automatically for new repositories by setting\n    - `init.autoSetupSubmodulePathConfig` to `true`, for example by running\n    + `init.defaultSubmodulePathConfig` to `true`, for example by running\n     \n      ## builtin/submodule--helper.c ##\n     @@ builtin/submodule--helper.c: static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n    @@ builtin/submodule--helper.c: static int module_gitdir(int argc, const char **arg\n     +\tstruct strbuf module_dir = STRBUF_INIT;\n     +\tDIR *dir;\n     +\tstruct dirent *de;\n    ++\tint repo_version = 0;\n     +\n     +\trepo_git_path_append(repo, &module_dir, \"modules/\");\n     +\n    @@ builtin/submodule--helper.c: static int module_gitdir(int argc, const char **arg\n     +\tclosedir(dir);\n     +\tstrbuf_release(&module_dir);\n     +\n    -+\tif (repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n    ++\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &repo_version);\n    ++\tif (repo_version == 0 &&\n    ++\t    repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n     +\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n    -+\t\t      \"Please enable it for migration to work, for example: \"\n    ++\t\t      \"Please set it for migration to work, for example: \"\n     +\t\t      \"git config core.repositoryformatversion 1\"));\n     +\n     +\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n    @@ builtin/submodule--helper.c: int cmd_submodule__helper(int argc,\n      \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n     \n      ## t/t7425-submodule-gitdir-path-extension.sh ##\n    -@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` respects init.autoSetupSub\n    - \tgit config --global --unset init.autoSetupSubmodulePathConfig\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed submodule changes and verify updates' '\n    + test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n    + \tgit config --global init.defaultSubmodulePathConfig true &&\n    + \tgit init repo-init &&\n    +-\tgit -C repo-init config extensions.submodulePathConfig > actual &&\n    +-\techo true > expect &&\n    ++\tgit -C repo-init config extensions.submodulePathConfig >actual &&\n    ++\techo true >expect &&\n    + \ttest_cmp expect actual &&\n    + \t# create a submodule and check gitdir\n    + \t(\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n    + \t\tgit init -b main sub &&\n    + \t\ttest_commit -C sub sub-initial &&\n    + \t\tgit submodule add ./sub sub &&\n    +-\t\tgit config submodule.sub.gitdir > actual &&\n    +-\t\techo \".git/modules/sub\" > expect &&\n    ++\t\tgit config submodule.sub.gitdir >actual &&\n    ++\t\techo \".git/modules/sub\" >expect &&\n    + \t\ttest_cmp expect actual\n    + \t) &&\n    + \tgit config --global --unset init.defaultSubmodulePathConfig\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone` respects init.defaultSubmodulePathConfig' '\n    + \t\tcd repo-clone &&\n    + \n    + \t\t# verify new repo extension is inherited from global config\n    +-\t\tgit config extensions.submodulePathConfig > actual &&\n    +-\t\techo true > expect &&\n    ++\t\tgit config extensions.submodulePathConfig >actual &&\n    ++\t\techo true >expect &&\n    + \t\ttest_cmp expect actual &&\n    + \n    + \t\t# new submodule has a gitdir config\n    + \t\tgit submodule add ../sub sub &&\n    + \t\ttest_path_is_dir .git/modules/sub &&\n    +-\t\tgit config submodule.sub.gitdir > actual &&\n    +-\t\techo \".git/modules/sub\" > expect &&\n    ++\t\tgit config submodule.sub.gitdir >actual &&\n    ++\t\techo \".git/modules/sub\" >expect &&\n    + \t\ttest_cmp expect actual\n    + \t) &&\n    + \tgit config --global --unset init.defaultSubmodulePathConfig\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmo\n    + \t\tcd repo-clone-recursive &&\n    + \n    + \t\t# verify new repo extension is inherited from global config\n    +-\t\tgit config extensions.submodulePathConfig > actual &&\n    +-\t\techo true > expect &&\n    ++\t\tgit config extensions.submodulePathConfig >actual &&\n    ++\t\techo true >expect &&\n    + \t\ttest_cmp expect actual &&\n    + \n    + \t\t# previous submodules should exist\n    +@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmo\n    + \t\t# create another submodule and check that gitdir is created\n    + \t\tgit submodule add ../sub new-sub &&\n    + \t\ttest_path_is_dir .git/modules/new-sub &&\n    +-\t\tgit config submodule.new-sub.gitdir > actual &&\n    +-\t\techo \".git/modules/new-sub\" > expect &&\n    ++\t\tgit config submodule.new-sub.gitdir >actual &&\n    ++\t\techo \".git/modules/new-sub\" >expect &&\n    + \t\ttest_cmp expect actual\n    + \t) &&\n    + \tgit config --global --unset init.defaultSubmodulePathConfig\n      '\n      \n     +test_expect_success 'submodule--helper migrates legacy modules' '\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --re\n     +\t\ttest_cmp expect actual &&\n     +\n     +\t\t# repository extension is enabled after migration\n    -+\t\tgit config extensions.submodulePathConfig > actual &&\n    -+\t\techo \"true\" > expect &&\n    ++\t\tgit config extensions.submodulePathConfig >actual &&\n    ++\t\techo \"true\" >expect &&\n     +\t\ttest_cmp expect actual\n     +\t)\n     +'\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --re\n     +\t(\n     +\t\tcd repo-clone-recursive &&\n     +\n    -+\t\t# init.autoSetupSubmodulePathConfig was disabled before clone, so\n    ++\t\t# init.defaultSubmodulePathConfig was disabled before clone, so\n     +\t\t# the repo extension config should also be off, the migration ignored\n     +\t\ttest_must_fail git config extensions.submodulePathConfig &&\n     +\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --re\n     +\trm -rf repo-clone-recursive &&\n     +\n     +\t# enable the extension, then retry the clone\n    -+\tgit config --global init.autoSetupSubmodulePathConfig true &&\n    ++\tgit config --global init.defaultSubmodulePathConfig true &&\n     +\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n     +\t(\n     +\t\tcd repo-clone-recursive &&\n     +\n     +\t\t# repository extension is enabled\n    -+\t\tgit config extensions.submodulePathConfig > actual &&\n    -+\t\techo \"true\" > expect &&\n    ++\t\tgit config extensions.submodulePathConfig >actual &&\n    ++\t\techo \"true\" >expect &&\n     +\t\ttest_cmp expect actual &&\n     +\n     +\t\t# gitdir configs exist for submodules\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --re\n     +\t\tgit config submodule.sub2.gitdir &&\n     +\t\ttest_path_is_dir .git/modules/sub1 &&\n     +\t\ttest_path_is_dir .git/modules/sub2\n    -+\t)\n    ++\t) &&\n    ++\tgit config --global --unset init.defaultSubmodulePathConfig\n     +'\n     +\n      test_done\n 7:  2f96ff248f =  7:  c554017f83 builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n 8:  5aa986bd40 !  8:  7a794b9b61 submodule--helper: fix filesystem collisions by encoding gitdir paths\n    @@ builtin/submodule--helper.c: static void create_default_gitdir_config(const char\n     \n      ## submodule.c ##\n     @@\n    - #include \"commit-reach.h\"\n      #include \"read-cache-ll.h\"\n      #include \"setup.h\"\n    + #include \"advice.h\"\n     +#include \"url.h\"\n      \n      static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n    @@ submodule.c: int validate_submodule_git_dir(char *git_dir, const char *submodule\n     \n      ## t/t7425-submodule-gitdir-path-extension.sh ##\n     @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` works after migration' '\n    - \t)\n    + \tgit config --global --unset init.defaultSubmodulePathConfig\n      '\n      \n     +test_expect_success 'setup submodules with nested git dirs' '\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --re\n     +\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules nested clone_nested &&\n     +\n     +\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n    -+\tgit -C clone_nested config submodule.hippo.gitdir > actual &&\n    ++\tgit -C clone_nested config submodule.hippo.gitdir >actual &&\n     +\ttest_grep \"\\.git/modules/hippo$\" actual &&\n     +\n     +\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks &&\n    -+\tgit -C clone_nested config submodule.hippo/hooks.gitdir > actual &&\n    ++\tgit -C clone_nested config submodule.hippo/hooks.gitdir >actual &&\n     +\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n     +'\n     +\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --re\n     +\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules --jobs=2 nested clone_parallel &&\n     +\n     +\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n    -+\tgit -C clone_nested config submodule.hippo.gitdir > actual &&\n    ++\tgit -C clone_nested config submodule.hippo.gitdir >actual &&\n     +\ttest_grep \"\\.git/modules/hippo$\" actual &&\n     +\n     +\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks &&\n    -+\tgit -C clone_nested config submodule.hippo/hooks.gitdir > actual &&\n    ++\tgit -C clone_nested config submodule.hippo/hooks.gitdir >actual &&\n     +\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n     +'\n     +\n 9:  044d7de8a0 =  9:  142a85a1af submodule: fix case-folding gitdir filesystem collisions\n10:  90d78b23cd = 10:  bafde20354 submodule: hash the submodule name for the gitdir path\n11:  4e596a2a90 ! 11:  e3fe1f7529 submodule: detect conflicts with existing gitdir configs\n    @@ submodule.c: static int check_casefolding_conflict(const char *git_dir,\n     +\n     +\treturn cb->conflict_found;\n     +}\n    ++\n     +static bool submodule_conflicts_with_existing(const char *gitdir, const char *submodule_name)\n     +{\n     +\tstruct submodule_from_gitdir_cb cb = { 0 };\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success CASE_INSENSITIVE\n     +\t(\n     +\t\tcd super_with_encoded &&\n     +\n    ++\t\tgit config core.repositoryformatversion 1 &&\n     +\t\tgit config extensions.submodulePathConfig true &&\n     +\n     +\t\t# Add a submodule with a nested path\n\nAdrian Ratiu (11):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  submodule: always validate gitdirs inside submodule_name_to_gitdir\n  builtin/submodule--helper: add gitdir command\n  submodule: introduce extensions.submodulePathConfig\n  submodule: allow runtime enabling extensions.submodulePathConfig\n  submodule--helper: add gitdir migration command\n  builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n  submodule--helper: fix filesystem collisions by encoding gitdir paths\n  submodule: fix case-folding gitdir filesystem collisions\n  submodule: hash the submodule name for the gitdir path\n  submodule: detect conflicts with existing gitdir configs\n\n Documentation/config/extensions.adoc       |  29 ++\n Documentation/config/init.adoc             |   6 +\n Documentation/config/submodule.adoc        |   7 +\n builtin/credential-store.c                 |   7 +-\n builtin/submodule--helper.c                | 204 +++++++-\n repository.c                               |   1 +\n repository.h                               |   1 +\n setup.c                                    |  17 +\n setup.h                                    |   1 +\n submodule.c                                | 223 +++++++--\n t/lib-verify-submodule-gitdir-path.sh      |  24 +\n t/meson.build                              |   1 +\n t/t7425-submodule-gitdir-path-extension.sh | 532 +++++++++++++++++++++\n t/t9902-completion.sh                      |   1 +\n url.c                                      |  13 +\n url.h                                      |  14 +\n 16 files changed, 1030 insertions(+), 51 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-gitdir-path-extension.sh\n\n-- \n2.51.2\n\n"},{"id":"533244","messageId":"20260107230145.517562-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 01/11] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:35Z","receivedAt":"2026-01-07T23:02:54Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded modules gitdir path leading to test failures.\n\nCall the submodule_name_to_gitdir() helper instead, which was invented\nexactly for this purpose and is already used by all the other locations\nwhich work on gitdirs.\n\nAlso narrow the scope of the submod_gitdir_path variable which is not\nused anymore in the updated \"else\" branch.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 13 +++++++------\n 1 file changed, 7 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex d537ab087a..6686714e56 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3190,13 +3190,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n \n static int add_submodule(const struct add_data *add_data)\n {\n-\tchar *submod_gitdir_path;\n \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n \tint ret = -1;\n \n \t/* perhaps the path already exists and is already a git repo, else clone it */\n \tif (is_directory(add_data->sm_path)) {\n+\t\tchar *submod_gitdir_path;\n \t\tstruct strbuf sm_path = STRBUF_INIT;\n \t\tstrbuf_addstr(&sm_path, add_data->sm_path);\n \t\tsubmod_gitdir_path = xstrfmt(\"%s/.git\", add_data->sm_path);\n@@ -3210,10 +3210,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3222,8 +3223,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3241,7 +3242,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.51.2\n\n"},{"id":"533246","messageId":"20260107230145.517562-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 02/11] submodule: always validate gitdirs inside submodule_name_to_gitdir","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:36Z","receivedAt":"2026-01-07T23:02:56Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Move the ad-hoc validation checks sprinkled across the source tree,\nafter calling submodule_name_to_gitdir() into the function proper,\nwhich now always validates the gitdir before returning it.\n\nThis simplifies the API and helps to:\n1. Avoid redundant validation calls after submodule_name_to_gitdir().\n2. Avoid the risk of callers forgetting to validate.\n3. Ensure gitdir paths provided by users via configs are always valid\n   (config gitdir paths are added in a subsequent commit).\n\nThe validation function can still be called as many times as needed\noutside submodule_name_to_gitdir(), for example we keep two calls\nwhich are still required, to avoid parallel clone races by re-running\nthe validation in builtin/submodule-helper.c.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c |  4 ----\n submodule.c                 | 12 ++++--------\n 2 files changed, 4 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 6686714e56..aff3f9135e 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1699,10 +1699,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n \t\t\t\t\t\t    clone_data->path);\n \n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\n \tif (!file_exists(sm_gitdir)) {\n \t\tif (clone_data->require_init && !stat(clone_data_path, &st) &&\n \t\t    !is_empty_dir(clone_data_path))\ndiff --git a/submodule.c b/submodule.c\nindex 40a5c6fb9d..f645372a18 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2166,11 +2166,6 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n \t\t\t\t\t\t sub->name);\n-\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n-\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n-\t\t\t\t      \"submodule's git dir\"),\n-\t\t\t\t    gitdir.buf);\n \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n \t\t\tstrbuf_release(&gitdir);\n \n@@ -2349,9 +2344,6 @@ static void relocate_single_git_dir_into_superproject(const char *path,\n \t\tdie(_(\"could not lookup name for submodule '%s'\"), path);\n \n \tsubmodule_name_to_gitdir(&new_gitdir, the_repository, sub->name);\n-\tif (validate_submodule_git_dir(new_gitdir.buf, sub->name) < 0)\n-\t\tdie(_(\"refusing to move '%s' into an existing git dir\"),\n-\t\t    real_old_git_dir);\n \tif (safe_create_leading_directories_const(the_repository, new_gitdir.buf) < 0)\n \t\tdie(_(\"could not create directory '%s'\"), new_gitdir.buf);\n \treal_new_git_dir = real_pathdup(new_gitdir.buf, 1);\n@@ -2600,4 +2592,8 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t */\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n+\n+\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n+\t\t      \"git dir\"), buf->buf);\n }\n-- \n2.51.2\n\n"},{"id":"533247","messageId":"20260107230145.517562-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 03/11] builtin/submodule--helper: add gitdir command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:37Z","receivedAt":"2026-01-07T23:03:04Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This exposes the gitdir name computed by submodule_name_to_gitdir()\ninternally, to make it easier for users and tests to interact with it.\n\nNext commit will add a gitdir configuration, so this helper can also be\nused to easily query that config or validate any gitdir path the user\nsets (submodule_name_to_git_dir now runs the validation logic, since\nour previous commit).\n\nBased-on-patch-by: Brandon Williams <bwilliams.eng@gmail.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex aff3f9135e..901213ee71 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1204,6 +1204,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3591,6 +3607,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\n-- \n2.51.2\n\n"},{"id":"533248","messageId":"20260107230145.517562-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 04/11] submodule: introduce extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:38Z","receivedAt":"2026-01-07T23:03:06Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"The idea of this extension is to abstract away the submodule gitdir\npath implementation: everyone is expected to use the config and not\nworry about how the path is computed internally, either in git or\nother implementations.\n\nWith this extension enabled, the submodule.<name>.gitdir repo config\nbecomes the single source of truth for all submodule gitdir paths.\n\nThe submodule.<name>.gitdir config is added automatically for all new\nsubmodules when this extension is enabled.\n\nGit will throw an error if the extension is enabled and a config is\nmissing, advising users how to migrate. Migration is manual for now.\n\nE.g. to add a missing config entry for an existing \"foo\" module:\ngit config submodule.foo.gitdir .git/modules/foo\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |  23 +++\n Documentation/config/submodule.adoc        |   7 +\n builtin/submodule--helper.c                |  54 ++++++-\n repository.c                               |   1 +\n repository.h                               |   1 +\n setup.c                                    |   7 +\n setup.h                                    |   1 +\n submodule.c                                |  61 ++++----\n t/lib-verify-submodule-gitdir-path.sh      |  24 ++++\n t/meson.build                              |   1 +\n t/t7425-submodule-gitdir-path-extension.sh | 160 +++++++++++++++++++++\n t/t9902-completion.sh                      |   1 +\n 12 files changed, 313 insertions(+), 28 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-gitdir-path-extension.sh\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 532456644b..f4f57c9114 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -73,6 +73,29 @@ relativeWorktrees:::\n \trepaired with either the `--relative-paths` option or with the\n \t`worktree.useRelativePaths` config set to `true`.\n \n+submodulePathConfig:::\n+\tThis extension is for the minority of users who:\n++\n+--\n+* Encounter errors like `refusing to create ... in another submodule's git dir`\n+  due to a number of reasons, like case-insensitive filesystem conflicts when\n+  creating modules named `foo` and `Foo`.\n+* Require more flexible submodule layouts, for example due to nested names like\n+  `foo`, `foo/bar` and `foo/baz` not supported by the default gitdir mechanism\n+  which uses `.git/modules/<plain-name>` locations, causing further conflicts.\n+--\n++\n+When `extensions.submodulePathConfig` is enabled, the `submodule.<name>.gitdir`\n+config becomes the single source of truth for all submodule gitdir paths and is\n+automatically set for all new submodules both during clone and init operations.\n++\n+Git will error out if a module does not have a corresponding\n+`submodule.<name>.gitdir` set.\n++\n+Existing (pre-extension) submodules need to be migrated by adding the missing\n+config entries. This is done manually for now, e.g. for each submodule:\n+`git config submodule.<name>.gitdir .git/modules/<name>`.\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/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 0672d99117..74f1659a91 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -52,6 +52,13 @@ submodule.<name>.active::\n \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n \tdetails.\n \n+submodule.<name>.gitdir::\n+\tThis sets the gitdir path for submodule <name>. This configuration is\n+\trespected when `extensions.submodulePathConfig` is enabled, otherwise it\n+\thas no effect. When enabled, this config becomes the single source of\n+\ttruth for submodule gitdir paths and Git will error if it is missing.\n+\tSee linkgit:git-config[1] for details.\n+\n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\n \tsubmodule's path to determine if the submodule is of interest to git\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 901213ee71..b3d6f9ff68 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -435,6 +435,48 @@ struct init_cb {\n };\n #define INIT_CB_INIT { 0 }\n \n+static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n+\t\t\t\t\t     const char *submodule_name)\n+{\n+\tconst char *value;\n+\tchar *key;\n+\n+\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n+\t\treturn -1;\n+\n+\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\n+\t/* Nothing to do if the config already exists. */\n+\tif (!repo_config_get_string_tmp(the_repository, key, &value)) {\n+\t\tfree(key);\n+\t\treturn 0;\n+\t}\n+\n+\tif (repo_config_set_gently(the_repository, key, gitdir_path->buf)) {\n+\t\tfree(key);\n+\t\treturn -1;\n+\t}\n+\n+\tfree(key);\n+\treturn 0;\n+}\n+\n+static void create_default_gitdir_config(const char *submodule_name)\n+{\n+\tstruct strbuf gitdir_path = STRBUF_INIT;\n+\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n+\tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n+\t      \"Please ensure it is set, for example by running something like: \"\n+\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n+\t    submodule_name, submodule_name, submodule_name);\n+}\n+\n static void init_submodule(const char *path, const char *prefix,\n \t\t\t   const char *super_prefix,\n \t\t\t   unsigned int flags)\n@@ -511,6 +553,10 @@ static void init_submodule(const char *path, const char *prefix,\n \t\tif (repo_config_set_gently(the_repository, sb.buf, upd))\n \t\t\tdie(_(\"Failed to register update mode for submodule path '%s'\"), displaypath);\n \t}\n+\n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tcreate_default_gitdir_config(sub->name);\n+\n \tstrbuf_release(&sb);\n \tfree(displaypath);\n \tfree(url);\n@@ -1801,8 +1847,9 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n \t\tunlink(head);\n \t\tfree(head);\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n+\t\tdie(_(\"refusing to create/use '%s' in another submodule's git dir. \"\n+\t\t      \"Enabling extensions.submodulePathConfig should fix this.\"),\n+\t\t    sm_gitdir);\n \t}\n \n \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n@@ -3582,6 +3629,9 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \tadd_data.progress = !!progress;\n \tadd_data.dissociate = !!dissociate;\n \n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tcreate_default_gitdir_config(add_data.sm_name);\n+\n \tif (add_submodule(&add_data))\n \t\tgoto cleanup;\n \tconfigure_added_submodule(&add_data);\ndiff --git a/repository.c b/repository.c\nindex c7e75215ac..46a7c99930 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -281,6 +281,7 @@ int repo_init(struct repository *repo,\n \trepo->repository_format_worktree_config = format.worktree_config;\n \trepo->repository_format_relative_worktrees = format.relative_worktrees;\n \trepo->repository_format_precious_objects = format.precious_objects;\n+\trepo->repository_format_submodule_path_cfg = format.submodule_path_cfg;\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 6063c4b846..7141237f97 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -165,6 +165,7 @@ struct repository {\n \tint repository_format_worktree_config;\n \tint repository_format_relative_worktrees;\n \tint repository_format_precious_objects;\n+\tint repository_format_submodule_path_cfg;\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 3a6a048620..428427d689 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -686,6 +686,9 @@ static enum extension_result handle_extension(const char *var,\n \t} else if (!strcmp(ext, \"relativeworktrees\")) {\n \t\tdata->relative_worktrees = git_config_bool(var, value);\n \t\treturn EXTENSION_OK;\n+\t} else if (!strcmp(ext, \"submodulepathconfig\")) {\n+\t\tdata->submodule_path_cfg = git_config_bool(var, value);\n+\t\treturn EXTENSION_OK;\n \t}\n \treturn EXTENSION_UNKNOWN;\n }\n@@ -1947,6 +1950,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\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\tthe_repository->repository_format_submodule_path_cfg =\n+\t\t\t\trepo_fmt.submodule_path_cfg;\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@@ -2045,6 +2050,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_submodule_path_cfg =\n+\t\tfmt->submodule_path_cfg;\n \tthe_repository->repository_format_relative_worktrees =\n \t\tfmt->relative_worktrees;\n \tthe_repository->repository_format_partial_clone =\ndiff --git a/setup.h b/setup.h\nindex d55dcc6608..0738dec244 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -167,6 +167,7 @@ struct repository_format {\n \tchar *partial_clone; /* value of extensions.partialclone */\n \tint worktree_config;\n \tint relative_worktrees;\n+\tint submodule_path_cfg;\n \tint is_bare;\n \tint hash_algo;\n \tint compat_hash_algo;\ndiff --git a/submodule.c b/submodule.c\nindex f645372a18..e0cd6f5dcc 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -31,6 +31,7 @@\n #include \"commit-reach.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"advice.h\"\n \n static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n static int initialized_fetch_ref_tips;\n@@ -2158,8 +2159,9 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\t\tif (validate_submodule_git_dir(git_dir,\n \t\t\t\t\t\t\t       sub->name) < 0)\n \t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n-\t\t\t\t\t      \"another submodule's git dir\"),\n-\t\t\t\t\t    git_dir);\n+\t\t\t\t\t      \"another submodule's git dir. \"\n+\t\t\t\t\t      \"Enabling extensions.submodulePathConfig \"\n+\t\t\t\t\t      \"should fix this.\"), git_dir);\n \t\t\t\tfree(git_dir);\n \t\t\t}\n \t\t} else {\n@@ -2570,30 +2572,37 @@ int submodule_to_gitdir(struct repository *repo,\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n-\t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n-\t */\n-\trepo_git_path_append(r, buf, \"modules/\");\n-\tstrbuf_addstr(buf, submodule_name);\n+\tif (!r->repository_format_submodule_path_cfg) {\n+\t\t/*\n+\t\t * If extensions.submodulePathConfig is disabled,\n+\t\t * continue to use the plain path.\n+\t\t */\n+\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n+\t} else {\n+\t\tconst char *gitdir;\n+\t\tchar *key;\n+\t\tint ret;\n \n-\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\t/* Otherwise the extension is enabled, so use the gitdir config. */\n+\t\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\t\tret = repo_config_get_string_tmp(r, key, &gitdir);\n+\t\tFREE_AND_NULL(key);\n+\n+\t\tif (ret)\n+\t\t\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n+\t\t\t      \"Please ensure it is set, for example by running something like: \"\n+\t\t\t      \"'git config submodule.%s.gitdir .git/modules/%s'. For details \"\n+\t\t\t      \"see the extensions.submodulePathConfig documentation.\"),\n+\t\t\t    submodule_name, submodule_name, submodule_name, submodule_name);\n+\n+\t\tstrbuf_addstr(buf, gitdir);\n+\t}\n+\n+\t/* validate because users might have modified the config */\n+\tif (validate_submodule_git_dir(buf->buf, submodule_name)) {\n+\t\tadvise(_(\"enabling extensions.submodulePathConfig might fix the \"\n+\t\t\t \"following error, if it's not already enabled.\"));\n \t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), buf->buf);\n+\t\t      \" git dir.\"), buf->buf);\n+\t}\n }\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..4e0cfdc605\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,24 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n+\n+# This does not check filesystem existence. That is done in submodule.c via the\n+# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n+# might or might not exist (e.g. when adding a new submodule), so this only\n+# checks the expected configuration path, which might be overridden by the user.\n+\n+verify_submodule_gitdir_path () {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\t# Compute expected absolute path\n+\t\texpected=\"$(git rev-parse --git-common-dir)/$path\" &&\n+\t\texpected=\"$(test-tool path-utils real_path \"$expected\")\" &&\n+\t\t# Compute actual absolute path\n+\t\tactual=\"$(git submodule--helper gitdir \"$name\")\" &&\n+\t\tactual=\"$(test-tool path-utils real_path \"$actual\")\" &&\n+\t\techo \"$expected\" >expect &&\n+\t\techo \"$actual\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..0b1de3251a 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -887,6 +887,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-gitdir-path-extension.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nnew file mode 100755\nindex 0000000000..453183e27c\n--- /dev/null\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -0,0 +1,160 @@\n+#!/bin/sh\n+\n+test_description='submodulePathConfig extension works as expected'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+       git config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed extension submodules' '\n+\tgit init -b main legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init -b main new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init -b main main &&\n+\t(\n+\t\tcd main &&\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\t# trigger the \"die_path_inside_submodule\" check\n+\t\ttest_must_fail git submodule add ../new-sub \"legacy/nested\" &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\tgit submodule add ../new-sub \"New Sub\" &&\n+\t\ttest_commit new &&\n+\n+\t\t# retrigger the \"die_path_inside_submodule\" check with encoding\n+\t\ttest_must_fail git submodule add ../new-sub \"New Sub/nested2\"\n+       )\n+'\n+\n+test_expect_success 'verify new submodule gitdir config' '\n+\tgit -C main config submodule.\"New Sub\".gitdir >actual &&\n+\techo \".git/modules/New Sub\" >expect &&\n+\ttest_cmp expect actual &&\n+\tverify_submodule_gitdir_path main \"New Sub\" \"modules/New Sub\"\n+'\n+\n+test_expect_success 'manual add and verify legacy submodule gitdir config' '\n+\t# the legacy module should not contain a gitdir config, because it\n+\t# was added before the extension was enabled. Add and test it.\n+\ttest_must_fail git -C main config submodule.legacy.gitdir &&\n+\tgit -C main config submodule.legacy.gitdir .git/modules/legacy &&\n+\tgit -C main config submodule.legacy.gitdir >actual &&\n+\techo \".git/modules/legacy\" >expect &&\n+\ttest_cmp expect actual &&\n+\tverify_submodule_gitdir_path main \"legacy\" \"modules/legacy\"\n+'\n+\n+test_expect_success 'gitdir config path is relative for both absolute and relative urls' '\n+\ttest_when_finished \"rm -rf relative-cfg-path-test\" &&\n+\tgit init -b main relative-cfg-path-test &&\n+\t(\n+\t\tcd relative-cfg-path-test &&\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\t# Test with absolute URL\n+\t\tgit submodule add \"$TRASH_DIRECTORY/new-sub\" sub-abs &&\n+\t\tgit config submodule.sub-abs.gitdir >actual &&\n+\t\techo \".git/modules/sub-abs\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# Test with relative URL\n+\t\tgit submodule add ../new-sub sub-rel &&\n+\t\tgit config submodule.sub-rel.gitdir >actual &&\n+\t\techo \".git/modules/sub-rel\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned-non-extension &&\n+\t(\n+\t\tcd cloned-non-extension &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/\"New Sub\" &&\n+\n+\t\ttest_must_fail git config submodule.legacy.gitdir &&\n+\t\ttest_must_fail git config submodule.\"New Sub\".gitdir &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t) &&\n+\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules main cloned-extension &&\n+\t(\n+\t\tcd cloned-extension &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\tgit config submodule.legacy.gitdir &&\n+\t\tgit config submodule.\"New Sub\".gitdir &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to encoded submodules' '\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C main config receive.denyCurrentBranch updateInstead &&\n+\t(\n+\t\tcd cloned-extension &&\n+\n+\t\tgit -C legacy switch --track -C main origin/main  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C \"New Sub\" switch --track -C main origin/main &&\n+\t\ttest_commit -C \"New Sub\" second-commit &&\n+\t\tgit -C \"New Sub\" push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C main origin/main  &&\n+\t\tgit add legacy \"New Sub\" &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_done\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 964e1f1569..ffb9c8b522 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n \tsubmodule.sub.fetchRecurseSubmodules Z\n \tsubmodule.sub.ignore Z\n \tsubmodule.sub.active Z\n+\tsubmodule.sub.gitdir Z\n \tEOF\n '\n \n-- \n2.51.2\n\n"},{"id":"533249","messageId":"20260107230145.517562-6-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 05/11] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:39Z","receivedAt":"2026-01-07T23:03:08Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new config `init.defaultSubmodulePathConfig` which allows\nenabling `extensions.submodulePathConfig` for new submodules by\ndefault (those created via git init or clone).\n\nImportant: setting init.defaultSubmodulePathConfig = true does\nnot globally enable `extensions.submodulePathConfig`. Existing\nrepositories will still have the extension disabled and will\nrequire migration (for example via git submodule--helper command\nadded in the next commit).\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |   4 +\n Documentation/config/init.adoc             |   6 +\n setup.c                                    |  10 ++\n t/t7425-submodule-gitdir-path-extension.sh | 125 +++++++++++++++++++++\n 4 files changed, 145 insertions(+)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex f4f57c9114..e8d9d9a19a 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -95,6 +95,10 @@ Git will error out if a module does not have a corresponding\n Existing (pre-extension) submodules need to be migrated by adding the missing\n config entries. This is done manually for now, e.g. for each submodule:\n `git config submodule.<name>.gitdir .git/modules/<name>`.\n++\n+The extension can be enabled automatically for new repositories by setting\n+`init.defaultSubmodulePathConfig` to `true`, for example by running\n+`git config --global init.defaultSubmodulePathConfig true`.\n \n worktreeConfig:::\n \tIf enabled, then worktrees will load config settings from the\ndiff --git a/Documentation/config/init.adoc b/Documentation/config/init.adoc\nindex e45b2a8121..7b4abdaf8b 100644\n--- a/Documentation/config/init.adoc\n+++ b/Documentation/config/init.adoc\n@@ -18,3 +18,9 @@ endif::[]\n \tSee `--ref-format=` in linkgit:git-init[1]. Both the command line\n \toption and the `GIT_DEFAULT_REF_FORMAT` environment variable take\n \tprecedence over this config.\n+\n+init.defaultSubmodulePathConfig::\n+\tA boolean that specifies if `git init` and `git clone` should\n+\tautomatically set `extensions.submodulePathConfig` to `true`. This\n+\tallows all new repositories to automatically use the submodule path\n+\textension. Defaults to `false` when unset.\ndiff --git a/setup.c b/setup.c\nindex 428427d689..0378483b69 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2310,6 +2310,7 @@ void initialize_repository_version(int hash_algo,\n {\n \tstruct strbuf repo_version = STRBUF_INIT;\n \tint target_version = GIT_REPO_VERSION;\n+\tint default_submodule_path_config = 0;\n \n \t/*\n \t * Note that we initialize the repository version to 1 when the ref\n@@ -2348,6 +2349,15 @@ void initialize_repository_version(int hash_algo,\n \t\tclear_repository_format(&repo_fmt);\n \t}\n \n+\trepo_config_get_bool(the_repository, \"init.defaultSubmodulePathConfig\",\n+\t\t\t     &default_submodule_path_config);\n+\tif (default_submodule_path_config) {\n+\t\t/* extensions.submodulepathconfig requires at least version 1 */\n+\t\tif (target_version == 0)\n+\t\t\ttarget_version = 1;\n+\t\trepo_config_set(the_repository, \"extensions.submodulepathconfig\", \"true\");\n+\t}\n+\n \tstrbuf_addf(&repo_version, \"%d\", target_version);\n \trepo_config_set(the_repository, \"core.repositoryformatversion\", repo_version.buf);\n \ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 453183e27c..6cb844e809 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -157,4 +157,129 @@ test_expect_success 'fetch mixed submodule changes and verify updates' '\n \t)\n '\n \n+test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n+\tgit config --global init.defaultSubmodulePathConfig true &&\n+\tgit init repo-init &&\n+\tgit -C repo-init config extensions.submodulePathConfig > actual &&\n+\techo true > expect &&\n+\ttest_cmp expect actual &&\n+\t# create a submodule and check gitdir\n+\t(\n+\t\tcd repo-init &&\n+\t\tgit init -b main sub &&\n+\t\ttest_commit -C sub sub-initial &&\n+\t\tgit submodule add ./sub sub &&\n+\t\tgit config submodule.sub.gitdir > actual &&\n+\t\techo \".git/modules/sub\" > expect &&\n+\t\ttest_cmp expect actual\n+\t) &&\n+\tgit config --global --unset init.defaultSubmodulePathConfig\n+'\n+\n+test_expect_success '`git init` does not set extension by default' '\n+\tgit init upstream &&\n+\ttest_commit -C upstream initial &&\n+\ttest_must_fail git -C upstream config extensions.submodulePathConfig &&\n+\t# create a pair of submodules and check gitdir is not created\n+\tgit init -b main sub &&\n+\ttest_commit -C sub sub-initial &&\n+\t(\n+\t\tcd upstream &&\n+\t\tgit submodule add ../sub sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\tgit submodule add ../sub sub2 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\tgit commit -m \"Add submodules\"\n+\t)\n+'\n+\n+test_expect_success '`git clone` does not set extension by default' '\n+\ttest_when_finished \"rm -rf repo-clone-no-ext\" &&\n+\tgit clone upstream repo-clone-no-ext &&\n+\t(\n+\t\tcd repo-clone-no-ext &&\n+\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\t\ttest_path_is_missing .git/modules/sub1 &&\n+\t\ttest_path_is_missing .git/modules/sub2 &&\n+\n+\t\t# create a submodule and check gitdir is not created\n+\t\tgit submodule add ../sub sub3 &&\n+\t\ttest_must_fail git config submodule.sub3.gitdir\n+\t)\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` does not set extension by default' '\n+\ttest_when_finished \"rm -rf repo-clone-no-ext\" &&\n+\tgit clone --recurse-submodules upstream repo-clone-no-ext &&\n+\t(\n+\t\tcd repo-clone-no-ext &&\n+\n+\t\t# verify that that submodules do not have gitdir set\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\n+\t\t# create another submodule and check that gitdir is not created\n+\t\tgit submodule add ../sub sub3 &&\n+\t\ttest_path_is_dir .git/modules/sub3 &&\n+\t\ttest_must_fail git config submodule.sub3.gitdir\n+\t)\n+\n+'\n+\n+test_expect_success '`git clone` respects init.defaultSubmodulePathConfig' '\n+\ttest_when_finished \"rm -rf repo-clone\" &&\n+\tgit config --global init.defaultSubmodulePathConfig true &&\n+\tgit clone upstream repo-clone &&\n+\t(\n+\t\tcd repo-clone &&\n+\n+\t\t# verify new repo extension is inherited from global config\n+\t\tgit config extensions.submodulePathConfig > actual &&\n+\t\techo true > expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# new submodule has a gitdir config\n+\t\tgit submodule add ../sub sub &&\n+\t\ttest_path_is_dir .git/modules/sub &&\n+\t\tgit config submodule.sub.gitdir > actual &&\n+\t\techo \".git/modules/sub\" > expect &&\n+\t\ttest_cmp expect actual\n+\t) &&\n+\tgit config --global --unset init.defaultSubmodulePathConfig\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmodulePathConfig' '\n+\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n+\tgit config --global init.defaultSubmodulePathConfig true &&\n+\tgit clone  --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# verify new repo extension is inherited from global config\n+\t\tgit config extensions.submodulePathConfig > actual &&\n+\t\techo true > expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# previous submodules should exist\n+\t\tgit config submodule.sub1.gitdir &&\n+\t\tgit config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\n+\t\t# create another submodule and check that gitdir is created\n+\t\tgit submodule add ../sub new-sub &&\n+\t\ttest_path_is_dir .git/modules/new-sub &&\n+\t\tgit config submodule.new-sub.gitdir > actual &&\n+\t\techo \".git/modules/new-sub\" > expect &&\n+\t\ttest_cmp expect actual\n+\t) &&\n+\tgit config --global --unset init.defaultSubmodulePathConfig\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"533250","messageId":"20260107230145.517562-7-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 06/11] submodule--helper: add gitdir migration command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:40Z","receivedAt":"2026-01-07T23:03:12Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Manually running\n\"git config submodule.<name>.gitdir .git/modules/<name>\"\nfor each submodule can be impractical, so add a migration command to\nsubmodule--helper to automatically create configs for all submodules\nas required by extensions.submodulePathConfig.\n\nThe command calls create_default_gitdir_config() which validates the\ngitdir paths before adding the configs.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |  6 +-\n builtin/submodule--helper.c                | 61 ++++++++++++++\n t/t7425-submodule-gitdir-path-extension.sh | 92 +++++++++++++++++++---\n 3 files changed, 145 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex e8d9d9a19a..2aef3315b1 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -93,8 +93,10 @@ Git will error out if a module does not have a corresponding\n `submodule.<name>.gitdir` set.\n +\n Existing (pre-extension) submodules need to be migrated by adding the missing\n-config entries. This is done manually for now, e.g. for each submodule:\n-`git config submodule.<name>.gitdir .git/modules/<name>`.\n+config entries. This can be done manually, e.g. for each submodule:\n+`git config submodule.<name>.gitdir .git/modules/<name>`, or via the\n+`git submodule--helper migrate-gitdir-configs` command which iterates over all\n+submodules and attempts to migrate them.\n +\n The extension can be enabled automatically for new repositories by setting\n `init.defaultSubmodulePathConfig` to `true`, for example by running\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex b3d6f9ff68..271d549bac 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1266,6 +1266,66 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n \treturn 0;\n }\n \n+static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n+\t\t\t  const char *prefix UNUSED, struct repository *repo)\n+{\n+\tstruct strbuf module_dir = STRBUF_INIT;\n+\tDIR *dir;\n+\tstruct dirent *de;\n+\tint repo_version = 0;\n+\n+\trepo_git_path_append(repo, &module_dir, \"modules/\");\n+\n+\tdir = opendir(module_dir.buf);\n+\tif (!dir)\n+\t\tdie(_(\"could not open '%s'\"), module_dir.buf);\n+\n+\twhile ((de = readdir(dir))) {\n+\t\tstruct strbuf gitdir_path = STRBUF_INIT;\n+\t\tchar *key;\n+\t\tconst char *value;\n+\n+\t\tif (is_dot_or_dotdot(de->d_name))\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_addf(&gitdir_path, \"%s/%s\", module_dir.buf, de->d_name);\n+\t\tif (!is_git_directory(gitdir_path.buf)) {\n+\t\t\tstrbuf_release(&gitdir_path);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tstrbuf_release(&gitdir_path);\n+\n+\t\tkey = xstrfmt(\"submodule.%s.gitdir\", de->d_name);\n+\t\tif (!repo_config_get_string_tmp(repo, key, &value)) {\n+\t\t\t/* Already has a gitdir config, nothing to do. */\n+\t\t\tfree(key);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfree(key);\n+\n+\t\tcreate_default_gitdir_config(de->d_name);\n+\t}\n+\n+\tclosedir(dir);\n+\tstrbuf_release(&module_dir);\n+\n+\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &repo_version);\n+\tif (repo_version == 0 &&\n+\t    repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n+\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n+\t\t      \"Please set it for migration to work, for example: \"\n+\t\t      \"git config core.repositoryformatversion 1\"));\n+\n+\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n+\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n+\t\t      \"for migration to work. Please enable it in the root repo: \"\n+\t\t      \"git config extensions.submodulePathConfig true\"));\n+\n+\trepo->repository_format_submodule_path_cfg = 1;\n+\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3657,6 +3717,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"migrate-gitdir-configs\", &fn, module_migrate),\n \t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 6cb844e809..d2a963d2f1 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -160,8 +160,8 @@ test_expect_success 'fetch mixed submodule changes and verify updates' '\n test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n \tgit config --global init.defaultSubmodulePathConfig true &&\n \tgit init repo-init &&\n-\tgit -C repo-init config extensions.submodulePathConfig > actual &&\n-\techo true > expect &&\n+\tgit -C repo-init config extensions.submodulePathConfig >actual &&\n+\techo true >expect &&\n \ttest_cmp expect actual &&\n \t# create a submodule and check gitdir\n \t(\n@@ -169,8 +169,8 @@ test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n \t\tgit init -b main sub &&\n \t\ttest_commit -C sub sub-initial &&\n \t\tgit submodule add ./sub sub &&\n-\t\tgit config submodule.sub.gitdir > actual &&\n-\t\techo \".git/modules/sub\" > expect &&\n+\t\tgit config submodule.sub.gitdir >actual &&\n+\t\techo \".git/modules/sub\" >expect &&\n \t\ttest_cmp expect actual\n \t) &&\n \tgit config --global --unset init.defaultSubmodulePathConfig\n@@ -240,15 +240,15 @@ test_expect_success '`git clone` respects init.defaultSubmodulePathConfig' '\n \t\tcd repo-clone &&\n \n \t\t# verify new repo extension is inherited from global config\n-\t\tgit config extensions.submodulePathConfig > actual &&\n-\t\techo true > expect &&\n+\t\tgit config extensions.submodulePathConfig >actual &&\n+\t\techo true >expect &&\n \t\ttest_cmp expect actual &&\n \n \t\t# new submodule has a gitdir config\n \t\tgit submodule add ../sub sub &&\n \t\ttest_path_is_dir .git/modules/sub &&\n-\t\tgit config submodule.sub.gitdir > actual &&\n-\t\techo \".git/modules/sub\" > expect &&\n+\t\tgit config submodule.sub.gitdir >actual &&\n+\t\techo \".git/modules/sub\" >expect &&\n \t\ttest_cmp expect actual\n \t) &&\n \tgit config --global --unset init.defaultSubmodulePathConfig\n@@ -262,8 +262,8 @@ test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmo\n \t\tcd repo-clone-recursive &&\n \n \t\t# verify new repo extension is inherited from global config\n-\t\tgit config extensions.submodulePathConfig > actual &&\n-\t\techo true > expect &&\n+\t\tgit config extensions.submodulePathConfig >actual &&\n+\t\techo true >expect &&\n \t\ttest_cmp expect actual &&\n \n \t\t# previous submodules should exist\n@@ -275,11 +275,79 @@ test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmo\n \t\t# create another submodule and check that gitdir is created\n \t\tgit submodule add ../sub new-sub &&\n \t\ttest_path_is_dir .git/modules/new-sub &&\n-\t\tgit config submodule.new-sub.gitdir > actual &&\n-\t\techo \".git/modules/new-sub\" > expect &&\n+\t\tgit config submodule.new-sub.gitdir >actual &&\n+\t\techo \".git/modules/new-sub\" >expect &&\n \t\ttest_cmp expect actual\n \t) &&\n \tgit config --global --unset init.defaultSubmodulePathConfig\n '\n \n+test_expect_success 'submodule--helper migrates legacy modules' '\n+\t(\n+\t\tcd upstream &&\n+\n+\t\t# previous submodules exist and were not migrated yet\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\n+\t\t# run migration\n+\t\tgit submodule--helper migrate-gitdir-configs &&\n+\n+\t\t# test that migration worked\n+\t\tgit config submodule.sub1.gitdir >actual &&\n+\t\techo \".git/modules/sub1\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit config submodule.sub2.gitdir >actual &&\n+\t\techo \".git/modules/sub2\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# repository extension is enabled after migration\n+\t\tgit config extensions.submodulePathConfig >actual &&\n+\t\techo \"true\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` works after migration' '\n+\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n+\n+\t# test with extension disabled after the upstream repo was migrated\n+\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# init.defaultSubmodulePathConfig was disabled before clone, so\n+\t\t# the repo extension config should also be off, the migration ignored\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\n+\t\t# modules should look like there was no migration done\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2\n+\t) &&\n+\trm -rf repo-clone-recursive &&\n+\n+\t# enable the extension, then retry the clone\n+\tgit config --global init.defaultSubmodulePathConfig true &&\n+\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# repository extension is enabled\n+\t\tgit config extensions.submodulePathConfig >actual &&\n+\t\techo \"true\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# gitdir configs exist for submodules\n+\t\tgit config submodule.sub1.gitdir &&\n+\t\tgit config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2\n+\t) &&\n+\tgit config --global --unset init.defaultSubmodulePathConfig\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"533251","messageId":"20260107230145.517562-8-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 07/11] builtin/credential-store: move is_rfc3986_unreserved to url.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:41Z","receivedAt":"2026-01-07T23:03:14Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"is_rfc3986_unreserved() was moved to credential-store.c and was made\nstatic by f89854362c (credential-store: move related functions to\ncredential-store file, 2023-06-06) under a correct assumption, at the\ntime, that it was the only place using it.\n\nHowever now we need it to apply URL-encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back as a\npublic function exposed via url.h, instead of the old helper path\n(strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n\nThis function will be used in subsequent commits which do the encoding.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c | 7 +------\n url.c                      | 6 ++++++\n url.h                      | 7 +++++++\n 3 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..bc1453c6b2 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -7,6 +7,7 @@\n #include \"path.h\"\n #include \"string-list.h\"\n #include \"parse-options.h\"\n+#include \"url.h\"\n #include \"write-or-die.h\"\n \n static struct lock_file credential_lock;\n@@ -76,12 +77,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/url.c b/url.c\nindex 282b12495a..adc289229c 100644\n--- a/url.c\n+++ b/url.c\n@@ -3,6 +3,12 @@\n #include \"strbuf.h\"\n #include \"url.h\"\n \n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex 2a27c34277..e644c3c809 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,4 +21,11 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+/*\n+ * The set of unreserved characters as per STD66 (RFC3986) is\n+ * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n+ * components without percent-encoding.\n+ */\n+int is_rfc3986_unreserved(char ch);\n+\n #endif /* URL_H */\n-- \n2.51.2\n\n"},{"id":"533252","messageId":"20260107230145.517562-9-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 08/11] submodule--helper: fix filesystem collisions by encoding gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:42Z","receivedAt":"2026-01-07T23:03:17Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Fix nested filesystem collisions by url-encoding gitdir paths stored\nin submodule.%s.gitdir, when extensions.submodulePathConfig is enabled.\n\nCredit goes to Junio and Patrick for coming up with this design: the\nencoding is only applied when necessary, to newly added submodules.\n\nExisting modules don't need the encoding because git already errors\nout when detecting nested gitdirs before this patch.\n\nThis commit adds the basic url-encoding and some tests. Next commits\nextend the encode -> validate -> retry loop to fix more conflicts.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 12 +++++\n submodule.c                                | 42 +++++++++++++++-\n t/t7425-submodule-gitdir-path-extension.sh | 57 ++++++++++++++++++++++\n 3 files changed, 110 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 271d549bac..cb8b850dd6 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -34,6 +34,7 @@\n #include \"list-objects-filter-options.h\"\n #include \"wildmatch.h\"\n #include \"strbuf.h\"\n+#include \"url.h\"\n \n #define OPT_QUIET (1 << 0)\n #define OPT_CACHED (1 << 1)\n@@ -465,12 +466,23 @@ static void create_default_gitdir_config(const char *submodule_name)\n {\n \tstruct strbuf gitdir_path = STRBUF_INIT;\n \n+\t/* Case 1: try the plain module name */\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n \tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n \t\tstrbuf_release(&gitdir_path);\n \t\treturn;\n \t}\n \n+\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n+\t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\n \t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\ndiff --git a/submodule.c b/submodule.c\nindex e0cd6f5dcc..9dc0938340 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -32,6 +32,7 @@\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n #include \"advice.h\"\n+#include \"url.h\"\n \n static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n static int initialized_fetch_ref_tips;\n@@ -2247,12 +2248,43 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+/*\n+ * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n+ * This does not print errors like the non-encoded version, because encoding is supposed\n+ * to mitigate / fix all these.\n+ */\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+{\n+\tconst char *modules_marker = \"/modules/\";\n+\tchar *p = git_dir, *last_submodule_name = NULL;\n+\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n+\t\t    \"extensions.submodulePathConfig enabled.\");\n+\n+\t/* Find the last submodule name in the gitdir path (modules can be nested). */\n+\twhile ((p = strstr(p, modules_marker))) {\n+\t\tlast_submodule_name = p + strlen(modules_marker);\n+\t\tp++;\n+\t}\n+\n+\t/* Prevent the use of '/' in encoded names */\n+\tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+static int validate_submodule_legacy_git_dir(char *git_dir, const char *submodule_name)\n {\n \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n \tchar *p;\n \tint ret = 0;\n \n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tBUG(\"validate_submodule_git_dir() must be called with \"\n+\t\t    \"extensions.submodulePathConfig disabled.\");\n+\n \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n \t    strcmp(p, submodule_name))\n \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n@@ -2288,6 +2320,14 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \treturn 0;\n }\n \n+int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+{\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\treturn validate_submodule_legacy_git_dir(git_dir, submodule_name);\n+\n+\treturn validate_submodule_encoded_git_dir(git_dir, submodule_name);\n+}\n+\n int validate_submodule_path(const char *path)\n {\n \tchar *p = xstrdup(path);\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex d2a963d2f1..6ee810462a 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -350,4 +350,61 @@ test_expect_success '`git clone --recurse-submodules` works after migration' '\n \tgit config --global --unset init.defaultSubmodulePathConfig\n '\n \n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of encoded sibling submodules must not be nested' '\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules nested clone_nested &&\n+\n+\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n+\tgit -C clone_nested config submodule.hippo.gitdir >actual &&\n+\ttest_grep \"\\.git/modules/hippo$\" actual &&\n+\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks &&\n+\tgit -C clone_nested config submodule.hippo/hooks.gitdir >actual &&\n+\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules --jobs=2 nested clone_parallel &&\n+\n+\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n+\tgit -C clone_nested config submodule.hippo.gitdir >actual &&\n+\ttest_grep \"\\.git/modules/hippo$\" actual &&\n+\n+\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks &&\n+\tgit -C clone_nested config submodule.hippo/hooks.gitdir >actual &&\n+\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n+'\n+\n+test_expect_success 'disabling extensions.submodulePathConfig prevents nested submodules' '\n+\t(\n+\t\tcd clone_nested &&\n+\t\t# disable extension and verify failure\n+\t\tgit config --replace-all extensions.submodulePathConfig false &&\n+\t\ttest_must_fail git submodule add ./thing2 hippo/foobar &&\n+\t\t# re-enable extension and verify it works\n+\t\tgit config --replace-all extensions.submodulePathConfig true &&\n+\t\tgit submodule add ./thing2 hippo/foobar\n+\t)\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"533253","messageId":"20260107230145.517562-10-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 09/11] submodule: fix case-folding gitdir filesystem collisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:43Z","receivedAt":"2026-01-07T23:03:23Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new check when extension.submodulePathConfig is enabled, to\ndetect and prevent case-folding filesystem colisions. When this\nnew check is triggered, a stricter casefolding aware URI encoding\nis used to percent-encode uppercase characters.\n\nBy using this check/retry mechanism the uppercase encoding is\nonly applied when necessary, so case-sensitive filesystems are\nnot affected.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 26 ++++++++++-\n submodule.c                                | 53 +++++++++++++++++++++-\n t/t7425-submodule-gitdir-path-extension.sh | 35 ++++++++++++++\n url.c                                      |  7 +++\n url.h                                      |  7 +++\n 5 files changed, 126 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex cb8b850dd6..402f98489a 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -473,7 +473,7 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\treturn;\n \t}\n \n-\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\t/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n \tstrbuf_reset(&gitdir_path);\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n \tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n@@ -482,6 +482,30 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\treturn;\n \t}\n \n+\t/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\treturn;\n+\n+\t/* Case 2.3: Try some derived gitdir names, see if one sticks */\n+\tfor (char c = '0'; c <= '9'; c++) {\n+\t\tstrbuf_reset(&gitdir_path);\n+\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n+\t\tstrbuf_addch(&gitdir_path, c);\n+\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\t\treturn;\n+\n+\t\tstrbuf_reset(&gitdir_path);\n+\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n+\t\tstrbuf_addch(&gitdir_path, c);\n+\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\t\treturn;\n+\t}\n+\n \t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\ndiff --git a/submodule.c b/submodule.c\nindex 9dc0938340..2fb0f404fd 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2248,15 +2248,58 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n+static int check_casefolding_conflict(const char *git_dir,\n+\t\t\t\t      const char *submodule_name,\n+\t\t\t\t      const bool suffixes_match)\n+{\n+\tchar *p, *modules_dir = xstrdup(git_dir);\n+\tstruct dirent *de;\n+\tDIR *dir = NULL;\n+\tint ret = 0;\n+\n+\tif ((p = find_last_dir_sep(modules_dir)))\n+\t\t*p = '\\0';\n+\n+\t/* No conflict is possible if modules_dir doesn't exist (first clone) */\n+\tif (!is_directory(modules_dir))\n+\t\tgoto cleanup;\n+\n+\tdir = opendir(modules_dir);\n+\tif (!dir) {\n+\t\tret = -1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/* Check for another directory under .git/modules that differs only in case. */\n+\twhile ((de = readdir(dir))) {\n+\t\tif (!strcmp(de->d_name, \".\") || !strcmp(de->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tif ((suffixes_match || is_git_directory(git_dir)) &&\n+\t\t    !strcasecmp(de->d_name, submodule_name) &&\n+\t\t    strcmp(de->d_name, submodule_name)) {\n+\t\t\tret = -1; /* collision found */\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+cleanup:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tfree(modules_dir);\n+\treturn ret;\n+}\n+\n /*\n  * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n  * This does not print errors like the non-encoded version, because encoding is supposed\n  * to mitigate / fix all these.\n  */\n-static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name)\n {\n \tconst char *modules_marker = \"/modules/\";\n \tchar *p = git_dir, *last_submodule_name = NULL;\n+\tint config_ignorecase = 0;\n \n \tif (!the_repository->repository_format_submodule_path_cfg)\n \t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n@@ -2272,6 +2315,14 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n \t\treturn -1;\n \n+\t/* Prevent conflicts on case-folding filesystems */\n+\trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n+\tif (ignore_case || config_ignorecase) {\n+\t\tbool suffixes_match = !strcmp(last_submodule_name, submodule_name);\n+\t\treturn check_casefolding_conflict(git_dir, submodule_name,\n+\t\t\t\t\t\t  suffixes_match);\n+\t}\n+\n \treturn 0;\n }\n \ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 6ee810462a..c49e67b4c8 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -407,4 +407,39 @@ test_expect_success 'disabling extensions.submodulePathConfig prevents nested su\n \t)\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are correctly encoded' '\n+\tgit clone -c extensions.submodulePathConfig=true main cloned-folding &&\n+\t(\n+\t\tcd cloned-folding &&\n+\n+\t\t# conflict: the \"folding\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"folding\" &&\n+\t\ttest_commit lowercase &&\n+\t\tgit submodule add ../new-sub \"FoldinG\" &&\n+\t\ttest_commit uppercase &&\n+\n+\t\t# conflict: the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"FOO\" &&\n+\t\ttest_commit uppercase-foo &&\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\ttest_commit lowercase-foo &&\n+\n+\t\t# create a multi conflict between foobar, fooBar and foo%42ar\n+\t\t# the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"foobar\" &&\n+\t\ttest_commit lowercase-foobar &&\n+\t\tgit submodule add ../new-sub \"foo%42ar\" &&\n+\t\ttest_commit encoded-foo%42ar &&\n+\t\tgit submodule add ../new-sub \"fooBar\" &&\n+\t\ttest_commit mixed-fooBar\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-folding \"folding\" \"modules/folding\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FoldinG\" \"modules/%46oldin%47\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FOO\" \"modules/FOO\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foobar\" \"modules/foobar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo%42ar\" \"modules/foo%42ar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n+'\n+\n test_done\ndiff --git a/url.c b/url.c\nindex adc289229c..3ca5987e90 100644\n--- a/url.c\n+++ b/url.c\n@@ -9,6 +9,13 @@ int is_rfc3986_unreserved(char ch)\n \t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n }\n \n+int is_casefolding_rfc3986_unreserved(char c)\n+{\n+\treturn (c >= 'a' && c <= 'z') ||\n+\t       (c >= '0' && c <= '9') ||\n+\t       c == '-' || c == '.' || c == '_' || c == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex e644c3c809..cd9140e994 100644\n--- a/url.h\n+++ b/url.h\n@@ -28,4 +28,11 @@ void str_end_url_with_slash(const char *url, char **dest);\n  */\n int is_rfc3986_unreserved(char ch);\n \n+/*\n+ * This is a variant of is_rfc3986_unreserved() that treats uppercase\n+ * letters as \"reserved\". This forces them to be percent-encoded, allowing\n+ * 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.\n+ */\n+int is_casefolding_rfc3986_unreserved(char c);\n+\n #endif /* URL_H */\n-- \n2.51.2\n\n"},{"id":"533254","messageId":"20260107230145.517562-11-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 10/11] submodule: hash the submodule name for the gitdir path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:44Z","receivedAt":"2026-01-07T23:03:27Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"If none of the previous plain-text / encoding / derivation steps work\nand case 2.4 is reached, then try a hash of the submodule name to see\nif that can be a valid gitdir before giving up and throwing an error.\n\nThis is a \"last resort\" type of measure to avoid conflicts since it\nloses the human readability of the gitdir path. This logic will be\nreached in rare cases, as can be seen in the test we added.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 19 +++++++\n t/t7425-submodule-gitdir-path-extension.sh | 59 ++++++++++++++++++++++\n 2 files changed, 78 insertions(+)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 402f98489a..ab4a6318b7 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -465,6 +465,10 @@ static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n static void create_default_gitdir_config(const char *submodule_name)\n {\n \tstruct strbuf gitdir_path = STRBUF_INIT;\n+\tstruct git_hash_ctx ctx;\n+\tchar hex_name_hash[GIT_MAX_HEXSZ + 1], header[128];\n+\tunsigned char raw_name_hash[GIT_MAX_RAWSZ];\n+\tint header_len;\n \n \t/* Case 1: try the plain module name */\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n@@ -506,6 +510,21 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\t\treturn;\n \t}\n \n+\t/* Case 2.4: If all the above failed, try a hash of the name as a last resort */\n+\theader_len = snprintf(header, sizeof(header), \"blob %zu\", strlen(submodule_name));\n+\tthe_hash_algo->init_fn(&ctx);\n+\tthe_hash_algo->update_fn(&ctx, header, header_len);\n+\tthe_hash_algo->update_fn(&ctx, \"\\0\", 1);\n+\tthe_hash_algo->update_fn(&ctx, submodule_name, strlen(submodule_name));\n+\tthe_hash_algo->final_fn(raw_name_hash, &ctx);\n+\thash_to_hex_algop_r(hex_name_hash, raw_name_hash, the_hash_algo);\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", hex_name_hash);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n \t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex c49e67b4c8..1c2d4905b1 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -442,4 +442,63 @@ test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are corre\n \tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a last resort' '\n+\tgit clone -c extensions.submodulePathConfig=true main cloned-hash &&\n+\t(\n+\t\tcd cloned-hash &&\n+\n+\t\t# conflict: add all submodule conflicting variants until we reach the\n+\t\t# final hashing conflict resolution for submodule \"foo\"\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\tgit submodule add ../new-sub \"foo0\" &&\n+\t\tgit submodule add ../new-sub \"foo1\" &&\n+\t\tgit submodule add ../new-sub \"foo2\" &&\n+\t\tgit submodule add ../new-sub \"foo3\" &&\n+\t\tgit submodule add ../new-sub \"foo4\" &&\n+\t\tgit submodule add ../new-sub \"foo5\" &&\n+\t\tgit submodule add ../new-sub \"foo6\" &&\n+\t\tgit submodule add ../new-sub \"foo7\" &&\n+\t\tgit submodule add ../new-sub \"foo8\" &&\n+\t\tgit submodule add ../new-sub \"foo9\" &&\n+\t\tgit submodule add ../new-sub \"%46oo\" &&\n+\t\tgit submodule add ../new-sub \"%46oo0\" &&\n+\t\tgit submodule add ../new-sub \"%46oo1\" &&\n+\t\tgit submodule add ../new-sub \"%46oo2\" &&\n+\t\tgit submodule add ../new-sub \"%46oo3\" &&\n+\t\tgit submodule add ../new-sub \"%46oo4\" &&\n+\t\tgit submodule add ../new-sub \"%46oo5\" &&\n+\t\tgit submodule add ../new-sub \"%46oo6\" &&\n+\t\tgit submodule add ../new-sub \"%46oo7\" &&\n+\t\tgit submodule add ../new-sub \"%46oo8\" &&\n+\t\tgit submodule add ../new-sub \"%46oo9\" &&\n+\t\ttest_commit add-foo-variants &&\n+\t\tgit submodule add ../new-sub \"Foo\" &&\n+\t\ttest_commit add-uppercase-foo\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo\" \"modules/foo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo0\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo1\" \"modules/foo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo2\" \"modules/foo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo3\" \"modules/foo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo4\" \"modules/foo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo5\" \"modules/foo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo6\" \"modules/foo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo7\" \"modules/foo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo8\" \"modules/foo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo9\" \"modules/foo9\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo\" \"modules/%46oo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo0\" \"modules/%46oo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo1\" \"modules/%46oo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo2\" \"modules/%46oo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo3\" \"modules/%46oo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo4\" \"modules/%46oo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo5\" \"modules/%46oo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo6\" \"modules/%46oo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo7\" \"modules/%46oo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo8\" \"modules/%46oo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo9\" \"modules/%46oo9\" &&\n+\thash=$(printf \"Foo\" | git hash-object --stdin) &&\n+\tverify_submodule_gitdir_path cloned-hash \"Foo\" \"modules/${hash}\"\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"533255","messageId":"20260107230145.517562-12-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"[PATCH v8 11/11] submodule: detect conflicts with existing gitdir configs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-07T23:01:45Z","receivedAt":"2026-01-07T23:03:29Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Credit goes to Emily and Josh for testing and noticing a corner-case\nwhich caused conflicts with existing gitdir configs to silently pass\nvalidation, then fail later in add_submodule() with a cryptic error:\n\nfatal: A git directory for 'nested%2fsub' is found locally with remote(s):\n  origin\t/.../trash directory.t7425-submodule-gitdir-path-extension/sub\n\nThis change ensures the validation step checks existing gitdirs for\nconflicts. We only have to do this for submodules having gitdirs,\nbecause those without submodule.%s.gitdir need to be migrated and\nwill throw an error earlier in the submodule codepath.\n\nQuoting Josh:\n My testing setup has been as follows:\n * Using our locally-built Git with our downstream patch of [1] included:\n   * create a repo \"sub\"\n   * create a repo \"super\"\n   * In \"super\":\n     * mkdir nested\n     * git submodule add ../sub nested/sub\n     * Verify that the submodule's gitdir is .git/modules/nested%2fsub\n * Using a build of git from upstream `next` plus this series:\n   * git config set --global extensions.submodulepathconfig true\n   * git clone --recurse-submodules super super2\n   * create a repo \"nested%2fsub\"\n   * In \"super2\":\n     * git submodule add ../nested%2fsub\n\nAt this point I'd expect the collision detection / encoding to take\neffect, but instead I get the error listed above.\nEnd quote\n\nSuggested-by: Josh Steadmon <steadmon@google.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n submodule.c                                | 61 ++++++++++++++++++++++\n t/t7425-submodule-gitdir-path-extension.sh | 28 ++++++++++\n 2 files changed, 89 insertions(+)\n\ndiff --git a/submodule.c b/submodule.c\nindex 2fb0f404fd..8c17da6a5a 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2290,6 +2290,62 @@ static int check_casefolding_conflict(const char *git_dir,\n \treturn ret;\n }\n \n+struct submodule_from_gitdir_cb {\n+\tconst char *gitdir;\n+\tconst char *submodule_name;\n+\tbool conflict_found;\n+};\n+\n+static int find_conflict_by_gitdir_cb(const char *var, const char *value,\n+\t\t\t\t      const struct config_context *ctx UNUSED, void *data)\n+{\n+\tstruct submodule_from_gitdir_cb *cb = data;\n+\tconst char *submodule_name_start;\n+\tsize_t submodule_name_len;\n+\tconst char *suffix = \".gitdir\";\n+\tsize_t suffix_len = strlen(suffix);\n+\n+\tif (!skip_prefix(var, \"submodule.\", &submodule_name_start))\n+\t\treturn 0;\n+\n+\t/* Check if submodule_name_start ends with \".gitdir\" */\n+\tsubmodule_name_len = strlen(submodule_name_start);\n+\tif (submodule_name_len < suffix_len ||\n+\t    strcmp(submodule_name_start + submodule_name_len - suffix_len, suffix) != 0)\n+\t\treturn 0; /* Does not end with \".gitdir\" */\n+\n+\tsubmodule_name_len -= suffix_len;\n+\n+\t/*\n+\t * A conflict happens if:\n+\t * 1. The submodule names are different and\n+\t * 2. The gitdir paths resolve to the same absolute path\n+\t */\n+\tif (value && strncmp(cb->submodule_name, submodule_name_start, submodule_name_len)) {\n+\t\tchar *abs_path_cb = absolute_pathdup(cb->gitdir);\n+\t\tchar *abs_path_value = absolute_pathdup(value);\n+\n+\t\tcb->conflict_found = !strcmp(abs_path_cb, abs_path_value);\n+\n+\t\tfree(abs_path_cb);\n+\t\tfree(abs_path_value);\n+\t}\n+\n+\treturn cb->conflict_found;\n+}\n+\n+static bool submodule_conflicts_with_existing(const char *gitdir, const char *submodule_name)\n+{\n+\tstruct submodule_from_gitdir_cb cb = { 0 };\n+\tcb.submodule_name = submodule_name;\n+\tcb.gitdir = gitdir;\n+\n+\t/* Find conflicts with existing repo gitdir configs */\n+\trepo_config(the_repository, find_conflict_by_gitdir_cb, &cb);\n+\n+\treturn cb.conflict_found;\n+}\n+\n /*\n  * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n  * This does not print errors like the non-encoded version, because encoding is supposed\n@@ -2315,6 +2371,11 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n \t\treturn -1;\n \n+\t/* Prevent conflicts with existing submodule gitdirs */\n+\tif (is_git_directory(git_dir) &&\n+\t    submodule_conflicts_with_existing(git_dir, submodule_name))\n+\t\t\treturn -1;\n+\n \t/* Prevent conflicts on case-folding filesystems */\n \trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n \tif (ignore_case || config_ignorecase) {\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 1c2d4905b1..50d618045b 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -501,4 +501,32 @@ test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a\n \tverify_submodule_gitdir_path cloned-hash \"Foo\" \"modules/${hash}\"\n '\n \n+test_expect_success 'submodule gitdir conflicts with previously encoded name (local config)' '\n+\tgit init -b main super_with_encoded &&\n+\t(\n+\t\tcd super_with_encoded &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\t# Add a submodule with a nested path\n+\t\tgit submodule add --name \"nested/sub\" ../sub nested/sub &&\n+\t\ttest_commit add-encoded-gitdir &&\n+\n+\t\tverify_submodule_gitdir_path . \"nested/sub\" \"modules/nested%2fsub\" &&\n+\t\ttest_path_is_dir \".git/modules/nested%2fsub\"\n+\t) &&\n+\n+\t# create a submodule that will conflict with the encoded gitdir name:\n+\t# the existing gitdir is \".git/modules/nested%2fsub\", which is used\n+\t# by \"nested/sub\", so the new submod will get another (non-conflicting)\n+\t# name: \"nested%252fsub\".\n+\t(\n+\t\tcd super_with_encoded &&\n+\t\tgit submodule add ../sub \"nested%2fsub\" &&\n+\t\tverify_submodule_gitdir_path . \"nested%2fsub\" \"modules/nested%252fsub\" &&\n+\t\ttest_path_is_dir \".git/modules/nested%252fsub\"\n+\t)\n+'\n+\n test_done\n-- \n2.51.2\n\n"},{"id":"533270","messageId":"aV9S2d_gRgfGNpJh@pks.im","threadId":"63967","inReplyTo":"20260107230145.517562-6-adrian.ratiu@collabora.com","subject":"Re: [PATCH v8 05/11] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-08T06:46:49Z","receivedAt":"2026-01-08T06:46:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Jan 08, 2026 at 01:01:39AM +0200, Adrian Ratiu wrote:\n> diff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\n> index 453183e27c..6cb844e809 100755\n> --- a/t/t7425-submodule-gitdir-path-extension.sh\n> +++ b/t/t7425-submodule-gitdir-path-extension.sh\n> @@ -157,4 +157,129 @@ test_expect_success 'fetch mixed submodule changes and verify updates' '\n>  \t)\n>  '\n>  \n> +test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n> +\tgit config --global init.defaultSubmodulePathConfig true &&\n> +\tgit init repo-init &&\n> +\tgit -C repo-init config extensions.submodulePathConfig > actual &&\n> +\techo true > expect &&\n> +\ttest_cmp expect actual &&\n> +\t# create a submodule and check gitdir\n> +\t(\n> +\t\tcd repo-init &&\n> +\t\tgit init -b main sub &&\n> +\t\ttest_commit -C sub sub-initial &&\n> +\t\tgit submodule add ./sub sub &&\n> +\t\tgit config submodule.sub.gitdir > actual &&\n> +\t\techo \".git/modules/sub\" > expect &&\n> +\t\ttest_cmp expect actual\n> +\t) &&\n> +\tgit config --global --unset init.defaultSubmodulePathConfig\n\nWe have `test_config ()` to handle setting and unsetting for you. Also,\nthis function uses `test_when_finished ()` so that we unset the config\neven if the test fails.\n\nYou have the same pattern in a test further down and in patch 6.\n\nPatrick\n"},{"id":"533271","messageId":"aV9S3rV6wKZp9CP2@pks.im","threadId":"63967","inReplyTo":"20260107230145.517562-7-adrian.ratiu@collabora.com","subject":"Re: [PATCH v8 06/11] submodule--helper: add gitdir migration command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-08T06:46:54Z","receivedAt":"2026-01-08T06:47:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Jan 08, 2026 at 01:01:40AM +0200, Adrian Ratiu wrote:\n> diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\n> index b3d6f9ff68..271d549bac 100644\n> --- a/builtin/submodule--helper.c\n> +++ b/builtin/submodule--helper.c\n> @@ -1266,6 +1266,66 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n>  \treturn 0;\n>  }\n>  \n> +static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n> +\t\t\t  const char *prefix UNUSED, struct repository *repo)\n> +{\n> +\tstruct strbuf module_dir = STRBUF_INIT;\n> +\tDIR *dir;\n> +\tstruct dirent *de;\n> +\tint repo_version = 0;\n> +\n> +\trepo_git_path_append(repo, &module_dir, \"modules/\");\n> +\n> +\tdir = opendir(module_dir.buf);\n> +\tif (!dir)\n> +\t\tdie(_(\"could not open '%s'\"), module_dir.buf);\n> +\n> +\twhile ((de = readdir(dir))) {\n> +\t\tstruct strbuf gitdir_path = STRBUF_INIT;\n> +\t\tchar *key;\n> +\t\tconst char *value;\n> +\n> +\t\tif (is_dot_or_dotdot(de->d_name))\n> +\t\t\tcontinue;\n> +\n> +\t\tstrbuf_addf(&gitdir_path, \"%s/%s\", module_dir.buf, de->d_name);\n> +\t\tif (!is_git_directory(gitdir_path.buf)) {\n> +\t\t\tstrbuf_release(&gitdir_path);\n> +\t\t\tcontinue;\n> +\t\t}\n> +\t\tstrbuf_release(&gitdir_path);\n> +\n> +\t\tkey = xstrfmt(\"submodule.%s.gitdir\", de->d_name);\n> +\t\tif (!repo_config_get_string_tmp(repo, key, &value)) {\n> +\t\t\t/* Already has a gitdir config, nothing to do. */\n> +\t\t\tfree(key);\n> +\t\t\tcontinue;\n> +\t\t}\n> +\t\tfree(key);\n> +\n> +\t\tcreate_default_gitdir_config(de->d_name);\n> +\t}\n> +\n> +\tclosedir(dir);\n> +\tstrbuf_release(&module_dir);\n> +\n> +\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &repo_version);\n> +\tif (repo_version == 0 &&\n> +\t    repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n> +\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n> +\t\t      \"Please set it for migration to work, for example: \"\n> +\t\t      \"git config core.repositoryformatversion 1\"));\n> +\n> +\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n> +\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n> +\t\t      \"for migration to work. Please enable it in the root repo: \"\n> +\t\t      \"git config extensions.submodulePathConfig true\"));\n\nThese error messages could probably use some newlines so that they're\nwrapped to a more readable length.\n\nPatrick\n"},{"id":"533272","messageId":"aV9S_oUJjitkeWjo@pks.im","threadId":"63967","inReplyTo":"20260107230145.517562-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v8 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-08T06:47:26Z","receivedAt":"2026-01-08T06:47:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Jan 08, 2026 at 01:01:34AM +0200, Adrian Ratiu wrote:\n> Changes in v8:\n> * Added a new test to ensure gitdir config path is relative (Patrick)\n> * Improved gitdir validation error message and added advice (Patrick)\n> * Renamed init.autoSetupSubmodulePathConfig to init.defaultSubmodulePathConfig\n>   and moved its init logic to initialize_repository_version() (Patrick)\n> * repositoryformatversion is only set to 1 if it's 0, so it doesn't\n>   overwrite potential future higher versions (Patrick)\n> * Fixed global init.defaultSubmodulePathConfig leak between tests (Adrian)\n> * Whitespace and other minor fixes (Junio, Patrick)\n\nI've had two more small comments, but other than that this version looks\ngood to me. Thanks!\n\nPatrick\n"},{"id":"533304","messageId":"bd2wmyi5pq5rd5l23nsk7d6lp3q4664omkq4pfjuwlcdkcajr3@v2c7vsfv2hxa","threadId":"63967","inReplyTo":"aV9S_oUJjitkeWjo@pks.im","subject":"Re: [PATCH v8 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Josh Steadmon","fromEmail":"steadmon@google.com","sentAt":"2026-01-08T22:30:47Z","receivedAt":"2026-01-08T22:30:55Z","isPatch":true,"sender":{"key":"steadmon@google.com","avatar":"https://avatars.githubusercontent.com/u/2654920?v=4"},"body":"On 2026.01.08 07:47, Patrick Steinhardt wrote:\n> On Thu, Jan 08, 2026 at 01:01:34AM +0200, Adrian Ratiu wrote:\n> > Changes in v8:\n> > * Added a new test to ensure gitdir config path is relative (Patrick)\n> > * Improved gitdir validation error message and added advice (Patrick)\n> > * Renamed init.autoSetupSubmodulePathConfig to init.defaultSubmodulePathConfig\n> >   and moved its init logic to initialize_repository_version() (Patrick)\n> > * repositoryformatversion is only set to 1 if it's 0, so it doesn't\n> >   overwrite potential future higher versions (Patrick)\n> > * Fixed global init.defaultSubmodulePathConfig leak between tests (Adrian)\n> > * Whitespace and other minor fixes (Junio, Patrick)\n> \n> I've had two more small comments, but other than that this version looks\n> good to me. Thanks!\n> \n> Patrick\n\nDisclaimer for the list: my employer is funding Adrian's work on this\nseries.\n\nI've tested this locally, and I'm convinced that this can replace our\ndownstream version of this feature without requiring user toil or\ndisruption.\n\nThanks Adrian for your work on this!\n\nThis looks good to me too (modulo Patrick's comments, which I agree\nwith).\n"},{"id":"533534","messageId":"xmqq7btoqa7l.fsf@gitster.g","threadId":"63967","inReplyTo":"bd2wmyi5pq5rd5l23nsk7d6lp3q4664omkq4pfjuwlcdkcajr3@v2c7vsfv2hxa","subject":"Re: [PATCH v8 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-11T18:24:14Z","receivedAt":"2026-01-11T18:24:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Josh Steadmon <steadmon@google.com> writes:\n\n> Thanks Adrian for your work on this!\n>\n> This looks good to me too (modulo Patrick's comments, which I agree\n> with).\n\nThanks, all.\n\nJosh, your confirmation that the polishment brought in with these\niterations would not break the internal users is very very much\nappreciated.\n"},{"id":"533666","messageId":"20260112184632.1334495-1-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20250816213642.3517822-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:21Z","receivedAt":"2026-01-12T18:54:27Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Hello everyone,\n\nFor those new to the series, we're implementing a submodule gitdir\nextension which allows us to have a unified way to determine gitdirs\nand do things like encode submodule paths to avoid FS conflicts.\n\nv9 addresses two more small feedback items from Patrick + some very\nsmall whitespace problems I introduced in v8.\n\nPatches 1-6 implement the basic mechanisms of the new extension.\nPatches 7-11 improve filesystem conflict detection and resolution.\n\nAs always, this is based on the latest master branch, I've checkd\nfor conflicts with next/seen, pushed to Github [1] and succesfully\nran the CI [2].\n\n1: https://github.com/10ne1/git/tree/dev/aratiu/encoding-v9\n2: https://github.com/10ne1/git/actions/runs/20926359849\n\nChanges in v9:\n* Replaced git config --global with test_config_global (Patrick)\n* Split die() messages to multiple lines (Patrick)\n* Moved some of the whitespace fixes added in v8 to the commit\n  which actually introduced the whitespace problem (Adrian)\n\nRange-diff between v8 -> v9:\n 1:  5aae0df74b =  1:  cda5f3688b submodule--helper: use submodule_name_to_gitdir in add_submodule\n 2:  041d921487 =  2:  f57fcc359a submodule: always validate gitdirs inside submodule_name_to_gitdir\n 3:  12ff77be2d =  3:  63e2bcd7bf builtin/submodule--helper: add gitdir command\n 4:  faaea085d2 =  4:  26d0bbff85 submodule: introduce extensions.submodulePathConfig\n 5:  3a65c86a38 !  5:  3f268165b9 submodule: allow runtime enabling extensions.submodulePathConfig\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed sub\n      '\n      \n     +test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n    -+\tgit config --global init.defaultSubmodulePathConfig true &&\n    ++\ttest_config_global init.defaultSubmodulePathConfig true &&\n     +\tgit init repo-init &&\n    -+\tgit -C repo-init config extensions.submodulePathConfig > actual &&\n    -+\techo true > expect &&\n    ++\tgit -C repo-init config extensions.submodulePathConfig >actual &&\n    ++\techo true >expect &&\n     +\ttest_cmp expect actual &&\n     +\t# create a submodule and check gitdir\n     +\t(\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed sub\n     +\t\tgit init -b main sub &&\n     +\t\ttest_commit -C sub sub-initial &&\n     +\t\tgit submodule add ./sub sub &&\n    -+\t\tgit config submodule.sub.gitdir > actual &&\n    -+\t\techo \".git/modules/sub\" > expect &&\n    ++\t\tgit config submodule.sub.gitdir >actual &&\n    ++\t\techo \".git/modules/sub\" >expect &&\n     +\t\ttest_cmp expect actual\n    -+\t) &&\n    -+\tgit config --global --unset init.defaultSubmodulePathConfig\n    ++\t)\n     +'\n     +\n     +test_expect_success '`git init` does not set extension by default' '\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed sub\n     +\n     +test_expect_success '`git clone` respects init.defaultSubmodulePathConfig' '\n     +\ttest_when_finished \"rm -rf repo-clone\" &&\n    -+\tgit config --global init.defaultSubmodulePathConfig true &&\n    ++\ttest_config_global init.defaultSubmodulePathConfig true &&\n     +\tgit clone upstream repo-clone &&\n     +\t(\n     +\t\tcd repo-clone &&\n     +\n     +\t\t# verify new repo extension is inherited from global config\n    -+\t\tgit config extensions.submodulePathConfig > actual &&\n    -+\t\techo true > expect &&\n    ++\t\tgit config extensions.submodulePathConfig >actual &&\n    ++\t\techo true >expect &&\n     +\t\ttest_cmp expect actual &&\n     +\n     +\t\t# new submodule has a gitdir config\n     +\t\tgit submodule add ../sub sub &&\n     +\t\ttest_path_is_dir .git/modules/sub &&\n    -+\t\tgit config submodule.sub.gitdir > actual &&\n    -+\t\techo \".git/modules/sub\" > expect &&\n    ++\t\tgit config submodule.sub.gitdir >actual &&\n    ++\t\techo \".git/modules/sub\" >expect &&\n     +\t\ttest_cmp expect actual\n    -+\t) &&\n    -+\tgit config --global --unset init.defaultSubmodulePathConfig\n    ++\t)\n     +'\n     +\n     +test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmodulePathConfig' '\n     +\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n    -+\tgit config --global init.defaultSubmodulePathConfig true &&\n    ++\ttest_config_global init.defaultSubmodulePathConfig true &&\n     +\tgit clone  --recurse-submodules upstream repo-clone-recursive &&\n     +\t(\n     +\t\tcd repo-clone-recursive &&\n     +\n     +\t\t# verify new repo extension is inherited from global config\n    -+\t\tgit config extensions.submodulePathConfig > actual &&\n    -+\t\techo true > expect &&\n    ++\t\tgit config extensions.submodulePathConfig >actual &&\n    ++\t\techo true >expect &&\n     +\t\ttest_cmp expect actual &&\n     +\n     +\t\t# previous submodules should exist\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed sub\n     +\t\t# create another submodule and check that gitdir is created\n     +\t\tgit submodule add ../sub new-sub &&\n     +\t\ttest_path_is_dir .git/modules/new-sub &&\n    -+\t\tgit config submodule.new-sub.gitdir > actual &&\n    -+\t\techo \".git/modules/new-sub\" > expect &&\n    ++\t\tgit config submodule.new-sub.gitdir >actual &&\n    ++\t\techo \".git/modules/new-sub\" >expect &&\n     +\t\ttest_cmp expect actual\n    -+\t) &&\n    -+\tgit config --global --unset init.defaultSubmodulePathConfig\n    ++\t)\n     +'\n     +\n      test_done\n 6:  c62db6b32f !  6:  0bbe9769d1 submodule--helper: add gitdir migration command\n    @@ builtin/submodule--helper.c: static int module_gitdir(int argc, const char **arg\n     +\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &repo_version);\n     +\tif (repo_version == 0 &&\n     +\t    repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n    -+\t\tdie(_(\"could not set core.repositoryformatversion to 1. \"\n    -+\t\t      \"Please set it for migration to work, for example: \"\n    ++\t\tdie(_(\"could not set core.repositoryformatversion to 1.\\n\"\n    ++\t\t      \"Please set it for migration to work, for example:\\n\"\n     +\t\t      \"git config core.repositoryformatversion 1\"));\n     +\n     +\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n    -+\t\tdie(_(\"could not enable submodulePathConfig extension. It is required \"\n    -+\t\t      \"for migration to work. Please enable it in the root repo: \"\n    ++\t\tdie(_(\"could not enable submodulePathConfig extension. It is required\\n\"\n    ++\t\t      \"for migration to work. Please enable it in the root repo:\\n\"\n     +\t\t      \"git config extensions.submodulePathConfig true\"));\n     +\n     +\trepo->repository_format_submodule_path_cfg = 1;\n    @@ builtin/submodule--helper.c: int cmd_submodule__helper(int argc,\n      \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n     \n      ## t/t7425-submodule-gitdir-path-extension.sh ##\n    -@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success 'fetch mixed submodule changes and verify updates' '\n    - test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n    - \tgit config --global init.defaultSubmodulePathConfig true &&\n    - \tgit init repo-init &&\n    --\tgit -C repo-init config extensions.submodulePathConfig > actual &&\n    --\techo true > expect &&\n    -+\tgit -C repo-init config extensions.submodulePathConfig >actual &&\n    -+\techo true >expect &&\n    - \ttest_cmp expect actual &&\n    - \t# create a submodule and check gitdir\n    - \t(\n    -@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n    - \t\tgit init -b main sub &&\n    - \t\ttest_commit -C sub sub-initial &&\n    - \t\tgit submodule add ./sub sub &&\n    --\t\tgit config submodule.sub.gitdir > actual &&\n    --\t\techo \".git/modules/sub\" > expect &&\n    -+\t\tgit config submodule.sub.gitdir >actual &&\n    -+\t\techo \".git/modules/sub\" >expect &&\n    - \t\ttest_cmp expect actual\n    - \t) &&\n    - \tgit config --global --unset init.defaultSubmodulePathConfig\n    -@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone` respects init.defaultSubmodulePathConfig' '\n    - \t\tcd repo-clone &&\n    - \n    - \t\t# verify new repo extension is inherited from global config\n    --\t\tgit config extensions.submodulePathConfig > actual &&\n    --\t\techo true > expect &&\n    -+\t\tgit config extensions.submodulePathConfig >actual &&\n    -+\t\techo true >expect &&\n    - \t\ttest_cmp expect actual &&\n    - \n    - \t\t# new submodule has a gitdir config\n    - \t\tgit submodule add ../sub sub &&\n    - \t\ttest_path_is_dir .git/modules/sub &&\n    --\t\tgit config submodule.sub.gitdir > actual &&\n    --\t\techo \".git/modules/sub\" > expect &&\n    -+\t\tgit config submodule.sub.gitdir >actual &&\n    -+\t\techo \".git/modules/sub\" >expect &&\n    - \t\ttest_cmp expect actual\n    - \t) &&\n    - \tgit config --global --unset init.defaultSubmodulePathConfig\n     @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmo\n    - \t\tcd repo-clone-recursive &&\n    - \n    - \t\t# verify new repo extension is inherited from global config\n    --\t\tgit config extensions.submodulePathConfig > actual &&\n    --\t\techo true > expect &&\n    -+\t\tgit config extensions.submodulePathConfig >actual &&\n    -+\t\techo true >expect &&\n    - \t\ttest_cmp expect actual &&\n    - \n    - \t\t# previous submodules should exist\n    -@@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmo\n    - \t\t# create another submodule and check that gitdir is created\n    - \t\tgit submodule add ../sub new-sub &&\n    - \t\ttest_path_is_dir .git/modules/new-sub &&\n    --\t\tgit config submodule.new-sub.gitdir > actual &&\n    --\t\techo \".git/modules/new-sub\" > expect &&\n    -+\t\tgit config submodule.new-sub.gitdir >actual &&\n    -+\t\techo \".git/modules/new-sub\" >expect &&\n    - \t\ttest_cmp expect actual\n    - \t) &&\n    - \tgit config --global --unset init.defaultSubmodulePathConfig\n    + \t)\n      '\n      \n     +test_expect_success 'submodule--helper migrates legacy modules' '\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --re\n     +\trm -rf repo-clone-recursive &&\n     +\n     +\t# enable the extension, then retry the clone\n    -+\tgit config --global init.defaultSubmodulePathConfig true &&\n    ++\ttest_config_global init.defaultSubmodulePathConfig true &&\n     +\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n     +\t(\n     +\t\tcd repo-clone-recursive &&\n    @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --re\n     +\t\tgit config submodule.sub2.gitdir &&\n     +\t\ttest_path_is_dir .git/modules/sub1 &&\n     +\t\ttest_path_is_dir .git/modules/sub2\n    -+\t) &&\n    -+\tgit config --global --unset init.defaultSubmodulePathConfig\n    ++\t)\n     +'\n     +\n      test_done\n 7:  c554017f83 =  7:  25e491de11 builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n 8:  7a794b9b61 !  8:  4fc31f2476 submodule--helper: fix filesystem collisions by encoding gitdir paths\n    @@ submodule.c: int validate_submodule_git_dir(char *git_dir, const char *submodule\n     \n      ## t/t7425-submodule-gitdir-path-extension.sh ##\n     @@ t/t7425-submodule-gitdir-path-extension.sh: test_expect_success '`git clone --recurse-submodules` works after migration' '\n    - \tgit config --global --unset init.defaultSubmodulePathConfig\n    + \t)\n      '\n      \n     +test_expect_success 'setup submodules with nested git dirs' '\n 9:  142a85a1af =  9:  45a3ad12bb submodule: fix case-folding gitdir filesystem collisions\n10:  bafde20354 = 10:  bb03d7be7a submodule: hash the submodule name for the gitdir path\n11:  e3fe1f7529 = 11:  6bde65c6d1 submodule: detect conflicts with existing gitdir configs\n\nAdrian Ratiu (11):\n  submodule--helper: use submodule_name_to_gitdir in add_submodule\n  submodule: always validate gitdirs inside submodule_name_to_gitdir\n  builtin/submodule--helper: add gitdir command\n  submodule: introduce extensions.submodulePathConfig\n  submodule: allow runtime enabling extensions.submodulePathConfig\n  submodule--helper: add gitdir migration command\n  builtin/credential-store: move is_rfc3986_unreserved to url.[ch]\n  submodule--helper: fix filesystem collisions by encoding gitdir paths\n  submodule: fix case-folding gitdir filesystem collisions\n  submodule: hash the submodule name for the gitdir path\n  submodule: detect conflicts with existing gitdir configs\n\n Documentation/config/extensions.adoc       |  29 ++\n Documentation/config/init.adoc             |   6 +\n Documentation/config/submodule.adoc        |   7 +\n builtin/credential-store.c                 |   7 +-\n builtin/submodule--helper.c                | 204 +++++++-\n repository.c                               |   1 +\n repository.h                               |   1 +\n setup.c                                    |  17 +\n setup.h                                    |   1 +\n submodule.c                                | 223 +++++++--\n t/lib-verify-submodule-gitdir-path.sh      |  24 +\n t/meson.build                              |   1 +\n t/t7425-submodule-gitdir-path-extension.sh | 528 +++++++++++++++++++++\n t/t9902-completion.sh                      |   1 +\n url.c                                      |  13 +\n url.h                                      |  14 +\n 16 files changed, 1026 insertions(+), 51 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-gitdir-path-extension.sh\n\n-- \n2.52.0\n\n"},{"id":"533662","messageId":"20260112184632.1334495-3-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 02/11] submodule: always validate gitdirs inside submodule_name_to_gitdir","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:23Z","receivedAt":"2026-01-12T18:54:29Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Move the ad-hoc validation checks sprinkled across the source tree,\nafter calling submodule_name_to_gitdir() into the function proper,\nwhich now always validates the gitdir before returning it.\n\nThis simplifies the API and helps to:\n1. Avoid redundant validation calls after submodule_name_to_gitdir().\n2. Avoid the risk of callers forgetting to validate.\n3. Ensure gitdir paths provided by users via configs are always valid\n   (config gitdir paths are added in a subsequent commit).\n\nThe validation function can still be called as many times as needed\noutside submodule_name_to_gitdir(), for example we keep two calls\nwhich are still required, to avoid parallel clone races by re-running\nthe validation in builtin/submodule-helper.c.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c |  4 ----\n submodule.c                 | 12 ++++--------\n 2 files changed, 4 insertions(+), 12 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 6686714e56..aff3f9135e 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1699,10 +1699,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tclone_data_path = to_free = xstrfmt(\"%s/%s\", repo_get_work_tree(the_repository),\n \t\t\t\t\t\t    clone_data->path);\n \n-\tif (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0)\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n-\n \tif (!file_exists(sm_gitdir)) {\n \t\tif (clone_data->require_init && !stat(clone_data_path, &st) &&\n \t\t    !is_empty_dir(clone_data_path))\ndiff --git a/submodule.c b/submodule.c\nindex 40a5c6fb9d..f645372a18 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2166,11 +2166,6 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\tstruct strbuf gitdir = STRBUF_INIT;\n \t\t\tsubmodule_name_to_gitdir(&gitdir, the_repository,\n \t\t\t\t\t\t sub->name);\n-\t\t\tif (validate_submodule_git_dir(gitdir.buf,\n-\t\t\t\t\t\t       sub->name) < 0)\n-\t\t\t\tdie(_(\"refusing to create/use '%s' in another \"\n-\t\t\t\t      \"submodule's git dir\"),\n-\t\t\t\t    gitdir.buf);\n \t\t\tconnect_work_tree_and_git_dir(path, gitdir.buf, 0);\n \t\t\tstrbuf_release(&gitdir);\n \n@@ -2349,9 +2344,6 @@ static void relocate_single_git_dir_into_superproject(const char *path,\n \t\tdie(_(\"could not lookup name for submodule '%s'\"), path);\n \n \tsubmodule_name_to_gitdir(&new_gitdir, the_repository, sub->name);\n-\tif (validate_submodule_git_dir(new_gitdir.buf, sub->name) < 0)\n-\t\tdie(_(\"refusing to move '%s' into an existing git dir\"),\n-\t\t    real_old_git_dir);\n \tif (safe_create_leading_directories_const(the_repository, new_gitdir.buf) < 0)\n \t\tdie(_(\"could not create directory '%s'\"), new_gitdir.buf);\n \treal_new_git_dir = real_pathdup(new_gitdir.buf, 1);\n@@ -2600,4 +2592,8 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t */\n \trepo_git_path_append(r, buf, \"modules/\");\n \tstrbuf_addstr(buf, submodule_name);\n+\n+\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n+\t\t      \"git dir\"), buf->buf);\n }\n-- \n2.52.0\n\n"},{"id":"533663","messageId":"20260112184632.1334495-2-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 01/11] submodule--helper: use submodule_name_to_gitdir in add_submodule","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:22Z","receivedAt":"2026-01-12T18:54:30Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"While testing submodule gitdir path encoding, I noticed submodule--helper\nis still using a hardcoded modules gitdir path leading to test failures.\n\nCall the submodule_name_to_gitdir() helper instead, which was invented\nexactly for this purpose and is already used by all the other locations\nwhich work on gitdirs.\n\nAlso narrow the scope of the submod_gitdir_path variable which is not\nused anymore in the updated \"else\" branch.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 13 +++++++------\n 1 file changed, 7 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex d537ab087a..6686714e56 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -3190,13 +3190,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)\n \n static int add_submodule(const struct add_data *add_data)\n {\n-\tchar *submod_gitdir_path;\n \tstruct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;\n \tstruct string_list reference = STRING_LIST_INIT_NODUP;\n \tint ret = -1;\n \n \t/* perhaps the path already exists and is already a git repo, else clone it */\n \tif (is_directory(add_data->sm_path)) {\n+\t\tchar *submod_gitdir_path;\n \t\tstruct strbuf sm_path = STRBUF_INIT;\n \t\tstrbuf_addstr(&sm_path, add_data->sm_path);\n \t\tsubmod_gitdir_path = xstrfmt(\"%s/.git\", add_data->sm_path);\n@@ -3210,10 +3210,11 @@ static int add_submodule(const struct add_data *add_data)\n \t\tfree(submod_gitdir_path);\n \t} else {\n \t\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\t\tstruct strbuf submod_gitdir = STRBUF_INIT;\n \n-\t\tsubmod_gitdir_path = xstrfmt(\".git/modules/%s\", add_data->sm_name);\n+\t\tsubmodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);\n \n-\t\tif (is_directory(submod_gitdir_path)) {\n+\t\tif (is_directory(submod_gitdir.buf)) {\n \t\t\tif (!add_data->force) {\n \t\t\t\tstruct strbuf msg = STRBUF_INIT;\n \t\t\t\tchar *die_msg;\n@@ -3222,8 +3223,8 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t\t    \"locally with remote(s):\\n\"),\n \t\t\t\t\t    add_data->sm_name);\n \n-\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir_path);\n-\t\t\t\tfree(submod_gitdir_path);\n+\t\t\t\tappend_fetch_remotes(&msg, submod_gitdir.buf);\n+\t\t\t\tstrbuf_release(&submod_gitdir);\n \n \t\t\t\tstrbuf_addf(&msg, _(\"If you want to reuse this local git \"\n \t\t\t\t\t\t    \"directory instead of cloning again from\\n\"\n@@ -3241,7 +3242,7 @@ static int add_submodule(const struct add_data *add_data)\n \t\t\t\t\t \"submodule '%s'\\n\"), add_data->sm_name);\n \t\t\t}\n \t\t}\n-\t\tfree(submod_gitdir_path);\n+\t\tstrbuf_release(&submod_gitdir);\n \n \t\tclone_data.prefix = add_data->prefix;\n \t\tclone_data.path = add_data->sm_path;\n-- \n2.52.0\n\n"},{"id":"533664","messageId":"20260112184632.1334495-4-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 03/11] builtin/submodule--helper: add gitdir command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:24Z","receivedAt":"2026-01-12T18:54:32Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"This exposes the gitdir name computed by submodule_name_to_gitdir()\ninternally, to make it easier for users and tests to interact with it.\n\nNext commit will add a gitdir configuration, so this helper can also be\nused to easily query that config or validate any gitdir path the user\nsets (submodule_name_to_git_dir now runs the validation logic, since\nour previous commit).\n\nBased-on-patch-by: Brandon Williams <bwilliams.eng@gmail.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c | 17 +++++++++++++++++\n 1 file changed, 17 insertions(+)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex aff3f9135e..901213ee71 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1204,6 +1204,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,\n \treturn ret;\n }\n \n+static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n+\t\t\t struct repository *repo)\n+{\n+\tstruct strbuf gitdir = STRBUF_INIT;\n+\n+\tif (argc != 2)\n+\t\tusage(_(\"git submodule--helper gitdir <name>\"));\n+\n+\tsubmodule_name_to_gitdir(&gitdir, repo, argv[1]);\n+\n+\tprintf(\"%s\\n\", gitdir.buf);\n+\n+\tstrbuf_release(&gitdir);\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3591,6 +3607,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\n \t\tOPT_SUBCOMMAND(\"update\", &fn, module_update),\n-- \n2.52.0\n\n"},{"id":"533665","messageId":"20260112184632.1334495-5-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 04/11] submodule: introduce extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:25Z","receivedAt":"2026-01-12T18:54:36Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"The idea of this extension is to abstract away the submodule gitdir\npath implementation: everyone is expected to use the config and not\nworry about how the path is computed internally, either in git or\nother implementations.\n\nWith this extension enabled, the submodule.<name>.gitdir repo config\nbecomes the single source of truth for all submodule gitdir paths.\n\nThe submodule.<name>.gitdir config is added automatically for all new\nsubmodules when this extension is enabled.\n\nGit will throw an error if the extension is enabled and a config is\nmissing, advising users how to migrate. Migration is manual for now.\n\nE.g. to add a missing config entry for an existing \"foo\" module:\ngit config submodule.foo.gitdir .git/modules/foo\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |  23 +++\n Documentation/config/submodule.adoc        |   7 +\n builtin/submodule--helper.c                |  54 ++++++-\n repository.c                               |   1 +\n repository.h                               |   1 +\n setup.c                                    |   7 +\n setup.h                                    |   1 +\n submodule.c                                |  61 ++++----\n t/lib-verify-submodule-gitdir-path.sh      |  24 ++++\n t/meson.build                              |   1 +\n t/t7425-submodule-gitdir-path-extension.sh | 160 +++++++++++++++++++++\n t/t9902-completion.sh                      |   1 +\n 12 files changed, 313 insertions(+), 28 deletions(-)\n create mode 100644 t/lib-verify-submodule-gitdir-path.sh\n create mode 100755 t/t7425-submodule-gitdir-path-extension.sh\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex 532456644b..f4f57c9114 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -73,6 +73,29 @@ relativeWorktrees:::\n \trepaired with either the `--relative-paths` option or with the\n \t`worktree.useRelativePaths` config set to `true`.\n \n+submodulePathConfig:::\n+\tThis extension is for the minority of users who:\n++\n+--\n+* Encounter errors like `refusing to create ... in another submodule's git dir`\n+  due to a number of reasons, like case-insensitive filesystem conflicts when\n+  creating modules named `foo` and `Foo`.\n+* Require more flexible submodule layouts, for example due to nested names like\n+  `foo`, `foo/bar` and `foo/baz` not supported by the default gitdir mechanism\n+  which uses `.git/modules/<plain-name>` locations, causing further conflicts.\n+--\n++\n+When `extensions.submodulePathConfig` is enabled, the `submodule.<name>.gitdir`\n+config becomes the single source of truth for all submodule gitdir paths and is\n+automatically set for all new submodules both during clone and init operations.\n++\n+Git will error out if a module does not have a corresponding\n+`submodule.<name>.gitdir` set.\n++\n+Existing (pre-extension) submodules need to be migrated by adding the missing\n+config entries. This is done manually for now, e.g. for each submodule:\n+`git config submodule.<name>.gitdir .git/modules/<name>`.\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/Documentation/config/submodule.adoc b/Documentation/config/submodule.adoc\nindex 0672d99117..74f1659a91 100644\n--- a/Documentation/config/submodule.adoc\n+++ b/Documentation/config/submodule.adoc\n@@ -52,6 +52,13 @@ submodule.<name>.active::\n \tsubmodule.active config option. See linkgit:gitsubmodules[7] for\n \tdetails.\n \n+submodule.<name>.gitdir::\n+\tThis sets the gitdir path for submodule <name>. This configuration is\n+\trespected when `extensions.submodulePathConfig` is enabled, otherwise it\n+\thas no effect. When enabled, this config becomes the single source of\n+\ttruth for submodule gitdir paths and Git will error if it is missing.\n+\tSee linkgit:git-config[1] for details.\n+\n submodule.active::\n \tA repeated field which contains a pathspec used to match against a\n \tsubmodule's path to determine if the submodule is of interest to git\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 901213ee71..b3d6f9ff68 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -435,6 +435,48 @@ struct init_cb {\n };\n #define INIT_CB_INIT { 0 }\n \n+static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n+\t\t\t\t\t     const char *submodule_name)\n+{\n+\tconst char *value;\n+\tchar *key;\n+\n+\tif (validate_submodule_git_dir(gitdir_path->buf, submodule_name))\n+\t\treturn -1;\n+\n+\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\n+\t/* Nothing to do if the config already exists. */\n+\tif (!repo_config_get_string_tmp(the_repository, key, &value)) {\n+\t\tfree(key);\n+\t\treturn 0;\n+\t}\n+\n+\tif (repo_config_set_gently(the_repository, key, gitdir_path->buf)) {\n+\t\tfree(key);\n+\t\treturn -1;\n+\t}\n+\n+\tfree(key);\n+\treturn 0;\n+}\n+\n+static void create_default_gitdir_config(const char *submodule_name)\n+{\n+\tstruct strbuf gitdir_path = STRBUF_INIT;\n+\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n+\tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n+\t      \"Please ensure it is set, for example by running something like: \"\n+\t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\n+\t    submodule_name, submodule_name, submodule_name);\n+}\n+\n static void init_submodule(const char *path, const char *prefix,\n \t\t\t   const char *super_prefix,\n \t\t\t   unsigned int flags)\n@@ -511,6 +553,10 @@ static void init_submodule(const char *path, const char *prefix,\n \t\tif (repo_config_set_gently(the_repository, sb.buf, upd))\n \t\t\tdie(_(\"Failed to register update mode for submodule path '%s'\"), displaypath);\n \t}\n+\n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tcreate_default_gitdir_config(sub->name);\n+\n \tstrbuf_release(&sb);\n \tfree(displaypath);\n \tfree(url);\n@@ -1801,8 +1847,9 @@ static int clone_submodule(const struct module_clone_data *clone_data,\n \t\tchar *head = xstrfmt(\"%s/HEAD\", sm_gitdir);\n \t\tunlink(head);\n \t\tfree(head);\n-\t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), sm_gitdir);\n+\t\tdie(_(\"refusing to create/use '%s' in another submodule's git dir. \"\n+\t\t      \"Enabling extensions.submodulePathConfig should fix this.\"),\n+\t\t    sm_gitdir);\n \t}\n \n \tconnect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);\n@@ -3582,6 +3629,9 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \tadd_data.progress = !!progress;\n \tadd_data.dissociate = !!dissociate;\n \n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tcreate_default_gitdir_config(add_data.sm_name);\n+\n \tif (add_submodule(&add_data))\n \t\tgoto cleanup;\n \tconfigure_added_submodule(&add_data);\ndiff --git a/repository.c b/repository.c\nindex c7e75215ac..46a7c99930 100644\n--- a/repository.c\n+++ b/repository.c\n@@ -281,6 +281,7 @@ int repo_init(struct repository *repo,\n \trepo->repository_format_worktree_config = format.worktree_config;\n \trepo->repository_format_relative_worktrees = format.relative_worktrees;\n \trepo->repository_format_precious_objects = format.precious_objects;\n+\trepo->repository_format_submodule_path_cfg = format.submodule_path_cfg;\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 6063c4b846..7141237f97 100644\n--- a/repository.h\n+++ b/repository.h\n@@ -165,6 +165,7 @@ struct repository {\n \tint repository_format_worktree_config;\n \tint repository_format_relative_worktrees;\n \tint repository_format_precious_objects;\n+\tint repository_format_submodule_path_cfg;\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 3a6a048620..428427d689 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -686,6 +686,9 @@ static enum extension_result handle_extension(const char *var,\n \t} else if (!strcmp(ext, \"relativeworktrees\")) {\n \t\tdata->relative_worktrees = git_config_bool(var, value);\n \t\treturn EXTENSION_OK;\n+\t} else if (!strcmp(ext, \"submodulepathconfig\")) {\n+\t\tdata->submodule_path_cfg = git_config_bool(var, value);\n+\t\treturn EXTENSION_OK;\n \t}\n \treturn EXTENSION_UNKNOWN;\n }\n@@ -1947,6 +1950,8 @@ const char *setup_git_directory_gently(int *nongit_ok)\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\tthe_repository->repository_format_submodule_path_cfg =\n+\t\t\t\trepo_fmt.submodule_path_cfg;\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@@ -2045,6 +2050,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_submodule_path_cfg =\n+\t\tfmt->submodule_path_cfg;\n \tthe_repository->repository_format_relative_worktrees =\n \t\tfmt->relative_worktrees;\n \tthe_repository->repository_format_partial_clone =\ndiff --git a/setup.h b/setup.h\nindex d55dcc6608..0738dec244 100644\n--- a/setup.h\n+++ b/setup.h\n@@ -167,6 +167,7 @@ struct repository_format {\n \tchar *partial_clone; /* value of extensions.partialclone */\n \tint worktree_config;\n \tint relative_worktrees;\n+\tint submodule_path_cfg;\n \tint is_bare;\n \tint hash_algo;\n \tint compat_hash_algo;\ndiff --git a/submodule.c b/submodule.c\nindex f645372a18..e0cd6f5dcc 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -31,6 +31,7 @@\n #include \"commit-reach.h\"\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n+#include \"advice.h\"\n \n static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n static int initialized_fetch_ref_tips;\n@@ -2158,8 +2159,9 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \t\t\t\tif (validate_submodule_git_dir(git_dir,\n \t\t\t\t\t\t\t       sub->name) < 0)\n \t\t\t\t\tdie(_(\"refusing to create/use '%s' in \"\n-\t\t\t\t\t      \"another submodule's git dir\"),\n-\t\t\t\t\t    git_dir);\n+\t\t\t\t\t      \"another submodule's git dir. \"\n+\t\t\t\t\t      \"Enabling extensions.submodulePathConfig \"\n+\t\t\t\t\t      \"should fix this.\"), git_dir);\n \t\t\t\tfree(git_dir);\n \t\t\t}\n \t\t} else {\n@@ -2570,30 +2572,37 @@ int submodule_to_gitdir(struct repository *repo,\n void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,\n \t\t\t      const char *submodule_name)\n {\n-\t/*\n-\t * NEEDSWORK: The current way of mapping a submodule's name to\n-\t * its location in .git/modules/ has problems with some naming\n-\t * schemes. For example, if a submodule is named \"foo\" and\n-\t * another is named \"foo/bar\" (whether present in the same\n-\t * superproject commit or not - the problem will arise if both\n-\t * superproject commits have been checked out at any point in\n-\t * time), or if two submodule names only have different cases in\n-\t * a case-insensitive filesystem.\n-\t *\n-\t * There are several solutions, including encoding the path in\n-\t * some way, introducing a submodule.<name>.gitdir config in\n-\t * .git/config (not .gitmodules) that allows overriding what the\n-\t * gitdir of a submodule would be (and teach Git, upon noticing\n-\t * a clash, to automatically determine a non-clashing name and\n-\t * to write such a config), or introducing a\n-\t * submodule.<name>.gitdir config in .gitmodules that repo\n-\t * administrators can explicitly set. Nothing has been decided,\n-\t * so for now, just append the name at the end of the path.\n-\t */\n-\trepo_git_path_append(r, buf, \"modules/\");\n-\tstrbuf_addstr(buf, submodule_name);\n+\tif (!r->repository_format_submodule_path_cfg) {\n+\t\t/*\n+\t\t * If extensions.submodulePathConfig is disabled,\n+\t\t * continue to use the plain path.\n+\t\t */\n+\t\trepo_git_path_append(r, buf, \"modules/%s\", submodule_name);\n+\t} else {\n+\t\tconst char *gitdir;\n+\t\tchar *key;\n+\t\tint ret;\n \n-\tif (validate_submodule_git_dir(buf->buf, submodule_name) < 0)\n+\t\t/* Otherwise the extension is enabled, so use the gitdir config. */\n+\t\tkey = xstrfmt(\"submodule.%s.gitdir\", submodule_name);\n+\t\tret = repo_config_get_string_tmp(r, key, &gitdir);\n+\t\tFREE_AND_NULL(key);\n+\n+\t\tif (ret)\n+\t\t\tdie(_(\"the 'submodule.%s.gitdir' config does not exist for module '%s'. \"\n+\t\t\t      \"Please ensure it is set, for example by running something like: \"\n+\t\t\t      \"'git config submodule.%s.gitdir .git/modules/%s'. For details \"\n+\t\t\t      \"see the extensions.submodulePathConfig documentation.\"),\n+\t\t\t    submodule_name, submodule_name, submodule_name, submodule_name);\n+\n+\t\tstrbuf_addstr(buf, gitdir);\n+\t}\n+\n+\t/* validate because users might have modified the config */\n+\tif (validate_submodule_git_dir(buf->buf, submodule_name)) {\n+\t\tadvise(_(\"enabling extensions.submodulePathConfig might fix the \"\n+\t\t\t \"following error, if it's not already enabled.\"));\n \t\tdie(_(\"refusing to create/use '%s' in another submodule's \"\n-\t\t      \"git dir\"), buf->buf);\n+\t\t      \" git dir.\"), buf->buf);\n+\t}\n }\ndiff --git a/t/lib-verify-submodule-gitdir-path.sh b/t/lib-verify-submodule-gitdir-path.sh\nnew file mode 100644\nindex 0000000000..4e0cfdc605\n--- /dev/null\n+++ b/t/lib-verify-submodule-gitdir-path.sh\n@@ -0,0 +1,24 @@\n+# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3\n+\n+# This does not check filesystem existence. That is done in submodule.c via the\n+# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs\n+# might or might not exist (e.g. when adding a new submodule), so this only\n+# checks the expected configuration path, which might be overridden by the user.\n+\n+verify_submodule_gitdir_path () {\n+\trepo=\"$1\" &&\n+\tname=\"$2\" &&\n+\tpath=\"$3\" &&\n+\t(\n+\t\tcd \"$repo\" &&\n+\t\t# Compute expected absolute path\n+\t\texpected=\"$(git rev-parse --git-common-dir)/$path\" &&\n+\t\texpected=\"$(test-tool path-utils real_path \"$expected\")\" &&\n+\t\t# Compute actual absolute path\n+\t\tactual=\"$(git submodule--helper gitdir \"$name\")\" &&\n+\t\tactual=\"$(test-tool path-utils real_path \"$actual\")\" &&\n+\t\techo \"$expected\" >expect &&\n+\t\techo \"$actual\" >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+}\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..0b1de3251a 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -887,6 +887,7 @@ integration_tests = [\n   't7422-submodule-output.sh',\n   't7423-submodule-symlinks.sh',\n   't7424-submodule-mixed-ref-formats.sh',\n+  't7425-submodule-gitdir-path-extension.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nnew file mode 100755\nindex 0000000000..453183e27c\n--- /dev/null\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -0,0 +1,160 @@\n+#!/bin/sh\n+\n+test_description='submodulePathConfig extension works as expected'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-verify-submodule-gitdir-path.sh\n+\n+test_expect_success 'setup: allow file protocol' '\n+       git config --global protocol.file.allow always\n+'\n+\n+test_expect_success 'create repo with mixed extension submodules' '\n+\tgit init -b main legacy-sub &&\n+\ttest_commit -C legacy-sub legacy-initial &&\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\n+\tgit init -b main new-sub &&\n+\ttest_commit -C new-sub new-initial &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD) &&\n+\n+\tgit init -b main main &&\n+\t(\n+\t\tcd main &&\n+\t\tgit submodule add ../legacy-sub legacy &&\n+\t\ttest_commit legacy-sub &&\n+\n+\t\t# trigger the \"die_path_inside_submodule\" check\n+\t\ttest_must_fail git submodule add ../new-sub \"legacy/nested\" &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\tgit submodule add ../new-sub \"New Sub\" &&\n+\t\ttest_commit new &&\n+\n+\t\t# retrigger the \"die_path_inside_submodule\" check with encoding\n+\t\ttest_must_fail git submodule add ../new-sub \"New Sub/nested2\"\n+       )\n+'\n+\n+test_expect_success 'verify new submodule gitdir config' '\n+\tgit -C main config submodule.\"New Sub\".gitdir >actual &&\n+\techo \".git/modules/New Sub\" >expect &&\n+\ttest_cmp expect actual &&\n+\tverify_submodule_gitdir_path main \"New Sub\" \"modules/New Sub\"\n+'\n+\n+test_expect_success 'manual add and verify legacy submodule gitdir config' '\n+\t# the legacy module should not contain a gitdir config, because it\n+\t# was added before the extension was enabled. Add and test it.\n+\ttest_must_fail git -C main config submodule.legacy.gitdir &&\n+\tgit -C main config submodule.legacy.gitdir .git/modules/legacy &&\n+\tgit -C main config submodule.legacy.gitdir >actual &&\n+\techo \".git/modules/legacy\" >expect &&\n+\ttest_cmp expect actual &&\n+\tverify_submodule_gitdir_path main \"legacy\" \"modules/legacy\"\n+'\n+\n+test_expect_success 'gitdir config path is relative for both absolute and relative urls' '\n+\ttest_when_finished \"rm -rf relative-cfg-path-test\" &&\n+\tgit init -b main relative-cfg-path-test &&\n+\t(\n+\t\tcd relative-cfg-path-test &&\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\t# Test with absolute URL\n+\t\tgit submodule add \"$TRASH_DIRECTORY/new-sub\" sub-abs &&\n+\t\tgit config submodule.sub-abs.gitdir >actual &&\n+\t\techo \".git/modules/sub-abs\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# Test with relative URL\n+\t\tgit submodule add ../new-sub sub-rel &&\n+\t\tgit config submodule.sub-rel.gitdir >actual &&\n+\t\techo \".git/modules/sub-rel\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'clone from repo with both legacy and new-style submodules' '\n+\tgit clone --recurse-submodules main cloned-non-extension &&\n+\t(\n+\t\tcd cloned-non-extension &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir .git/modules/\"New Sub\" &&\n+\n+\t\ttest_must_fail git config submodule.legacy.gitdir &&\n+\t\ttest_must_fail git config submodule.\"New Sub\".gitdir &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t) &&\n+\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules main cloned-extension &&\n+\t(\n+\t\tcd cloned-extension &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\tgit config submodule.legacy.gitdir &&\n+\t\tgit config submodule.\"New Sub\".gitdir &&\n+\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_expect_success 'commit and push changes to encoded submodules' '\n+\tgit -C legacy-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C new-sub config receive.denyCurrentBranch updateInstead &&\n+\tgit -C main config receive.denyCurrentBranch updateInstead &&\n+\t(\n+\t\tcd cloned-extension &&\n+\n+\t\tgit -C legacy switch --track -C main origin/main  &&\n+\t\ttest_commit -C legacy second-commit &&\n+\t\tgit -C legacy push &&\n+\n+\t\tgit -C \"New Sub\" switch --track -C main origin/main &&\n+\t\ttest_commit -C \"New Sub\" second-commit &&\n+\t\tgit -C \"New Sub\" push &&\n+\n+\t\t# Stage and commit submodule changes in superproject\n+\t\tgit switch --track -C main origin/main  &&\n+\t\tgit add legacy \"New Sub\" &&\n+\t\tgit commit -m \"update submodules\" &&\n+\n+\t\t# push superproject commit to main repo\n+\t\tgit push\n+\t) &&\n+\n+\t# update expected legacy & new submodule checksums\n+\tlegacy_rev=$(git -C legacy-sub rev-parse HEAD) &&\n+\tnew_rev=$(git -C new-sub rev-parse HEAD)\n+'\n+\n+test_expect_success 'fetch mixed submodule changes and verify updates' '\n+\t(\n+\t\tcd main &&\n+\n+\t\t# only update submodules because superproject was\n+\t\t# pushed into at the end of last test\n+\t\tgit submodule update --init --recursive &&\n+\n+\t\ttest_path_is_dir .git/modules/legacy &&\n+\t\ttest_path_is_dir \".git/modules/New Sub\" &&\n+\n+\t\t# Verify both submodules are at the expected commits\n+\t\tgit submodule status >list &&\n+\t\ttest_grep \"$legacy_rev legacy\" list &&\n+\t\ttest_grep \"$new_rev New Sub\" list\n+\t)\n+'\n+\n+test_done\ndiff --git a/t/t9902-completion.sh b/t/t9902-completion.sh\nindex 964e1f1569..ffb9c8b522 100755\n--- a/t/t9902-completion.sh\n+++ b/t/t9902-completion.sh\n@@ -3053,6 +3053,7 @@ test_expect_success 'git config set - variable name - __git_compute_second_level\n \tsubmodule.sub.fetchRecurseSubmodules Z\n \tsubmodule.sub.ignore Z\n \tsubmodule.sub.active Z\n+\tsubmodule.sub.gitdir Z\n \tEOF\n '\n \n-- \n2.52.0\n\n"},{"id":"533667","messageId":"20260112184632.1334495-6-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 05/11] submodule: allow runtime enabling extensions.submodulePathConfig","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:26Z","receivedAt":"2026-01-12T18:54:39Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new config `init.defaultSubmodulePathConfig` which allows\nenabling `extensions.submodulePathConfig` for new submodules by\ndefault (those created via git init or clone).\n\nImportant: setting init.defaultSubmodulePathConfig = true does\nnot globally enable `extensions.submodulePathConfig`. Existing\nrepositories will still have the extension disabled and will\nrequire migration (for example via git submodule--helper command\nadded in the next commit).\n\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |   4 +\n Documentation/config/init.adoc             |   6 +\n setup.c                                    |  10 ++\n t/t7425-submodule-gitdir-path-extension.sh | 122 +++++++++++++++++++++\n 4 files changed, 142 insertions(+)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex f4f57c9114..e8d9d9a19a 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -95,6 +95,10 @@ Git will error out if a module does not have a corresponding\n Existing (pre-extension) submodules need to be migrated by adding the missing\n config entries. This is done manually for now, e.g. for each submodule:\n `git config submodule.<name>.gitdir .git/modules/<name>`.\n++\n+The extension can be enabled automatically for new repositories by setting\n+`init.defaultSubmodulePathConfig` to `true`, for example by running\n+`git config --global init.defaultSubmodulePathConfig true`.\n \n worktreeConfig:::\n \tIf enabled, then worktrees will load config settings from the\ndiff --git a/Documentation/config/init.adoc b/Documentation/config/init.adoc\nindex e45b2a8121..7b4abdaf8b 100644\n--- a/Documentation/config/init.adoc\n+++ b/Documentation/config/init.adoc\n@@ -18,3 +18,9 @@ endif::[]\n \tSee `--ref-format=` in linkgit:git-init[1]. Both the command line\n \toption and the `GIT_DEFAULT_REF_FORMAT` environment variable take\n \tprecedence over this config.\n+\n+init.defaultSubmodulePathConfig::\n+\tA boolean that specifies if `git init` and `git clone` should\n+\tautomatically set `extensions.submodulePathConfig` to `true`. This\n+\tallows all new repositories to automatically use the submodule path\n+\textension. Defaults to `false` when unset.\ndiff --git a/setup.c b/setup.c\nindex 428427d689..0378483b69 100644\n--- a/setup.c\n+++ b/setup.c\n@@ -2310,6 +2310,7 @@ void initialize_repository_version(int hash_algo,\n {\n \tstruct strbuf repo_version = STRBUF_INIT;\n \tint target_version = GIT_REPO_VERSION;\n+\tint default_submodule_path_config = 0;\n \n \t/*\n \t * Note that we initialize the repository version to 1 when the ref\n@@ -2348,6 +2349,15 @@ void initialize_repository_version(int hash_algo,\n \t\tclear_repository_format(&repo_fmt);\n \t}\n \n+\trepo_config_get_bool(the_repository, \"init.defaultSubmodulePathConfig\",\n+\t\t\t     &default_submodule_path_config);\n+\tif (default_submodule_path_config) {\n+\t\t/* extensions.submodulepathconfig requires at least version 1 */\n+\t\tif (target_version == 0)\n+\t\t\ttarget_version = 1;\n+\t\trepo_config_set(the_repository, \"extensions.submodulepathconfig\", \"true\");\n+\t}\n+\n \tstrbuf_addf(&repo_version, \"%d\", target_version);\n \trepo_config_set(the_repository, \"core.repositoryformatversion\", repo_version.buf);\n \ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 453183e27c..03ac165de9 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -157,4 +157,126 @@ test_expect_success 'fetch mixed submodule changes and verify updates' '\n \t)\n '\n \n+test_expect_success '`git init` respects init.defaultSubmodulePathConfig' '\n+\ttest_config_global init.defaultSubmodulePathConfig true &&\n+\tgit init repo-init &&\n+\tgit -C repo-init config extensions.submodulePathConfig >actual &&\n+\techo true >expect &&\n+\ttest_cmp expect actual &&\n+\t# create a submodule and check gitdir\n+\t(\n+\t\tcd repo-init &&\n+\t\tgit init -b main sub &&\n+\t\ttest_commit -C sub sub-initial &&\n+\t\tgit submodule add ./sub sub &&\n+\t\tgit config submodule.sub.gitdir >actual &&\n+\t\techo \".git/modules/sub\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '`git init` does not set extension by default' '\n+\tgit init upstream &&\n+\ttest_commit -C upstream initial &&\n+\ttest_must_fail git -C upstream config extensions.submodulePathConfig &&\n+\t# create a pair of submodules and check gitdir is not created\n+\tgit init -b main sub &&\n+\ttest_commit -C sub sub-initial &&\n+\t(\n+\t\tcd upstream &&\n+\t\tgit submodule add ../sub sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\tgit submodule add ../sub sub2 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\tgit commit -m \"Add submodules\"\n+\t)\n+'\n+\n+test_expect_success '`git clone` does not set extension by default' '\n+\ttest_when_finished \"rm -rf repo-clone-no-ext\" &&\n+\tgit clone upstream repo-clone-no-ext &&\n+\t(\n+\t\tcd repo-clone-no-ext &&\n+\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\t\ttest_path_is_missing .git/modules/sub1 &&\n+\t\ttest_path_is_missing .git/modules/sub2 &&\n+\n+\t\t# create a submodule and check gitdir is not created\n+\t\tgit submodule add ../sub sub3 &&\n+\t\ttest_must_fail git config submodule.sub3.gitdir\n+\t)\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` does not set extension by default' '\n+\ttest_when_finished \"rm -rf repo-clone-no-ext\" &&\n+\tgit clone --recurse-submodules upstream repo-clone-no-ext &&\n+\t(\n+\t\tcd repo-clone-no-ext &&\n+\n+\t\t# verify that that submodules do not have gitdir set\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\n+\t\t# create another submodule and check that gitdir is not created\n+\t\tgit submodule add ../sub sub3 &&\n+\t\ttest_path_is_dir .git/modules/sub3 &&\n+\t\ttest_must_fail git config submodule.sub3.gitdir\n+\t)\n+\n+'\n+\n+test_expect_success '`git clone` respects init.defaultSubmodulePathConfig' '\n+\ttest_when_finished \"rm -rf repo-clone\" &&\n+\ttest_config_global init.defaultSubmodulePathConfig true &&\n+\tgit clone upstream repo-clone &&\n+\t(\n+\t\tcd repo-clone &&\n+\n+\t\t# verify new repo extension is inherited from global config\n+\t\tgit config extensions.submodulePathConfig >actual &&\n+\t\techo true >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# new submodule has a gitdir config\n+\t\tgit submodule add ../sub sub &&\n+\t\ttest_path_is_dir .git/modules/sub &&\n+\t\tgit config submodule.sub.gitdir >actual &&\n+\t\techo \".git/modules/sub\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmodulePathConfig' '\n+\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n+\ttest_config_global init.defaultSubmodulePathConfig true &&\n+\tgit clone  --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# verify new repo extension is inherited from global config\n+\t\tgit config extensions.submodulePathConfig >actual &&\n+\t\techo true >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# previous submodules should exist\n+\t\tgit config submodule.sub1.gitdir &&\n+\t\tgit config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\n+\t\t# create another submodule and check that gitdir is created\n+\t\tgit submodule add ../sub new-sub &&\n+\t\ttest_path_is_dir .git/modules/new-sub &&\n+\t\tgit config submodule.new-sub.gitdir >actual &&\n+\t\techo \".git/modules/new-sub\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\n-- \n2.52.0\n\n"},{"id":"533668","messageId":"20260112184632.1334495-7-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 06/11] submodule--helper: add gitdir migration command","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:27Z","receivedAt":"2026-01-12T18:54:43Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Manually running\n\"git config submodule.<name>.gitdir .git/modules/<name>\"\nfor each submodule can be impractical, so add a migration command to\nsubmodule--helper to automatically create configs for all submodules\nas required by extensions.submodulePathConfig.\n\nThe command calls create_default_gitdir_config() which validates the\ngitdir paths before adding the configs.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n Documentation/config/extensions.adoc       |  6 +-\n builtin/submodule--helper.c                | 61 ++++++++++++++++++++\n t/t7425-submodule-gitdir-path-extension.sh | 67 ++++++++++++++++++++++\n 3 files changed, 132 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/extensions.adoc b/Documentation/config/extensions.adoc\nindex e8d9d9a19a..2aef3315b1 100644\n--- a/Documentation/config/extensions.adoc\n+++ b/Documentation/config/extensions.adoc\n@@ -93,8 +93,10 @@ Git will error out if a module does not have a corresponding\n `submodule.<name>.gitdir` set.\n +\n Existing (pre-extension) submodules need to be migrated by adding the missing\n-config entries. This is done manually for now, e.g. for each submodule:\n-`git config submodule.<name>.gitdir .git/modules/<name>`.\n+config entries. This can be done manually, e.g. for each submodule:\n+`git config submodule.<name>.gitdir .git/modules/<name>`, or via the\n+`git submodule--helper migrate-gitdir-configs` command which iterates over all\n+submodules and attempts to migrate them.\n +\n The extension can be enabled automatically for new repositories by setting\n `init.defaultSubmodulePathConfig` to `true`, for example by running\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex b3d6f9ff68..498c0ca770 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -1266,6 +1266,66 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,\n \treturn 0;\n }\n \n+static int module_migrate(int argc UNUSED, const char **argv UNUSED,\n+\t\t\t  const char *prefix UNUSED, struct repository *repo)\n+{\n+\tstruct strbuf module_dir = STRBUF_INIT;\n+\tDIR *dir;\n+\tstruct dirent *de;\n+\tint repo_version = 0;\n+\n+\trepo_git_path_append(repo, &module_dir, \"modules/\");\n+\n+\tdir = opendir(module_dir.buf);\n+\tif (!dir)\n+\t\tdie(_(\"could not open '%s'\"), module_dir.buf);\n+\n+\twhile ((de = readdir(dir))) {\n+\t\tstruct strbuf gitdir_path = STRBUF_INIT;\n+\t\tchar *key;\n+\t\tconst char *value;\n+\n+\t\tif (is_dot_or_dotdot(de->d_name))\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_addf(&gitdir_path, \"%s/%s\", module_dir.buf, de->d_name);\n+\t\tif (!is_git_directory(gitdir_path.buf)) {\n+\t\t\tstrbuf_release(&gitdir_path);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tstrbuf_release(&gitdir_path);\n+\n+\t\tkey = xstrfmt(\"submodule.%s.gitdir\", de->d_name);\n+\t\tif (!repo_config_get_string_tmp(repo, key, &value)) {\n+\t\t\t/* Already has a gitdir config, nothing to do. */\n+\t\t\tfree(key);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tfree(key);\n+\n+\t\tcreate_default_gitdir_config(de->d_name);\n+\t}\n+\n+\tclosedir(dir);\n+\tstrbuf_release(&module_dir);\n+\n+\trepo_config_get_int(the_repository, \"core.repositoryformatversion\", &repo_version);\n+\tif (repo_version == 0 &&\n+\t    repo_config_set_gently(repo, \"core.repositoryformatversion\", \"1\"))\n+\t\tdie(_(\"could not set core.repositoryformatversion to 1.\\n\"\n+\t\t      \"Please set it for migration to work, for example:\\n\"\n+\t\t      \"git config core.repositoryformatversion 1\"));\n+\n+\tif (repo_config_set_gently(repo, \"extensions.submodulePathConfig\", \"true\"))\n+\t\tdie(_(\"could not enable submodulePathConfig extension. It is required\\n\"\n+\t\t      \"for migration to work. Please enable it in the root repo:\\n\"\n+\t\t      \"git config extensions.submodulePathConfig true\"));\n+\n+\trepo->repository_format_submodule_path_cfg = 1;\n+\n+\treturn 0;\n+}\n+\n struct sync_cb {\n \tconst char *prefix;\n \tconst char *super_prefix;\n@@ -3657,6 +3717,7 @@ int cmd_submodule__helper(int argc,\n \t\tNULL\n \t};\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"migrate-gitdir-configs\", &fn, module_migrate),\n \t\tOPT_SUBCOMMAND(\"gitdir\", &fn, module_gitdir),\n \t\tOPT_SUBCOMMAND(\"clone\", &fn, module_clone),\n \t\tOPT_SUBCOMMAND(\"add\", &fn, module_add),\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 03ac165de9..89e2feab8b 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -279,4 +279,71 @@ test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmo\n \t)\n '\n \n+test_expect_success 'submodule--helper migrates legacy modules' '\n+\t(\n+\t\tcd upstream &&\n+\n+\t\t# previous submodules exist and were not migrated yet\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2 &&\n+\n+\t\t# run migration\n+\t\tgit submodule--helper migrate-gitdir-configs &&\n+\n+\t\t# test that migration worked\n+\t\tgit config submodule.sub1.gitdir >actual &&\n+\t\techo \".git/modules/sub1\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit config submodule.sub2.gitdir >actual &&\n+\t\techo \".git/modules/sub2\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# repository extension is enabled after migration\n+\t\tgit config extensions.submodulePathConfig >actual &&\n+\t\techo \"true\" >expect &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '`git clone --recurse-submodules` works after migration' '\n+\ttest_when_finished \"rm -rf repo-clone-recursive\" &&\n+\n+\t# test with extension disabled after the upstream repo was migrated\n+\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# init.defaultSubmodulePathConfig was disabled before clone, so\n+\t\t# the repo extension config should also be off, the migration ignored\n+\t\ttest_must_fail git config extensions.submodulePathConfig &&\n+\n+\t\t# modules should look like there was no migration done\n+\t\ttest_must_fail git config submodule.sub1.gitdir &&\n+\t\ttest_must_fail git config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2\n+\t) &&\n+\trm -rf repo-clone-recursive &&\n+\n+\t# enable the extension, then retry the clone\n+\ttest_config_global init.defaultSubmodulePathConfig true &&\n+\tgit clone --recurse-submodules upstream repo-clone-recursive &&\n+\t(\n+\t\tcd repo-clone-recursive &&\n+\n+\t\t# repository extension is enabled\n+\t\tgit config extensions.submodulePathConfig >actual &&\n+\t\techo \"true\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# gitdir configs exist for submodules\n+\t\tgit config submodule.sub1.gitdir &&\n+\t\tgit config submodule.sub2.gitdir &&\n+\t\ttest_path_is_dir .git/modules/sub1 &&\n+\t\ttest_path_is_dir .git/modules/sub2\n+\t)\n+'\n+\n test_done\n-- \n2.52.0\n\n"},{"id":"533669","messageId":"20260112184632.1334495-8-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 07/11] builtin/credential-store: move is_rfc3986_unreserved to url.[ch]","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:28Z","receivedAt":"2026-01-12T18:54:43Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"is_rfc3986_unreserved() was moved to credential-store.c and was made\nstatic by f89854362c (credential-store: move related functions to\ncredential-store file, 2023-06-06) under a correct assumption, at the\ntime, that it was the only place using it.\n\nHowever now we need it to apply URL-encoding to submodule names when\nconstructing gitdir paths, to avoid conflicts, so bring it back as a\npublic function exposed via url.h, instead of the old helper path\n(strbuf), which has nothing to do with 3986 encoding/decoding anymore.\n\nThis function will be used in subsequent commits which do the encoding.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/credential-store.c | 7 +------\n url.c                      | 6 ++++++\n url.h                      | 7 +++++++\n 3 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/credential-store.c b/builtin/credential-store.c\nindex b74e06cc93..bc1453c6b2 100644\n--- a/builtin/credential-store.c\n+++ b/builtin/credential-store.c\n@@ -7,6 +7,7 @@\n #include \"path.h\"\n #include \"string-list.h\"\n #include \"parse-options.h\"\n+#include \"url.h\"\n #include \"write-or-die.h\"\n \n static struct lock_file credential_lock;\n@@ -76,12 +77,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,\n \t\tdie_errno(\"unable to write credential store\");\n }\n \n-static int is_rfc3986_unreserved(char ch)\n-{\n-\treturn isalnum(ch) ||\n-\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n-}\n-\n static int is_rfc3986_reserved_or_unreserved(char ch)\n {\n \tif (is_rfc3986_unreserved(ch))\ndiff --git a/url.c b/url.c\nindex 282b12495a..adc289229c 100644\n--- a/url.c\n+++ b/url.c\n@@ -3,6 +3,12 @@\n #include \"strbuf.h\"\n #include \"url.h\"\n \n+int is_rfc3986_unreserved(char ch)\n+{\n+\treturn isalnum(ch) ||\n+\t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex 2a27c34277..e644c3c809 100644\n--- a/url.h\n+++ b/url.h\n@@ -21,4 +21,11 @@ char *url_decode_parameter_value(const char **query);\n void end_url_with_slash(struct strbuf *buf, const char *url);\n void str_end_url_with_slash(const char *url, char **dest);\n \n+/*\n+ * The set of unreserved characters as per STD66 (RFC3986) is\n+ * '[A-Za-z0-9-._~]'. These characters are safe to appear in URI\n+ * components without percent-encoding.\n+ */\n+int is_rfc3986_unreserved(char ch);\n+\n #endif /* URL_H */\n-- \n2.52.0\n\n"},{"id":"533670","messageId":"20260112184632.1334495-9-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 08/11] submodule--helper: fix filesystem collisions by encoding gitdir paths","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:29Z","receivedAt":"2026-01-12T18:54:45Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Fix nested filesystem collisions by url-encoding gitdir paths stored\nin submodule.%s.gitdir, when extensions.submodulePathConfig is enabled.\n\nCredit goes to Junio and Patrick for coming up with this design: the\nencoding is only applied when necessary, to newly added submodules.\n\nExisting modules don't need the encoding because git already errors\nout when detecting nested gitdirs before this patch.\n\nThis commit adds the basic url-encoding and some tests. Next commits\nextend the encode -> validate -> retry loop to fix more conflicts.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSuggested-by: Patrick Steinhardt <ps@pks.im>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 12 +++++\n submodule.c                                | 42 +++++++++++++++-\n t/t7425-submodule-gitdir-path-extension.sh | 57 ++++++++++++++++++++++\n 3 files changed, 110 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 498c0ca770..2c61810644 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -34,6 +34,7 @@\n #include \"list-objects-filter-options.h\"\n #include \"wildmatch.h\"\n #include \"strbuf.h\"\n+#include \"url.h\"\n \n #define OPT_QUIET (1 << 0)\n #define OPT_CACHED (1 << 1)\n@@ -465,12 +466,23 @@ static void create_default_gitdir_config(const char *submodule_name)\n {\n \tstruct strbuf gitdir_path = STRBUF_INIT;\n \n+\t/* Case 1: try the plain module name */\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n \tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n \t\tstrbuf_release(&gitdir_path);\n \t\treturn;\n \t}\n \n+\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n+\t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\n \t      \"'git config submodule.%s.gitdir .git/modules/%s'\"),\ndiff --git a/submodule.c b/submodule.c\nindex e0cd6f5dcc..9dc0938340 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -32,6 +32,7 @@\n #include \"read-cache-ll.h\"\n #include \"setup.h\"\n #include \"advice.h\"\n+#include \"url.h\"\n \n static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;\n static int initialized_fetch_ref_tips;\n@@ -2247,12 +2248,43 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n-int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+/*\n+ * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n+ * This does not print errors like the non-encoded version, because encoding is supposed\n+ * to mitigate / fix all these.\n+ */\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+{\n+\tconst char *modules_marker = \"/modules/\";\n+\tchar *p = git_dir, *last_submodule_name = NULL;\n+\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n+\t\t    \"extensions.submodulePathConfig enabled.\");\n+\n+\t/* Find the last submodule name in the gitdir path (modules can be nested). */\n+\twhile ((p = strstr(p, modules_marker))) {\n+\t\tlast_submodule_name = p + strlen(modules_marker);\n+\t\tp++;\n+\t}\n+\n+\t/* Prevent the use of '/' in encoded names */\n+\tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n+\t\treturn -1;\n+\n+\treturn 0;\n+}\n+\n+static int validate_submodule_legacy_git_dir(char *git_dir, const char *submodule_name)\n {\n \tsize_t len = strlen(git_dir), suffix_len = strlen(submodule_name);\n \tchar *p;\n \tint ret = 0;\n \n+\tif (the_repository->repository_format_submodule_path_cfg)\n+\t\tBUG(\"validate_submodule_git_dir() must be called with \"\n+\t\t    \"extensions.submodulePathConfig disabled.\");\n+\n \tif (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||\n \t    strcmp(p, submodule_name))\n \t\tBUG(\"submodule name '%s' not a suffix of git dir '%s'\",\n@@ -2288,6 +2320,14 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n \treturn 0;\n }\n \n+int validate_submodule_git_dir(char *git_dir, const char *submodule_name)\n+{\n+\tif (!the_repository->repository_format_submodule_path_cfg)\n+\t\treturn validate_submodule_legacy_git_dir(git_dir, submodule_name);\n+\n+\treturn validate_submodule_encoded_git_dir(git_dir, submodule_name);\n+}\n+\n int validate_submodule_path(const char *path)\n {\n \tchar *p = xstrdup(path);\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 89e2feab8b..ce1428a2ff 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -346,4 +346,61 @@ test_expect_success '`git clone --recurse-submodules` works after migration' '\n \t)\n '\n \n+test_expect_success 'setup submodules with nested git dirs' '\n+\tgit init nested &&\n+\ttest_commit -C nested nested &&\n+\t(\n+\t\tcd nested &&\n+\t\tcat >.gitmodules <<-EOF &&\n+\t\t[submodule \"hippo\"]\n+\t\t\turl = .\n+\t\t\tpath = thing1\n+\t\t[submodule \"hippo/hooks\"]\n+\t\t\turl = .\n+\t\t\tpath = thing2\n+\t\tEOF\n+\t\tgit clone . thing1 &&\n+\t\tgit clone . thing2 &&\n+\t\tgit add .gitmodules thing1 thing2 &&\n+\t\ttest_tick &&\n+\t\tgit commit -m nested\n+\t)\n+'\n+\n+test_expect_success 'git dirs of encoded sibling submodules must not be nested' '\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules nested clone_nested &&\n+\n+\tverify_submodule_gitdir_path clone_nested hippo modules/hippo &&\n+\tgit -C clone_nested config submodule.hippo.gitdir >actual &&\n+\ttest_grep \"\\.git/modules/hippo$\" actual &&\n+\n+\tverify_submodule_gitdir_path clone_nested hippo/hooks modules/hippo%2fhooks &&\n+\tgit -C clone_nested config submodule.hippo/hooks.gitdir >actual &&\n+\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n+'\n+\n+test_expect_success 'submodule git dir nesting detection must work with parallel cloning' '\n+\tgit clone -c extensions.submodulePathConfig=true --recurse-submodules --jobs=2 nested clone_parallel &&\n+\n+\tverify_submodule_gitdir_path clone_parallel hippo modules/hippo &&\n+\tgit -C clone_nested config submodule.hippo.gitdir >actual &&\n+\ttest_grep \"\\.git/modules/hippo$\" actual &&\n+\n+\tverify_submodule_gitdir_path clone_parallel hippo/hooks modules/hippo%2fhooks &&\n+\tgit -C clone_nested config submodule.hippo/hooks.gitdir >actual &&\n+\ttest_grep \"\\.git/modules/hippo%2fhooks$\" actual\n+'\n+\n+test_expect_success 'disabling extensions.submodulePathConfig prevents nested submodules' '\n+\t(\n+\t\tcd clone_nested &&\n+\t\t# disable extension and verify failure\n+\t\tgit config --replace-all extensions.submodulePathConfig false &&\n+\t\ttest_must_fail git submodule add ./thing2 hippo/foobar &&\n+\t\t# re-enable extension and verify it works\n+\t\tgit config --replace-all extensions.submodulePathConfig true &&\n+\t\tgit submodule add ./thing2 hippo/foobar\n+\t)\n+'\n+\n test_done\n-- \n2.52.0\n\n"},{"id":"533671","messageId":"20260112184632.1334495-10-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 09/11] submodule: fix case-folding gitdir filesystem collisions","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:30Z","receivedAt":"2026-01-12T18:54:49Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Add a new check when extension.submodulePathConfig is enabled, to\ndetect and prevent case-folding filesystem colisions. When this\nnew check is triggered, a stricter casefolding aware URI encoding\nis used to percent-encode uppercase characters.\n\nBy using this check/retry mechanism the uppercase encoding is\nonly applied when necessary, so case-sensitive filesystems are\nnot affected.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 26 ++++++++++-\n submodule.c                                | 53 +++++++++++++++++++++-\n t/t7425-submodule-gitdir-path-extension.sh | 35 ++++++++++++++\n url.c                                      |  7 +++\n url.h                                      |  7 +++\n 5 files changed, 126 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 2c61810644..3d5a81201e 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -473,7 +473,7 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\treturn;\n \t}\n \n-\t/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n+\t/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */\n \tstrbuf_reset(&gitdir_path);\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n \tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n@@ -482,6 +482,30 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\treturn;\n \t}\n \n+\t/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\treturn;\n+\n+\t/* Case 2.3: Try some derived gitdir names, see if one sticks */\n+\tfor (char c = '0'; c <= '9'; c++) {\n+\t\tstrbuf_reset(&gitdir_path);\n+\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);\n+\t\tstrbuf_addch(&gitdir_path, c);\n+\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\t\treturn;\n+\n+\t\tstrbuf_reset(&gitdir_path);\n+\t\trepo_git_path_append(the_repository, &gitdir_path, \"modules/\");\n+\t\tstrbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);\n+\t\tstrbuf_addch(&gitdir_path, c);\n+\t\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))\n+\t\t\treturn;\n+\t}\n+\n \t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\ndiff --git a/submodule.c b/submodule.c\nindex 9dc0938340..2fb0f404fd 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2248,15 +2248,58 @@ int submodule_move_head(const char *path, const char *super_prefix,\n \treturn ret;\n }\n \n+static int check_casefolding_conflict(const char *git_dir,\n+\t\t\t\t      const char *submodule_name,\n+\t\t\t\t      const bool suffixes_match)\n+{\n+\tchar *p, *modules_dir = xstrdup(git_dir);\n+\tstruct dirent *de;\n+\tDIR *dir = NULL;\n+\tint ret = 0;\n+\n+\tif ((p = find_last_dir_sep(modules_dir)))\n+\t\t*p = '\\0';\n+\n+\t/* No conflict is possible if modules_dir doesn't exist (first clone) */\n+\tif (!is_directory(modules_dir))\n+\t\tgoto cleanup;\n+\n+\tdir = opendir(modules_dir);\n+\tif (!dir) {\n+\t\tret = -1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\t/* Check for another directory under .git/modules that differs only in case. */\n+\twhile ((de = readdir(dir))) {\n+\t\tif (!strcmp(de->d_name, \".\") || !strcmp(de->d_name, \"..\"))\n+\t\t\tcontinue;\n+\n+\t\tif ((suffixes_match || is_git_directory(git_dir)) &&\n+\t\t    !strcasecmp(de->d_name, submodule_name) &&\n+\t\t    strcmp(de->d_name, submodule_name)) {\n+\t\t\tret = -1; /* collision found */\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+cleanup:\n+\tif (dir)\n+\t\tclosedir(dir);\n+\tfree(modules_dir);\n+\treturn ret;\n+}\n+\n /*\n  * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n  * This does not print errors like the non-encoded version, because encoding is supposed\n  * to mitigate / fix all these.\n  */\n-static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name UNUSED)\n+static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodule_name)\n {\n \tconst char *modules_marker = \"/modules/\";\n \tchar *p = git_dir, *last_submodule_name = NULL;\n+\tint config_ignorecase = 0;\n \n \tif (!the_repository->repository_format_submodule_path_cfg)\n \t\tBUG(\"validate_submodule_encoded_git_dir() must be called with \"\n@@ -2272,6 +2315,14 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n \t\treturn -1;\n \n+\t/* Prevent conflicts on case-folding filesystems */\n+\trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n+\tif (ignore_case || config_ignorecase) {\n+\t\tbool suffixes_match = !strcmp(last_submodule_name, submodule_name);\n+\t\treturn check_casefolding_conflict(git_dir, submodule_name,\n+\t\t\t\t\t\t  suffixes_match);\n+\t}\n+\n \treturn 0;\n }\n \ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex ce1428a2ff..3cca93c897 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -403,4 +403,39 @@ test_expect_success 'disabling extensions.submodulePathConfig prevents nested su\n \t)\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are correctly encoded' '\n+\tgit clone -c extensions.submodulePathConfig=true main cloned-folding &&\n+\t(\n+\t\tcd cloned-folding &&\n+\n+\t\t# conflict: the \"folding\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"folding\" &&\n+\t\ttest_commit lowercase &&\n+\t\tgit submodule add ../new-sub \"FoldinG\" &&\n+\t\ttest_commit uppercase &&\n+\n+\t\t# conflict: the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"FOO\" &&\n+\t\ttest_commit uppercase-foo &&\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\ttest_commit lowercase-foo &&\n+\n+\t\t# create a multi conflict between foobar, fooBar and foo%42ar\n+\t\t# the \"foo\" gitdir will already be taken\n+\t\tgit submodule add ../new-sub \"foobar\" &&\n+\t\ttest_commit lowercase-foobar &&\n+\t\tgit submodule add ../new-sub \"foo%42ar\" &&\n+\t\ttest_commit encoded-foo%42ar &&\n+\t\tgit submodule add ../new-sub \"fooBar\" &&\n+\t\ttest_commit mixed-fooBar\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-folding \"folding\" \"modules/folding\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FoldinG\" \"modules/%46oldin%47\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"FOO\" \"modules/FOO\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foobar\" \"modules/foobar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"foo%42ar\" \"modules/foo%42ar\" &&\n+\tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n+'\n+\n test_done\ndiff --git a/url.c b/url.c\nindex adc289229c..3ca5987e90 100644\n--- a/url.c\n+++ b/url.c\n@@ -9,6 +9,13 @@ int is_rfc3986_unreserved(char ch)\n \t\tch == '-' || ch == '_' || ch == '.' || ch == '~';\n }\n \n+int is_casefolding_rfc3986_unreserved(char c)\n+{\n+\treturn (c >= 'a' && c <= 'z') ||\n+\t       (c >= '0' && c <= '9') ||\n+\t       c == '-' || c == '.' || c == '_' || c == '~';\n+}\n+\n int is_urlschemechar(int first_flag, int ch)\n {\n \t/*\ndiff --git a/url.h b/url.h\nindex e644c3c809..cd9140e994 100644\n--- a/url.h\n+++ b/url.h\n@@ -28,4 +28,11 @@ void str_end_url_with_slash(const char *url, char **dest);\n  */\n int is_rfc3986_unreserved(char ch);\n \n+/*\n+ * This is a variant of is_rfc3986_unreserved() that treats uppercase\n+ * letters as \"reserved\". This forces them to be percent-encoded, allowing\n+ * 'Foo' (%46oo) and 'foo' (foo) to be distinct on case-folding filesystems.\n+ */\n+int is_casefolding_rfc3986_unreserved(char c);\n+\n #endif /* URL_H */\n-- \n2.52.0\n\n"},{"id":"533672","messageId":"20260112184632.1334495-11-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 10/11] submodule: hash the submodule name for the gitdir path","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:31Z","receivedAt":"2026-01-12T18:54:52Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"If none of the previous plain-text / encoding / derivation steps work\nand case 2.4 is reached, then try a hash of the submodule name to see\nif that can be a valid gitdir before giving up and throwing an error.\n\nThis is a \"last resort\" type of measure to avoid conflicts since it\nloses the human readability of the gitdir path. This logic will be\nreached in rare cases, as can be seen in the test we added.\n\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n builtin/submodule--helper.c                | 19 +++++++\n t/t7425-submodule-gitdir-path-extension.sh | 59 ++++++++++++++++++++++\n 2 files changed, 78 insertions(+)\n\ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 3d5a81201e..b621d14275 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -465,6 +465,10 @@ static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,\n static void create_default_gitdir_config(const char *submodule_name)\n {\n \tstruct strbuf gitdir_path = STRBUF_INIT;\n+\tstruct git_hash_ctx ctx;\n+\tchar hex_name_hash[GIT_MAX_HEXSZ + 1], header[128];\n+\tunsigned char raw_name_hash[GIT_MAX_RAWSZ];\n+\tint header_len;\n \n \t/* Case 1: try the plain module name */\n \trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", submodule_name);\n@@ -506,6 +510,21 @@ static void create_default_gitdir_config(const char *submodule_name)\n \t\t\treturn;\n \t}\n \n+\t/* Case 2.4: If all the above failed, try a hash of the name as a last resort */\n+\theader_len = snprintf(header, sizeof(header), \"blob %zu\", strlen(submodule_name));\n+\tthe_hash_algo->init_fn(&ctx);\n+\tthe_hash_algo->update_fn(&ctx, header, header_len);\n+\tthe_hash_algo->update_fn(&ctx, \"\\0\", 1);\n+\tthe_hash_algo->update_fn(&ctx, submodule_name, strlen(submodule_name));\n+\tthe_hash_algo->final_fn(raw_name_hash, &ctx);\n+\thash_to_hex_algop_r(hex_name_hash, raw_name_hash, the_hash_algo);\n+\tstrbuf_reset(&gitdir_path);\n+\trepo_git_path_append(the_repository, &gitdir_path, \"modules/%s\", hex_name_hash);\n+\tif (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {\n+\t\tstrbuf_release(&gitdir_path);\n+\t\treturn;\n+\t}\n+\n \t/* Case 3: nothing worked, error out */\n \tdie(_(\"failed to set a valid default config for 'submodule.%s.gitdir'. \"\n \t      \"Please ensure it is set, for example by running something like: \"\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex 3cca93c897..a76e64a9f7 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -438,4 +438,63 @@ test_expect_success CASE_INSENSITIVE_FS 'verify case-folding conflicts are corre\n \tverify_submodule_gitdir_path cloned-folding \"fooBar\" \"modules/fooBar0\"\n '\n \n+test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a last resort' '\n+\tgit clone -c extensions.submodulePathConfig=true main cloned-hash &&\n+\t(\n+\t\tcd cloned-hash &&\n+\n+\t\t# conflict: add all submodule conflicting variants until we reach the\n+\t\t# final hashing conflict resolution for submodule \"foo\"\n+\t\tgit submodule add ../new-sub \"foo\" &&\n+\t\tgit submodule add ../new-sub \"foo0\" &&\n+\t\tgit submodule add ../new-sub \"foo1\" &&\n+\t\tgit submodule add ../new-sub \"foo2\" &&\n+\t\tgit submodule add ../new-sub \"foo3\" &&\n+\t\tgit submodule add ../new-sub \"foo4\" &&\n+\t\tgit submodule add ../new-sub \"foo5\" &&\n+\t\tgit submodule add ../new-sub \"foo6\" &&\n+\t\tgit submodule add ../new-sub \"foo7\" &&\n+\t\tgit submodule add ../new-sub \"foo8\" &&\n+\t\tgit submodule add ../new-sub \"foo9\" &&\n+\t\tgit submodule add ../new-sub \"%46oo\" &&\n+\t\tgit submodule add ../new-sub \"%46oo0\" &&\n+\t\tgit submodule add ../new-sub \"%46oo1\" &&\n+\t\tgit submodule add ../new-sub \"%46oo2\" &&\n+\t\tgit submodule add ../new-sub \"%46oo3\" &&\n+\t\tgit submodule add ../new-sub \"%46oo4\" &&\n+\t\tgit submodule add ../new-sub \"%46oo5\" &&\n+\t\tgit submodule add ../new-sub \"%46oo6\" &&\n+\t\tgit submodule add ../new-sub \"%46oo7\" &&\n+\t\tgit submodule add ../new-sub \"%46oo8\" &&\n+\t\tgit submodule add ../new-sub \"%46oo9\" &&\n+\t\ttest_commit add-foo-variants &&\n+\t\tgit submodule add ../new-sub \"Foo\" &&\n+\t\ttest_commit add-uppercase-foo\n+\t) &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo\" \"modules/foo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo0\" \"modules/foo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo1\" \"modules/foo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo2\" \"modules/foo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo3\" \"modules/foo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo4\" \"modules/foo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo5\" \"modules/foo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo6\" \"modules/foo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo7\" \"modules/foo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo8\" \"modules/foo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"foo9\" \"modules/foo9\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo\" \"modules/%46oo\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo0\" \"modules/%46oo0\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo1\" \"modules/%46oo1\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo2\" \"modules/%46oo2\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo3\" \"modules/%46oo3\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo4\" \"modules/%46oo4\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo5\" \"modules/%46oo5\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo6\" \"modules/%46oo6\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo7\" \"modules/%46oo7\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo8\" \"modules/%46oo8\" &&\n+\tverify_submodule_gitdir_path cloned-hash \"%46oo9\" \"modules/%46oo9\" &&\n+\thash=$(printf \"Foo\" | git hash-object --stdin) &&\n+\tverify_submodule_gitdir_path cloned-hash \"Foo\" \"modules/${hash}\"\n+'\n+\n test_done\n-- \n2.52.0\n\n"},{"id":"533673","messageId":"20260112184632.1334495-12-adrian.ratiu@collabora.com","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"[PATCH v9 11/11] submodule: detect conflicts with existing gitdir configs","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T18:46:32Z","receivedAt":"2026-01-12T18:54:53Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"Credit goes to Emily and Josh for testing and noticing a corner-case\nwhich caused conflicts with existing gitdir configs to silently pass\nvalidation, then fail later in add_submodule() with a cryptic error:\n\nfatal: A git directory for 'nested%2fsub' is found locally with remote(s):\n  origin\t/.../trash directory.t7425-submodule-gitdir-path-extension/sub\n\nThis change ensures the validation step checks existing gitdirs for\nconflicts. We only have to do this for submodules having gitdirs,\nbecause those without submodule.%s.gitdir need to be migrated and\nwill throw an error earlier in the submodule codepath.\n\nQuoting Josh:\n My testing setup has been as follows:\n * Using our locally-built Git with our downstream patch of [1] included:\n   * create a repo \"sub\"\n   * create a repo \"super\"\n   * In \"super\":\n     * mkdir nested\n     * git submodule add ../sub nested/sub\n     * Verify that the submodule's gitdir is .git/modules/nested%2fsub\n * Using a build of git from upstream `next` plus this series:\n   * git config set --global extensions.submodulepathconfig true\n   * git clone --recurse-submodules super super2\n   * create a repo \"nested%2fsub\"\n   * In \"super2\":\n     * git submodule add ../nested%2fsub\n\nAt this point I'd expect the collision detection / encoding to take\neffect, but instead I get the error listed above.\nEnd quote\n\nSuggested-by: Josh Steadmon <steadmon@google.com>\nSigned-off-by: Adrian Ratiu <adrian.ratiu@collabora.com>\n---\n submodule.c                                | 61 ++++++++++++++++++++++\n t/t7425-submodule-gitdir-path-extension.sh | 28 ++++++++++\n 2 files changed, 89 insertions(+)\n\ndiff --git a/submodule.c b/submodule.c\nindex 2fb0f404fd..8c17da6a5a 100644\n--- a/submodule.c\n+++ b/submodule.c\n@@ -2290,6 +2290,62 @@ static int check_casefolding_conflict(const char *git_dir,\n \treturn ret;\n }\n \n+struct submodule_from_gitdir_cb {\n+\tconst char *gitdir;\n+\tconst char *submodule_name;\n+\tbool conflict_found;\n+};\n+\n+static int find_conflict_by_gitdir_cb(const char *var, const char *value,\n+\t\t\t\t      const struct config_context *ctx UNUSED, void *data)\n+{\n+\tstruct submodule_from_gitdir_cb *cb = data;\n+\tconst char *submodule_name_start;\n+\tsize_t submodule_name_len;\n+\tconst char *suffix = \".gitdir\";\n+\tsize_t suffix_len = strlen(suffix);\n+\n+\tif (!skip_prefix(var, \"submodule.\", &submodule_name_start))\n+\t\treturn 0;\n+\n+\t/* Check if submodule_name_start ends with \".gitdir\" */\n+\tsubmodule_name_len = strlen(submodule_name_start);\n+\tif (submodule_name_len < suffix_len ||\n+\t    strcmp(submodule_name_start + submodule_name_len - suffix_len, suffix) != 0)\n+\t\treturn 0; /* Does not end with \".gitdir\" */\n+\n+\tsubmodule_name_len -= suffix_len;\n+\n+\t/*\n+\t * A conflict happens if:\n+\t * 1. The submodule names are different and\n+\t * 2. The gitdir paths resolve to the same absolute path\n+\t */\n+\tif (value && strncmp(cb->submodule_name, submodule_name_start, submodule_name_len)) {\n+\t\tchar *abs_path_cb = absolute_pathdup(cb->gitdir);\n+\t\tchar *abs_path_value = absolute_pathdup(value);\n+\n+\t\tcb->conflict_found = !strcmp(abs_path_cb, abs_path_value);\n+\n+\t\tfree(abs_path_cb);\n+\t\tfree(abs_path_value);\n+\t}\n+\n+\treturn cb->conflict_found;\n+}\n+\n+static bool submodule_conflicts_with_existing(const char *gitdir, const char *submodule_name)\n+{\n+\tstruct submodule_from_gitdir_cb cb = { 0 };\n+\tcb.submodule_name = submodule_name;\n+\tcb.gitdir = gitdir;\n+\n+\t/* Find conflicts with existing repo gitdir configs */\n+\trepo_config(the_repository, find_conflict_by_gitdir_cb, &cb);\n+\n+\treturn cb.conflict_found;\n+}\n+\n /*\n  * Encoded gitdir validation, only used when extensions.submodulePathConfig is enabled.\n  * This does not print errors like the non-encoded version, because encoding is supposed\n@@ -2315,6 +2371,11 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu\n \tif (!last_submodule_name || strchr(last_submodule_name, '/'))\n \t\treturn -1;\n \n+\t/* Prevent conflicts with existing submodule gitdirs */\n+\tif (is_git_directory(git_dir) &&\n+\t    submodule_conflicts_with_existing(git_dir, submodule_name))\n+\t\t\treturn -1;\n+\n \t/* Prevent conflicts on case-folding filesystems */\n \trepo_config_get_bool(the_repository, \"core.ignorecase\", &config_ignorecase);\n \tif (ignore_case || config_ignorecase) {\ndiff --git a/t/t7425-submodule-gitdir-path-extension.sh b/t/t7425-submodule-gitdir-path-extension.sh\nindex a76e64a9f7..ea86ecf7ee 100755\n--- a/t/t7425-submodule-gitdir-path-extension.sh\n+++ b/t/t7425-submodule-gitdir-path-extension.sh\n@@ -497,4 +497,32 @@ test_expect_success CASE_INSENSITIVE_FS 'verify hashing conflict resolution as a\n \tverify_submodule_gitdir_path cloned-hash \"Foo\" \"modules/${hash}\"\n '\n \n+test_expect_success 'submodule gitdir conflicts with previously encoded name (local config)' '\n+\tgit init -b main super_with_encoded &&\n+\t(\n+\t\tcd super_with_encoded &&\n+\n+\t\tgit config core.repositoryformatversion 1 &&\n+\t\tgit config extensions.submodulePathConfig true &&\n+\n+\t\t# Add a submodule with a nested path\n+\t\tgit submodule add --name \"nested/sub\" ../sub nested/sub &&\n+\t\ttest_commit add-encoded-gitdir &&\n+\n+\t\tverify_submodule_gitdir_path . \"nested/sub\" \"modules/nested%2fsub\" &&\n+\t\ttest_path_is_dir \".git/modules/nested%2fsub\"\n+\t) &&\n+\n+\t# create a submodule that will conflict with the encoded gitdir name:\n+\t# the existing gitdir is \".git/modules/nested%2fsub\", which is used\n+\t# by \"nested/sub\", so the new submod will get another (non-conflicting)\n+\t# name: \"nested%252fsub\".\n+\t(\n+\t\tcd super_with_encoded &&\n+\t\tgit submodule add ../sub \"nested%2fsub\" &&\n+\t\tverify_submodule_gitdir_path . \"nested%2fsub\" \"modules/nested%252fsub\" &&\n+\t\ttest_path_is_dir \".git/modules/nested%252fsub\"\n+\t)\n+'\n+\n test_done\n-- \n2.52.0\n\n"},{"id":"533679","messageId":"xmqq7btmmvqn.fsf@gitster.g","threadId":"63967","inReplyTo":"20260112184632.1334495-1-adrian.ratiu@collabora.com","subject":"Re: [PATCH v9 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-12T20:17:20Z","receivedAt":"2026-01-12T20:17:24Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n\n> Changes in v9:\n> * Replaced git config --global with test_config_global (Patrick)\n> * Split die() messages to multiple lines (Patrick)\n> * Moved some of the whitespace fixes added in v8 to the commit\n>   which actually introduced the whitespace problem (Adrian)\n\nThanks.  It appears that we are getting to the point of diminishing\nreturns, perhaps, in which case we should declare victory and plan\nto merge it down soonish.\n\nAgain, a report like Josh did previously about this not breaking the\nusers you care about would be very much appreciated.\n\n"},{"id":"533685","messageId":"87344ad06s.fsf@gentoo.mail-host-address-is-not-set","threadId":"63967","inReplyTo":"xmqq7btmmvqn.fsf@gitster.g","subject":"Re: [PATCH v9 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Adrian Ratiu","fromEmail":"adrian.ratiu@collabora.com","sentAt":"2026-01-12T20:51:23Z","receivedAt":"2026-01-12T20:51:46Z","isPatch":true,"sender":{"key":"adrian.ratiu@collabora.com","avatar":"https://avatars.githubusercontent.com/u/12472556?v=4"},"body":"On Mon, 12 Jan 2026, Junio C Hamano <gitster@pobox.com> wrote:\n> Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n>\n>> Changes in v9:\n>> * Replaced git config --global with test_config_global (Patrick)\n>> * Split die() messages to multiple lines (Patrick)\n>> * Moved some of the whitespace fixes added in v8 to the commit\n>>   which actually introduced the whitespace problem (Adrian)\n>\n> Thanks.  It appears that we are getting to the point of diminishing\n> returns, perhaps, in which case we should declare victory and plan\n> to merge it down soonish.\n>\n> Again, a report like Josh did previously about this not breaking the\n> users you care about would be very much appreciated.\n\nAgreed. The logic is basically unchanged since v7, so I think we could\ngo ahead and merge based on Josh's report on v8.\n\nA big Thank You to all who contributed to this series,\nAdrian\n"},{"id":"533702","messageId":"aWXh9hQYxFhWYC6a@pks.im","threadId":"63967","inReplyTo":"87344ad06s.fsf@gentoo.mail-host-address-is-not-set","subject":"Re: [PATCH v9 00/11] Add submodulePathConfig extension and gitdir encoding","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T06:12:18Z","receivedAt":"2026-01-13T06:12:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Jan 12, 2026 at 10:51:23PM +0200, Adrian Ratiu wrote:\n> On Mon, 12 Jan 2026, Junio C Hamano <gitster@pobox.com> wrote:\n> > Adrian Ratiu <adrian.ratiu@collabora.com> writes:\n> >\n> >> Changes in v9:\n> >> * Replaced git config --global with test_config_global (Patrick)\n> >> * Split die() messages to multiple lines (Patrick)\n> >> * Moved some of the whitespace fixes added in v8 to the commit\n> >>   which actually introduced the whitespace problem (Adrian)\n> >\n> > Thanks.  It appears that we are getting to the point of diminishing\n> > returns, perhaps, in which case we should declare victory and plan\n> > to merge it down soonish.\n> >\n> > Again, a report like Josh did previously about this not breaking the\n> > users you care about would be very much appreciated.\n> \n> Agreed. The logic is basically unchanged since v7, so I think we could\n> go ahead and merge based on Josh's report on v8.\n\nLikewise, I'm happy with the state of this version now. Thanks!\n\nPatrick\n"}]}