{"thread":{"id":"66422","subject":"[RFC PATCH] submodule: make '^/' work like '../' but with an absolute path","startedAt":"2026-09-29T18:51:46Z","lastAt":"2026-09-29T18:51:46Z","messageCount":1,"participants":["Stanislav Aleksandrov"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"553630","messageId":"20260929185122.3127574-1-lightofmysoul@gmail.com","threadId":"66422","inReplyTo":null,"subject":"[RFC PATCH] submodule: make '^/' work like '../' but with an absolute path","fromName":"Stanislav Aleksandrov","fromEmail":"lightofmysoul@gmail.com","sentAt":"2026-09-29T18:51:22Z","receivedAt":"2026-09-29T18:51:46Z","isPatch":true,"body":"A submodule URL like \"../../org/lib.git\" breaks when the superproject\nis forked to a different depth, e.g. into a GitLab subgroup. An\nabsolute URL doesn't break, but it forces one protocol on everyone.\n\nResolve a URL starting with \"^/\" against the superproject's remote,\nlike \"../\", but from the root of the server. Protocol, user, host and\nport stay the same. For \"^/org/lib.git\":\n\n    https://host/me/super.git      ->  https://host/org/lib.git\n    git@host:group/sub/super.git   ->  git@host:org/lib.git\n\nUnlike \"../\", \"^/\" fails in a superproject cloned from a local path or\nwithout a remote, since there is no server. It also fails for\n\"<transport>::<address>\" remotes, whose address only the helper can\nparse.\n\nThe syntax comes from svn:externals, where \"^/\" is the repository root.\n\nSigned-off-by: Stanislav Aleksandrov <lightofmysoul@gmail.com>\n---\nRFC: the syntax is new, and it changes the meaning of local paths\nthat start with \"^/\".\n\n- Why not \"/path\" or \"//host/path\"? \"/path\" is already a local\n  absolute path, \"//host\" is a network path on Windows, and neither\n  can express \"git@host:path\" or SSH host aliases.\n- Why not url.<base>.insteadOf? It has to be set up on every clone;\n  .gitmodules cannot carry it.\n- Compatibility: a local \"^/...\" path needs a \"^\" directory in the\n  superproject and protocol.file.allow=always, and \"git submodule\n  add\" refuses it.\n- Known costs: \"^\" is special in cmd.exe, Git Bash rewrites \"^/...\"\n  arguments, and libgit2, JGit and forges would need to learn it.\n\nGitLab has an open request for the same thing:\nhttps://gitlab.com/gitlab-org/gitlab/-/issues/393295\n\n Documentation/git-submodule.adoc       |   8 +-\n Documentation/gitmodules.adoc          |   7 +-\n builtin/submodule--helper.c            |  39 +++--\n remote.c                               |  61 ++++++++\n remote.h                               |  15 ++\n submodule-config.c                     |   8 +-\n submodule-config.h                     |   6 +\n t/helper/test-submodule.c              |   7 +-\n t/meson.build                          |   1 +\n t/t0060-path-utils.sh                  |  43 ++++++\n t/t7427-submodule-root-relative-url.sh | 190 +++++++++++++++++++++++++\n t/t7450-bad-git-dotfiles.sh            |   5 +\n 12 files changed, 366 insertions(+), 24 deletions(-)\n create mode 100755 t/t7427-submodule-root-relative-url.sh\n\ndiff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc\nindex 722d827908..f512625eb8 100644\n--- a/Documentation/git-submodule.adoc\n+++ b/Documentation/git-submodule.adoc\n@@ -40,8 +40,9 @@ subcommands are available to perform operations on the submodules.\n \tproject: the current project is termed the \"superproject\".\n +\n _<repository>_ is the URL of the new submodule's `origin` repository.\n-This may be either an absolute URL, or (if it begins with `./`\n-or `../`), the location relative to the superproject's default remote\n+This may be an absolute URL, (if it begins with `^/`) an absolute path\n+on the server of the superproject's default remote, or (if it begins with\n+`./` or `../`), the location relative to the superproject's default remote\n repository (Please note that to specify a repository `foo.git`\n which is located right next to a superproject `bar.git`, you'll\n have to use `../foo.git` instead of `./foo.git` - as one might expect\n@@ -53,7 +54,8 @@ of the current branch. If no such remote-tracking branch exists or\n the `HEAD` is detached, `origin` is assumed to be the default remote.\n If the superproject doesn't have a default remote configured\n the superproject is its own authoritative upstream and the current\n-working directory is used instead.\n+working directory is used instead. A `^/` path needs a default remote\n+with a host.\n +\n The optional argument _<path>_ is the relative location for the cloned\n submodule to exist in the superproject. If _<path>_ is not given, the\ndiff --git a/Documentation/gitmodules.adoc b/Documentation/gitmodules.adoc\nindex fd96639806..9f6cc9a4ef 100644\n--- a/Documentation/gitmodules.adoc\n+++ b/Documentation/gitmodules.adoc\n@@ -31,9 +31,10 @@ submodule.<name>.path::\n \n submodule.<name>.url::\n \tDefines a URL from which the submodule repository can be cloned.\n-\tThis may be either an absolute URL ready to be passed to\n-\tlinkgit:git-clone[1] or (if it begins with `./` or `../`) a location\n-\trelative to the superproject's origin repository.\n+\tThis may be an absolute URL ready to be passed to\n+\tlinkgit:git-clone[1], (if it begins with `./` or `../`) a location\n+\trelative to the superproject's origin repository, or (if it begins\n+\twith `^/`) an absolute path on the server of that repository.\n \n In addition, there are a number of optional keys:\n \ndiff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c\nindex 5d3bcda334..16884ca6b6 100644\n--- a/builtin/submodule--helper.c\n+++ b/builtin/submodule--helper.c\n@@ -50,7 +50,8 @@ static char *get_default_remote(void)\n \treturn xstrdup(repo_default_remote(the_repository));\n }\n \n-static char *resolve_relative_url(const char *rel_url, const char *up_path, int quiet)\n+static char *resolve_relative_url_gently(const char *rel_url,\n+\t\t\t\t\t const char *up_path, int quiet)\n {\n \tchar *remoteurl, *resolved_url;\n \tchar *remote = get_default_remote();\n@@ -58,14 +59,17 @@ static char *resolve_relative_url(const char *rel_url, const char *up_path, int\n \n \tstrbuf_addf(&remotesb, \"remote.%s.url\", remote);\n \tif (repo_config_get_string(the_repository, remotesb.buf, &remoteurl)) {\n-\t\tif (!quiet)\n+\t\tif (!quiet && !starts_with(rel_url, \"^/\"))\n \t\t\twarning(_(\"could not look up configuration '%s'. \"\n \t\t\t\t  \"Assuming this repository is its own \"\n \t\t\t\t  \"authoritative upstream.\"),\n \t\t\t\tremotesb.buf);\n \t\tremoteurl = xgetcwd();\n \t}\n-\tresolved_url = relative_url(remoteurl, rel_url, up_path);\n+\tif (starts_with(rel_url, \"^/\"))\n+\t\tresolved_url = root_relative_url(remoteurl, rel_url);\n+\telse\n+\t\tresolved_url = relative_url(remoteurl, rel_url, up_path);\n \n \tfree(remote);\n \tfree(remoteurl);\n@@ -74,6 +78,16 @@ static char *resolve_relative_url(const char *rel_url, const char *up_path, int\n \treturn resolved_url;\n }\n \n+static char *resolve_relative_url(const char *rel_url, const char *up_path, int quiet)\n+{\n+\tchar *resolved_url = resolve_relative_url_gently(rel_url, up_path, quiet);\n+\n+\tif (!resolved_url)\n+\t\tdie(_(\"cannot resolve '%s' without a remote url that has a host\"),\n+\t\t    rel_url);\n+\treturn resolved_url;\n+}\n+\n static int get_default_remote_submodule(const char *module_path, char **default_remote)\n {\n \tconst struct submodule *sub;\n@@ -87,11 +101,10 @@ static int get_default_remote_submodule(const char *module_path, char **default_\n \t\turl = xstrdup(sub->url);\n \n \t\t/* Possibly a url relative to parent */\n-\t\tif (starts_with_dot_dot_slash(url) ||\n-\t\t    starts_with_dot_slash(url)) {\n+\t\tif (submodule_url_is_relative(url)) {\n \t\t\tchar *oldurl = url;\n \n-\t\t\turl = resolve_relative_url(oldurl, NULL, 1);\n+\t\t\turl = resolve_relative_url_gently(oldurl, NULL, 1);\n \t\t\tfree(oldurl);\n \t\t}\n \t}\n@@ -618,8 +631,7 @@ static void init_submodule(const char *path, const char *prefix,\n \t\turl = xstrdup(sub->url);\n \n \t\t/* Possibly a url relative to parent */\n-\t\tif (starts_with_dot_dot_slash(url) ||\n-\t\t    starts_with_dot_slash(url)) {\n+\t\tif (submodule_url_is_relative(url)) {\n \t\t\tchar *oldurl = url;\n \n \t\t\turl = resolve_relative_url(oldurl, NULL, 0);\n@@ -1450,8 +1462,7 @@ static void sync_submodule(const char *path, const char *prefix,\n \tsub = submodule_from_path(the_repository, null_oid(the_hash_algo), path);\n \n \tif (sub && sub->url) {\n-\t\tif (starts_with_dot_dot_slash(sub->url) ||\n-\t\t    starts_with_dot_slash(sub->url)) {\n+\t\tif (submodule_url_is_relative(sub->url)) {\n \t\t\tchar *up_path = get_up_path(path);\n \n \t\t\tsub_origin_url = resolve_relative_url(sub->url, up_path, 1);\n@@ -2314,8 +2325,7 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce,\n \tstrbuf_reset(&sb);\n \tstrbuf_addf(&sb, \"submodule.%s.url\", sub->name);\n \tif (repo_config_get_string_tmp(the_repository, sb.buf, &url)) {\n-\t\tif (sub->url && (starts_with_dot_slash(sub->url) ||\n-\t\t\t\t starts_with_dot_dot_slash(sub->url))) {\n+\t\tif (sub->url && submodule_url_is_relative(sub->url)) {\n \t\t\turl = resolve_relative_url(sub->url, NULL, 0);\n \t\t\tneed_free_url = 1;\n \t\t} else\n@@ -3710,8 +3720,7 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \t\tfree(sm_path);\n \t}\n \n-\tif (starts_with_dot_dot_slash(add_data.repo) ||\n-\t    starts_with_dot_slash(add_data.repo)) {\n+\tif (submodule_url_is_relative(add_data.repo)) {\n \t\tif (prefix)\n \t\t\tdie(_(\"Relative path can only be used from the toplevel \"\n \t\t\t      \"of the working tree\"));\n@@ -3722,7 +3731,7 @@ static int module_add(int argc, const char **argv, const char *prefix,\n \t} else if (is_dir_sep(add_data.repo[0]) || strchr(add_data.repo, ':')) {\n \t\tadd_data.realrepo = add_data.repo;\n \t} else {\n-\t\tdie(_(\"repo URL: '%s' must be absolute or begin with ./|../\"),\n+\t\tdie(_(\"repo URL: '%s' must be absolute or begin with ./|../|^/\"),\n \t\t    add_data.repo);\n \t}\n \ndiff --git a/remote.c b/remote.c\nindex fe62068463..05ef35e3ab 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -3095,6 +3095,67 @@ char *relative_url(const char *remote_url, const char *url,\n \treturn strbuf_detach(&sb, NULL);\n }\n \n+static const char *skip_bracketed_host(const char *host)\n+{\n+\tconst char *start = strstr(host, \"@[\");\n+\tconst char *end;\n+\n+\tstart = start ? start + 1 : host;\n+\tif (*start != '[')\n+\t\treturn host;\n+\tend = strchr(start + 1, ']');\n+\treturn end ? end : host;\n+}\n+\n+static int has_host(const char *start, const char *end)\n+{\n+\tconst char *p;\n+\n+\tfor (p = start; p < end; p++)\n+\t\tif (*p == '@')\n+\t\t\tstart = p + 1;\n+\treturn start < end && *start != ':' && !starts_with(start, \"[]\");\n+}\n+\n+char *root_relative_url(const char *remote_url, const char *url)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tconst char *path, *host, *end;\n+\tint scp;\n+\n+\tif (!skip_prefix(url, \"^/\", &path))\n+\t\tBUG(\"not a root-relative url: '%s'\", url);\n+\tif (*path == '/' || *path == ':')\n+\t\tdie(_(\"root-relative url '%s' must not start with '^//' or '^/:'\"),\n+\t\t    url);\n+\n+\tfor (end = remote_url; is_urlschemechar(end == remote_url, *end); end++)\n+\t\t;\n+\tif (starts_with(end, \"::\") || starts_with(remote_url, \"file://\") ||\n+\t    url_is_local_not_ssh(remote_url))\n+\t\treturn NULL;\n+\n+\tscp = !is_url(remote_url);\n+\tif (scp) {\n+\t\thost = remote_url;\n+\t\tend = strchr(skip_bracketed_host(remote_url), ':');\n+\t} else {\n+\t\thost = strstr(remote_url, \"://\") + 3;\n+\t\tend = strchrnul(host, '/');\n+\t}\n+\tif (!end || !has_host(host, end))\n+\t\treturn NULL;\n+\n+\tstrbuf_add(&sb, remote_url, end - remote_url);\n+\tstrbuf_addch(&sb, scp ? ':' : '/');\n+\tif (scp && end[1] == '/')\n+\t\tstrbuf_addch(&sb, '/');\n+\tstrbuf_addstr(&sb, path);\n+\tif (ends_with(path, \"/\"))\n+\t\tstrbuf_setlen(&sb, sb.len - 1);\n+\treturn strbuf_detach(&sb, NULL);\n+}\n+\n int valid_remote_name(const char *name)\n {\n \tint result;\ndiff --git a/remote.h b/remote.h\nindex cca02033b9..8a759ae20d 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -478,6 +478,21 @@ void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);\n char *relative_url(const char *remote_url, const char *url,\n \t\t   const char *up_path);\n \n+/*\n+ * The `url` argument starts with \"^/\" and names a repository relative to\n+ * the root of the server that `remote_url` points to: the path of\n+ * `remote_url` is replaced with the rest of `url`, keeping its scheme, user,\n+ * host and port. Returns NULL if `remote_url` has no host, and dies if\n+ * `url` continues with '/' or ':', which could change the kind of URL.\n+ *\n+ * remote_url                 url            outcome\n+ * https://a.com/b/c          ^/d/e          https://a.com/d/e\n+ * ssh://u@a.com:22/b/c       ^/d/e          ssh://u@a.com:22/d/e\n+ * u@a.com:b/c                ^/d/e          u@a.com:d/e\n+ * u@a.com:/b/c               ^/d/e          u@a.com:/d/e\n+ */\n+char *root_relative_url(const char *remote_url, const char *url);\n+\n int valid_remote_name(const char *name);\n \n #endif\ndiff --git a/submodule-config.c b/submodule-config.c\nindex 37c3be377b..dfa819112c 100644\n--- a/submodule-config.c\n+++ b/submodule-config.c\n@@ -237,9 +237,10 @@ int check_submodule_name(const char *name)\n \treturn 0;\n }\n \n-static int submodule_url_is_relative(const char *url)\n+int submodule_url_is_relative(const char *url)\n {\n-\treturn starts_with_dot_slash(url) || starts_with_dot_dot_slash(url);\n+\treturn starts_with_dot_slash(url) || starts_with_dot_dot_slash(url) ||\n+\t       starts_with(url, \"^/\");\n }\n \n /*\n@@ -342,6 +343,9 @@ int check_submodule_url(const char *url)\n \t\tif (count_leading_dotdots(url, &next) > 0 &&\n \t\t    (*next == ':' || *next == '/'))\n \t\t\treturn -1;\n+\t\tif (skip_prefix(url, \"^/\", &next) &&\n+\t\t    (*next == ':' || *next == '/'))\n+\t\t\treturn -1;\n \t}\n \n \telse if (url_to_curl_url(url, &curl_url)) {\ndiff --git a/submodule-config.h b/submodule-config.h\nindex 755570d5d1..3e947291d5 100644\n--- a/submodule-config.h\n+++ b/submodule-config.h\n@@ -94,6 +94,12 @@ int check_submodule_name(const char *name);\n /* Returns 0 if the URL valid per RFC3986 and -1 otherwise. */\n int check_submodule_url(const char *url);\n \n+/*\n+ * Returns 1 if the URL is resolved against the superproject's remote,\n+ * i.e. starts with \"./\", \"../\" or \"^/\", and 0 otherwise.\n+ */\n+int submodule_url_is_relative(const char *url);\n+\n /*\n  * Note: these helper functions exist solely to maintain backward\n  * compatibility with 'fetch' and 'update_clone' storing configuration in\ndiff --git a/t/helper/test-submodule.c b/t/helper/test-submodule.c\nindex ea9bef0904..f28daf52fe 100644\n--- a/t/helper/test-submodule.c\n+++ b/t/helper/test-submodule.c\n@@ -123,7 +123,12 @@ static int cmd__submodule_resolve_relative_url(int argc, const char **argv)\n \tif (!strcmp(up_path, \"(null)\"))\n \t\tup_path = NULL;\n \n-\tres = relative_url(remoteurl, url, up_path);\n+\tif (starts_with(url, \"^/\"))\n+\t\tres = root_relative_url(remoteurl, url);\n+\telse\n+\t\tres = relative_url(remoteurl, url, up_path);\n+\tif (!res)\n+\t\tdie(\"cannot resolve '%s' against '%s'\", url, remoteurl);\n \tputs(res);\n \tfree(res);\n \tfree(remoteurl);\ndiff --git a/t/meson.build b/t/meson.build\nindex 3ca7b27104..9d3ac94aae 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -918,6 +918,7 @@ integration_tests = [\n   't7424-submodule-mixed-ref-formats.sh',\n   't7425-submodule-gitdir-path-extension.sh',\n   't7426-submodule-get-default-remote.sh',\n+  't7427-submodule-root-relative-url.sh',\n   't7450-bad-git-dotfiles.sh',\n   't7500-commit-template-squash-signoff.sh',\n   't7501-commit-basic-functionality.sh',\ndiff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh\nindex 56faf5fe73..c8618e8a30 100755\n--- a/t/t0060-path-utils.sh\n+++ b/t/t0060-path-utils.sh\n@@ -7,6 +7,11 @@ test_description='Test various path utilities'\n \n . ./test-lib.sh\n \n+# Keep MSYS2 from turning \"^/...\" arguments into Windows paths. The value\n+# must not look like a path itself, or MSYS2 rewrites it for child processes.\n+MSYS2_ARG_CONV_EXCL='^'\n+export MSYS2_ARG_CONV_EXCL\n+\n norm_path() {\n \texpected=$(test-tool path-utils print_path \"$2\")\n \ttest_expect_success $3 \"normalize path: $1 => $2\" \"\n@@ -435,6 +440,44 @@ test_submodule_relative_url \"(null)\" \"user@host:path/to/repo\" \"../subrepo\" \"user\n test_submodule_relative_url \"(null)\" \"user@host:repo\" \"../subrepo\" \"user@host:subrepo\"\n test_submodule_relative_url \"(null)\" \"user@host:repo\" \"../../subrepo\" \".:subrepo\"\n \n+test_submodule_relative_url \"(null)\" \"https://example.com/me/super.git\" \"^/org/lib.git\" \"https://example.com/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"https://example.com/a/b/c/super.git\" \"^/org/lib.git\" \"https://example.com/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"https://example.com\" \"^/org/lib.git\" \"https://example.com/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"https://user@example.com:8443/me/super.git\" \"^/org/lib.git\" \"https://user@example.com:8443/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"https://example.com/me/super.git\" \"^/org/lib/\" \"https://example.com/org/lib\"\n+test_submodule_relative_url \"../\" \"https://example.com/me/super.git\" \"^/org/lib.git\" \"https://example.com/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"helper://example.com/me/super.git\" \"^/org/lib.git\" \"helper://example.com/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"ssh://git@example.com:2222/a/b/super.git\" \"^/org/lib.git\" \"ssh://git@example.com:2222/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"ssh://git@[::1]:2222/me/super.git\" \"^/org/lib.git\" \"ssh://git@[::1]:2222/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"ssh://example.com/~user/super.git\" \"^/org/lib.git\" \"ssh://example.com/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"git@example.com:me/super.git\" \"^/org/lib.git\" \"git@example.com:org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"git@example.com:a/b/super.git\" \"^/org/lib.git\" \"git@example.com:org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"git@example.com:/srv/git/super.git\" \"^/org/lib.git\" \"git@example.com:/org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"example.com:~user/super.git\" \"^/org/lib.git\" \"example.com:org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"git@[::1]:me/super.git\" \"^/org/lib.git\" \"git@[::1]:org/lib.git\"\n+test_submodule_relative_url \"(null)\" \"[::1]:me/super.git\" \"^/org/lib.git\" \"[::1]:org/lib.git\"\n+\n+test_expect_success 'root-relative submodule url needs a remote with a host' '\n+\tfor remote in /srv/git/super.git ../super.git file:///srv/git/super.git \\\n+\t\thelper::https://example.com/super.git \"[::1]\" \\\n+\t\thttps:///srv/git/super.git https://user@/super.git \\\n+\t\tssh://:22/super.git :super.git git@:super.git \"[]:super.git\"\n+\tdo\n+\t\ttest_must_fail test-tool submodule resolve-relative-url \\\n+\t\t\t\"(null)\" \"$remote\" \"^/org/lib.git\" 2>err &&\n+\t\ttest_grep \"cannot resolve\" err || return 1\n+\tdone\n+'\n+\n+test_expect_success 'root-relative submodule url cannot change the kind of url' '\n+\tfor url in \"^//evil.example.com/x.git\" \"^/:evil\"\n+\tdo\n+\t\ttest_must_fail test-tool submodule resolve-relative-url \\\n+\t\t\t\"(null)\" host:/srv/super.git \"$url\" 2>err &&\n+\t\ttest_grep \"must not start with\" err || return 1\n+\tdone\n+'\n+\n test_expect_success 'match .gitmodules' '\n \ttest-tool path-utils is_dotgitmodules \\\n \t\t.gitmodules \\\ndiff --git a/t/t7427-submodule-root-relative-url.sh b/t/t7427-submodule-root-relative-url.sh\nnew file mode 100755\nindex 0000000000..526dfec8d2\n--- /dev/null\n+++ b/t/t7427-submodule-root-relative-url.sh\n@@ -0,0 +1,190 @@\n+#!/bin/sh\n+\n+test_description='submodule urls relative to the server root (^/)'\n+\n+. ./test-lib.sh\n+\n+# Keep MSYS2 from turning \"^/...\" arguments into Windows paths. The value\n+# must not look like a path itself, or MSYS2 rewrites it for child processes.\n+MSYS2_ARG_CONV_EXCL='^'\n+export MSYS2_ARG_CONV_EXCL\n+\n+test_expect_success 'setup' '\n+\tgit config --global protocol.file.allow always &&\n+\tgit config --global url.\"$(pwd)/server/\".insteadOf https://example.com/ &&\n+\tgit config --global --add url.\"$(pwd)/server/\".insteadOf git@example.com: &&\n+\n+\tgit init --bare server/org/dep.git &&\n+\tgit init --bare server/org/lib.git &&\n+\tgit init --bare server/org/team/super.git &&\n+\n+\tgit clone https://example.com/org/dep.git dep &&\n+\ttest_commit -C dep dep &&\n+\tgit -C dep push origin HEAD &&\n+\n+\tgit clone https://example.com/org/lib.git lib &&\n+\ttest_commit -C lib lib &&\n+\tgit -C lib submodule add ^/org/dep.git dep &&\n+\tgit -C lib commit -m \"add dep\" &&\n+\tgit -C lib push origin HEAD &&\n+\n+\tgit clone https://example.com/org/team/super.git super &&\n+\ttest_commit -C super super &&\n+\tgit -C super submodule add ^/org/lib.git lib &&\n+\tgit -C super commit -m \"add lib\" &&\n+\tgit -C super push origin HEAD &&\n+\n+\tgit clone --bare server/org/team/super.git server/me/super.git\n+'\n+\n+test_expect_success 'add records the url as given' '\n+\ttest_cmp_config -C super \"^/org/lib.git\" -f .gitmodules submodule.lib.url &&\n+\ttest_cmp_config -C super https://example.com/org/lib.git submodule.lib.url\n+'\n+\n+test_expect_success 'clone of the upstream in a subgroup' '\n+\tgit clone --recurse-submodules https://example.com/org/team/super.git upstream &&\n+\ttest_cmp_config -C upstream https://example.com/org/lib.git submodule.lib.url &&\n+\ttest_path_is_file upstream/lib/lib.t &&\n+\ttest_path_is_file upstream/lib/dep/dep.t\n+'\n+\n+test_expect_success 'clone of a fork at a different depth' '\n+\tgit clone --recurse-submodules https://example.com/me/super.git fork &&\n+\ttest_cmp_config -C fork https://example.com/org/lib.git submodule.lib.url &&\n+\ttest_cmp_config -C fork/lib https://example.com/org/dep.git submodule.dep.url &&\n+\ttest_path_is_file fork/lib/dep/dep.t\n+'\n+\n+test_expect_success 'clone over scp-like ssh uses ssh for submodules' '\n+\tgit clone --recurse-submodules git@example.com:me/super.git fork-ssh &&\n+\ttest_cmp_config -C fork-ssh git@example.com:org/lib.git submodule.lib.url &&\n+\ttest_cmp_config -C fork-ssh/lib git@example.com:org/dep.git submodule.dep.url &&\n+\ttest_path_is_file fork-ssh/lib/dep/dep.t\n+'\n+\n+test_expect_success 'sync follows a changed superproject remote' '\n+\tgit -C fork remote set-url origin ssh://git@example.com:2222/me/super.git &&\n+\tgit -C fork submodule sync &&\n+\ttest_cmp_config -C fork ssh://git@example.com:2222/org/lib.git submodule.lib.url &&\n+\ttest_cmp_config -C fork/lib ssh://git@example.com:2222/org/lib.git remote.origin.url\n+'\n+\n+test_expect_success 'get-default-remote finds the submodule remote by its ^/ url' '\n+\tgit -C fork/lib remote rename origin upstream &&\n+\tgit -C fork/lib remote add other https://example.com/org/dep.git &&\n+\techo upstream >expect &&\n+\tgit -C fork submodule--helper get-default-remote lib >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'init fails when the remote has no host' '\n+\tgit clone server/me/super.git local &&\n+\ttest_must_fail git -C local submodule init 2>err &&\n+\ttest_grep \"cannot resolve .* without a remote url that has a host\" err\n+'\n+\n+test_expect_success 'init without a remote does not suggest the cwd fallback' '\n+\tgit init noremote &&\n+\tgit -C noremote config -f .gitmodules submodule.lib.path lib &&\n+\tgit -C noremote config -f .gitmodules submodule.lib.url \"^/org/lib.git\" &&\n+\tgit -C noremote update-index --add --cacheinfo \\\n+\t\t160000,$(git -C lib rev-parse HEAD),lib &&\n+\ttest_must_fail git -C noremote submodule init 2>err &&\n+\ttest_grep \"cannot resolve\" err &&\n+\ttest_grep ! \"authoritative upstream\" err\n+'\n+\n+test_expect_success 'update fails when the remote has no host' '\n+\tgit -C noremote config submodule.lib.active true &&\n+\ttest_must_fail git -C noremote submodule update 2>err &&\n+\ttest_grep \"cannot resolve .* without a remote url that has a host\" err\n+'\n+\n+test_expect_success 'update --init fails even if the submodule is not updated' '\n+\ttest_must_fail git -C noremote -c submodule.lib.update=none \\\n+\t\tsubmodule update --init 2>err &&\n+\ttest_grep \"cannot resolve\" err\n+'\n+\n+test_expect_success 'sync fails when the remote has no host' '\n+\tgit -C local config submodule.lib.active true &&\n+\ttest_must_fail git -C local submodule sync 2>err &&\n+\ttest_grep \"cannot resolve .* without a remote url that has a host\" err\n+'\n+\n+test_expect_success 'add fails when the remote has no host' '\n+\ttest_must_fail git -C local submodule add \"^/org/dep.git\" dep 2>err &&\n+\ttest_grep \"cannot resolve .* without a remote url that has a host\" err &&\n+\ttest_path_is_missing local/dep\n+'\n+\n+test_expect_success 'clone --recurse-submodules stops when the remote has no host' '\n+\tgit init --bare server/org/mixed.git &&\n+\tgit init mixed &&\n+\tgit -C mixed config -f .gitmodules submodule.lib.path lib &&\n+\tgit -C mixed config -f .gitmodules submodule.lib.url \"^/org/lib.git\" &&\n+\tgit -C mixed config -f .gitmodules submodule.zz.path zz &&\n+\tgit -C mixed config -f .gitmodules submodule.zz.url ../dep.git &&\n+\tgit -C mixed update-index --add \\\n+\t\t--cacheinfo 160000,$(git -C lib rev-parse HEAD),lib \\\n+\t\t--cacheinfo 160000,$(git -C dep rev-parse HEAD),zz &&\n+\tgit -C mixed add .gitmodules &&\n+\tgit -C mixed commit -m mixed &&\n+\tgit -C mixed push \"$(pwd)/server/org/mixed.git\" HEAD &&\n+\n+\ttest_must_fail git clone --recurse-submodules server/org/mixed.git \\\n+\t\tmixed-clone 2>err &&\n+\ttest_grep \"cannot resolve .* without a remote url that has a host\" err &&\n+\ttest_path_is_missing mixed-clone/zz/dep.t\n+'\n+\n+test_expect_success 'nested ^/ fails when the submodule remote has no host' '\n+\tgit init --bare server/org/rel.git &&\n+\tgit init rel &&\n+\tgit -C rel config -f .gitmodules submodule.lib.path lib &&\n+\tgit -C rel config -f .gitmodules submodule.lib.url ../lib.git &&\n+\tgit -C rel update-index --add --cacheinfo \\\n+\t\t160000,$(git -C lib rev-parse HEAD),lib &&\n+\tgit -C rel add .gitmodules &&\n+\tgit -C rel commit -m rel &&\n+\tgit -C rel push \"$(pwd)/server/org/rel.git\" HEAD &&\n+\n+\ttest_must_fail git clone --recurse-submodules server/org/rel.git \\\n+\t\trel-clone 2>err &&\n+\ttest_grep \"cannot resolve .*dep.git. without a remote url that has a host\" err\n+'\n+\n+test_expect_success 'explicit url overrides a root-relative one' '\n+\tgit -C local config submodule.lib.url \"$(pwd)/server/org/lib.git\" &&\n+\tgit -C local submodule update --init &&\n+\ttest_path_is_file local/lib/lib.t &&\n+\tgit -C local submodule update --remote &&\n+\ttest_path_is_file local/lib/lib.t\n+'\n+\n+test_expect_success '^/ means the server root even if a local ^ directory exists' '\n+\tgit init literal &&\n+\tmkdir -p \"literal/^/org\" &&\n+\tgit clone --bare server/org/lib.git \"literal/^/org/lib.git\" &&\n+\tgit -C literal config -f .gitmodules submodule.lib.path lib &&\n+\tgit -C literal config -f .gitmodules submodule.lib.url \"^/org/lib.git\" &&\n+\tgit -C literal update-index --add --cacheinfo \\\n+\t\t160000,$(git -C lib rev-parse HEAD),lib &&\n+\n+\tgit -C literal remote add origin https://example.com/me/super.git &&\n+\tgit -C literal submodule init &&\n+\ttest_cmp_config -C literal https://example.com/org/lib.git submodule.lib.url &&\n+\n+\tgit -C literal config --unset submodule.lib.url &&\n+\tgit -C literal remote set-url origin \"$(pwd)/server/me/super.git\" &&\n+\ttest_must_fail git -C literal submodule init\n+'\n+\n+test_expect_success 'add from a subdirectory is refused' '\n+\tmkdir super/sub &&\n+\ttest_must_fail git -C super/sub submodule add ^/org/dep.git dep 2>err &&\n+\ttest_grep \"Relative path can only be used from the toplevel\" err\n+'\n+\n+test_done\ndiff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh\nindex 72c7f6f73b..0df7cb4a92 100755\n--- a/t/t7450-bad-git-dotfiles.sh\n+++ b/t/t7450-bad-git-dotfiles.sh\n@@ -49,12 +49,17 @@ test_expect_success 'check urls' '\n \t./bar/baz/foo.git\n \thttps://example.com/foo.git\n \thttp://example.com:80/deeper/foo.git\n+\t^/org/foo.git\n \tEOF\n \n \ttest-tool submodule check-url >actual <<-\\EOF &&\n \t./bar/baz/foo.git\n \thttps://example.com/foo.git\n \thttp://example.com:80/deeper/foo.git\n+\t^/org/foo.git\n+\t^/%0ahost=example.com/foo.git\n+\t^//evil.example.com/foo.git\n+\t^/:foo.git\n \t-a./foo\n \t../../..//test/foo.git\n \t../../../../../:localhost:8080/foo.git\n-- \n2.55.0\n\n"}]}