[RFC PATCH] submodule: make '^/' work like '../' but with an absolute path
From: Stanislav Aleksandrov <hidden>
Date: 2026-09-29 18:51:46
Subsystem:
documentation, the rest · Maintainers:
Jonathan Corbet, Linus Torvalds
A submodule URL like "../../org/lib.git" breaks when the superproject
is forked to a different depth, e.g. into a GitLab subgroup. An
absolute URL doesn't break, but it forces one protocol on everyone.
Resolve a URL starting with "^/" against the superproject's remote,
like "../", but from the root of the server. Protocol, user, host and
port stay the same. For "^/org/lib.git":
https://host/me/super.git -> https://host/org/lib.git
git@host:group/sub/super.git -> git@host:org/lib.git
Unlike "../", "^/" fails in a superproject cloned from a local path or
without a remote, since there is no server. It also fails for
"<transport>::<address>" remotes, whose address only the helper can
parse.
The syntax comes from svn:externals, where "^/" is the repository root.
Signed-off-by: Stanislav Aleksandrov <redacted>
---
RFC: the syntax is new, and it changes the meaning of local paths
that start with "^/".
- Why not "/path" or "//host/path"? "/path" is already a local
absolute path, "//host" is a network path on Windows, and neither
can express "git@host:path" or SSH host aliases.
- Why not url.<base>.insteadOf? It has to be set up on every clone;
.gitmodules cannot carry it.
- Compatibility: a local "^/..." path needs a "^" directory in the
superproject and protocol.file.allow=always, and "git submodule
add" refuses it.
- Known costs: "^" is special in cmd.exe, Git Bash rewrites "^/..."
arguments, and libgit2, JGit and forges would need to learn it.
GitLab has an open request for the same thing:
https://gitlab.com/gitlab-org/gitlab/-/issues/393295
Documentation/git-submodule.adoc | 8 +-
Documentation/gitmodules.adoc | 7 +-
builtin/submodule--helper.c | 39 +++--
remote.c | 61 ++++++++
remote.h | 15 ++
submodule-config.c | 8 +-
submodule-config.h | 6 +
t/helper/test-submodule.c | 7 +-
t/meson.build | 1 +
t/t0060-path-utils.sh | 43 ++++++
t/t7427-submodule-root-relative-url.sh | 190 +++++++++++++++++++++++++
t/t7450-bad-git-dotfiles.sh | 5 +
12 files changed, 366 insertions(+), 24 deletions(-)
create mode 100755 t/t7427-submodule-root-relative-url.sh
diff --git a/Documentation/git-submodule.adoc b/Documentation/git-submodule.adoc
index 722d827908..f512625eb8 100644
--- a/Documentation/git-submodule.adoc
+++ b/Documentation/git-submodule.adoc@@ -40,8 +40,9 @@ subcommands are available to perform operations on the submodules. project: the current project is termed the "superproject". + _<repository>_ is the URL of the new submodule's `origin` repository. -This may be either an absolute URL, or (if it begins with `./` -or `../`), the location relative to the superproject's default remote +This may be an absolute URL, (if it begins with `^/`) an absolute path +on the server of the superproject's default remote, or (if it begins with +`./` or `../`), the location relative to the superproject's default remote repository (Please note that to specify a repository `foo.git` which is located right next to a superproject `bar.git`, you'll have to use `../foo.git` instead of `./foo.git` - as one might expect
@@ -53,7 +54,8 @@ of the current branch. If no such remote-tracking branch exists or the `HEAD` is detached, `origin` is assumed to be the default remote. If the superproject doesn't have a default remote configured the superproject is its own authoritative upstream and the current -working directory is used instead. +working directory is used instead. A `^/` path needs a default remote +with a host. + The optional argument _<path>_ is the relative location for the cloned submodule to exist in the superproject. If _<path>_ is not given, the
diff --git a/Documentation/gitmodules.adoc b/Documentation/gitmodules.adoc
index fd96639806..9f6cc9a4ef 100644
--- a/Documentation/gitmodules.adoc
+++ b/Documentation/gitmodules.adoc@@ -31,9 +31,10 @@ submodule.<name>.path:: submodule.<name>.url:: Defines a URL from which the submodule repository can be cloned. - This may be either an absolute URL ready to be passed to - linkgit:git-clone[1] or (if it begins with `./` or `../`) a location - relative to the superproject's origin repository. + This may be an absolute URL ready to be passed to + linkgit:git-clone[1], (if it begins with `./` or `../`) a location + relative to the superproject's origin repository, or (if it begins + with `^/`) an absolute path on the server of that repository. In addition, there are a number of optional keys:
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index 5d3bcda334..16884ca6b6 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c@@ -50,7 +50,8 @@ static char *get_default_remote(void) return xstrdup(repo_default_remote(the_repository)); } -static char *resolve_relative_url(const char *rel_url, const char *up_path, int quiet) +static char *resolve_relative_url_gently(const char *rel_url, + const char *up_path, int quiet) { char *remoteurl, *resolved_url; char *remote = get_default_remote();
@@ -58,14 +59,17 @@ static char *resolve_relative_url(const char *rel_url, const char *up_path, int strbuf_addf(&remotesb, "remote.%s.url", remote); if (repo_config_get_string(the_repository, remotesb.buf, &remoteurl)) { - if (!quiet) + if (!quiet && !starts_with(rel_url, "^/")) warning(_("could not look up configuration '%s'. " "Assuming this repository is its own " "authoritative upstream."), remotesb.buf); remoteurl = xgetcwd(); } - resolved_url = relative_url(remoteurl, rel_url, up_path); + if (starts_with(rel_url, "^/")) + resolved_url = root_relative_url(remoteurl, rel_url); + else + resolved_url = relative_url(remoteurl, rel_url, up_path); free(remote); free(remoteurl);
@@ -74,6 +78,16 @@ static char *resolve_relative_url(const char *rel_url, const char *up_path, int return resolved_url; } +static char *resolve_relative_url(const char *rel_url, const char *up_path, int quiet) +{ + char *resolved_url = resolve_relative_url_gently(rel_url, up_path, quiet); + + if (!resolved_url) + die(_("cannot resolve '%s' without a remote url that has a host"), + rel_url); + return resolved_url; +} + static int get_default_remote_submodule(const char *module_path, char **default_remote) { const struct submodule *sub;
@@ -87,11 +101,10 @@ static int get_default_remote_submodule(const char *module_path, char **default_ url = xstrdup(sub->url); /* Possibly a url relative to parent */ - if (starts_with_dot_dot_slash(url) || - starts_with_dot_slash(url)) { + if (submodule_url_is_relative(url)) { char *oldurl = url; - url = resolve_relative_url(oldurl, NULL, 1); + url = resolve_relative_url_gently(oldurl, NULL, 1); free(oldurl); } }
@@ -618,8 +631,7 @@ static void init_submodule(const char *path, const char *prefix, url = xstrdup(sub->url); /* Possibly a url relative to parent */ - if (starts_with_dot_dot_slash(url) || - starts_with_dot_slash(url)) { + if (submodule_url_is_relative(url)) { char *oldurl = url; url = resolve_relative_url(oldurl, NULL, 0);
@@ -1450,8 +1462,7 @@ static void sync_submodule(const char *path, const char *prefix, sub = submodule_from_path(the_repository, null_oid(the_hash_algo), path); if (sub && sub->url) { - if (starts_with_dot_dot_slash(sub->url) || - starts_with_dot_slash(sub->url)) { + if (submodule_url_is_relative(sub->url)) { char *up_path = get_up_path(path); sub_origin_url = resolve_relative_url(sub->url, up_path, 1);
@@ -2314,8 +2325,7 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce, strbuf_reset(&sb); strbuf_addf(&sb, "submodule.%s.url", sub->name); if (repo_config_get_string_tmp(the_repository, sb.buf, &url)) { - if (sub->url && (starts_with_dot_slash(sub->url) || - starts_with_dot_dot_slash(sub->url))) { + if (sub->url && submodule_url_is_relative(sub->url)) { url = resolve_relative_url(sub->url, NULL, 0); need_free_url = 1; } else
@@ -3710,8 +3720,7 @@ static int module_add(int argc, const char **argv, const char *prefix, free(sm_path); } - if (starts_with_dot_dot_slash(add_data.repo) || - starts_with_dot_slash(add_data.repo)) { + if (submodule_url_is_relative(add_data.repo)) { if (prefix) die(_("Relative path can only be used from the toplevel " "of the working tree"));
@@ -3722,7 +3731,7 @@ static int module_add(int argc, const char **argv, const char *prefix, } else if (is_dir_sep(add_data.repo[0]) || strchr(add_data.repo, ':')) { add_data.realrepo = add_data.repo; } else { - die(_("repo URL: '%s' must be absolute or begin with ./|../"), + die(_("repo URL: '%s' must be absolute or begin with ./|../|^/"), add_data.repo); }
diff --git a/remote.c b/remote.c
index fe62068463..05ef35e3ab 100644
--- a/remote.c
+++ b/remote.c@@ -3095,6 +3095,67 @@ char *relative_url(const char *remote_url, const char *url, return strbuf_detach(&sb, NULL); } +static const char *skip_bracketed_host(const char *host) +{ + const char *start = strstr(host, "@["); + const char *end; + + start = start ? start + 1 : host; + if (*start != '[') + return host; + end = strchr(start + 1, ']'); + return end ? end : host; +} + +static int has_host(const char *start, const char *end) +{ + const char *p; + + for (p = start; p < end; p++) + if (*p == '@') + start = p + 1; + return start < end && *start != ':' && !starts_with(start, "[]"); +} + +char *root_relative_url(const char *remote_url, const char *url) +{ + struct strbuf sb = STRBUF_INIT; + const char *path, *host, *end; + int scp; + + if (!skip_prefix(url, "^/", &path)) + BUG("not a root-relative url: '%s'", url); + if (*path == '/' || *path == ':') + die(_("root-relative url '%s' must not start with '^//' or '^/:'"), + url); + + for (end = remote_url; is_urlschemechar(end == remote_url, *end); end++) + ; + if (starts_with(end, "::") || starts_with(remote_url, "file://") || + url_is_local_not_ssh(remote_url)) + return NULL; + + scp = !is_url(remote_url); + if (scp) { + host = remote_url; + end = strchr(skip_bracketed_host(remote_url), ':'); + } else { + host = strstr(remote_url, "://") + 3; + end = strchrnul(host, '/'); + } + if (!end || !has_host(host, end)) + return NULL; + + strbuf_add(&sb, remote_url, end - remote_url); + strbuf_addch(&sb, scp ? ':' : '/'); + if (scp && end[1] == '/') + strbuf_addch(&sb, '/'); + strbuf_addstr(&sb, path); + if (ends_with(path, "/")) + strbuf_setlen(&sb, sb.len - 1); + return strbuf_detach(&sb, NULL); +} + int valid_remote_name(const char *name) { int result;
diff --git a/remote.h b/remote.h
index cca02033b9..8a759ae20d 100644
--- a/remote.h
+++ b/remote.h@@ -478,6 +478,21 @@ void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *); char *relative_url(const char *remote_url, const char *url, const char *up_path); +/* + * The `url` argument starts with "^/" and names a repository relative to + * the root of the server that `remote_url` points to: the path of + * `remote_url` is replaced with the rest of `url`, keeping its scheme, user, + * host and port. Returns NULL if `remote_url` has no host, and dies if + * `url` continues with '/' or ':', which could change the kind of URL. + * + * remote_url url outcome + * https://a.com/b/c ^/d/e https://a.com/d/e + * ssh://u@a.com:22/b/c ^/d/e ssh://u@a.com:22/d/e + * u@a.com:b/c ^/d/e u@a.com:d/e + * u@a.com:/b/c ^/d/e u@a.com:/d/e + */ +char *root_relative_url(const char *remote_url, const char *url); + int valid_remote_name(const char *name); #endif
diff --git a/submodule-config.c b/submodule-config.c
index 37c3be377b..dfa819112c 100644
--- a/submodule-config.c
+++ b/submodule-config.c@@ -237,9 +237,10 @@ int check_submodule_name(const char *name) return 0; } -static int submodule_url_is_relative(const char *url) +int submodule_url_is_relative(const char *url) { - return starts_with_dot_slash(url) || starts_with_dot_dot_slash(url); + return starts_with_dot_slash(url) || starts_with_dot_dot_slash(url) || + starts_with(url, "^/"); } /*
@@ -342,6 +343,9 @@ int check_submodule_url(const char *url) if (count_leading_dotdots(url, &next) > 0 && (*next == ':' || *next == '/')) return -1; + if (skip_prefix(url, "^/", &next) && + (*next == ':' || *next == '/')) + return -1; } else if (url_to_curl_url(url, &curl_url)) {
diff --git a/submodule-config.h b/submodule-config.h
index 755570d5d1..3e947291d5 100644
--- a/submodule-config.h
+++ b/submodule-config.h@@ -94,6 +94,12 @@ int check_submodule_name(const char *name); /* Returns 0 if the URL valid per RFC3986 and -1 otherwise. */ int check_submodule_url(const char *url); +/* + * Returns 1 if the URL is resolved against the superproject's remote, + * i.e. starts with "./", "../" or "^/", and 0 otherwise. + */ +int submodule_url_is_relative(const char *url); + /* * Note: these helper functions exist solely to maintain backward * compatibility with 'fetch' and 'update_clone' storing configuration in
diff --git a/t/helper/test-submodule.c b/t/helper/test-submodule.c
index ea9bef0904..f28daf52fe 100644
--- a/t/helper/test-submodule.c
+++ b/t/helper/test-submodule.c@@ -123,7 +123,12 @@ static int cmd__submodule_resolve_relative_url(int argc, const char **argv) if (!strcmp(up_path, "(null)")) up_path = NULL; - res = relative_url(remoteurl, url, up_path); + if (starts_with(url, "^/")) + res = root_relative_url(remoteurl, url); + else + res = relative_url(remoteurl, url, up_path); + if (!res) + die("cannot resolve '%s' against '%s'", url, remoteurl); puts(res); free(res); free(remoteurl);
diff --git a/t/meson.build b/t/meson.build
index 3ca7b27104..9d3ac94aae 100644
--- a/t/meson.build
+++ b/t/meson.build@@ -918,6 +918,7 @@ integration_tests = [ 't7424-submodule-mixed-ref-formats.sh', 't7425-submodule-gitdir-path-extension.sh', 't7426-submodule-get-default-remote.sh', + 't7427-submodule-root-relative-url.sh', 't7450-bad-git-dotfiles.sh', 't7500-commit-template-squash-signoff.sh', 't7501-commit-basic-functionality.sh',
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 56faf5fe73..c8618e8a30 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh@@ -7,6 +7,11 @@ test_description='Test various path utilities' . ./test-lib.sh +# Keep MSYS2 from turning "^/..." arguments into Windows paths. The value +# must not look like a path itself, or MSYS2 rewrites it for child processes. +MSYS2_ARG_CONV_EXCL='^' +export MSYS2_ARG_CONV_EXCL + norm_path() { expected=$(test-tool path-utils print_path "$2") test_expect_success $3 "normalize path: $1 => $2" "
@@ -435,6 +440,44 @@ test_submodule_relative_url "(null)" "user@host:path/to/repo" "../subrepo" "user test_submodule_relative_url "(null)" "user@host:repo" "../subrepo" "user@host:subrepo" test_submodule_relative_url "(null)" "user@host:repo" "../../subrepo" ".:subrepo" +test_submodule_relative_url "(null)" "https://example.com/me/super.git" "^/org/lib.git" "https://example.com/org/lib.git" +test_submodule_relative_url "(null)" "https://example.com/a/b/c/super.git" "^/org/lib.git" "https://example.com/org/lib.git" +test_submodule_relative_url "(null)" "https://example.com" "^/org/lib.git" "https://example.com/org/lib.git" +test_submodule_relative_url "(null)" "https://user@example.com:8443/me/super.git" "^/org/lib.git" "https://user@example.com:8443/org/lib.git" +test_submodule_relative_url "(null)" "https://example.com/me/super.git" "^/org/lib/" "https://example.com/org/lib" +test_submodule_relative_url "../" "https://example.com/me/super.git" "^/org/lib.git" "https://example.com/org/lib.git" +test_submodule_relative_url "(null)" "helper://example.com/me/super.git" "^/org/lib.git" "helper://example.com/org/lib.git" +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" +test_submodule_relative_url "(null)" "ssh://git@[::1]:2222/me/super.git" "^/org/lib.git" "ssh://git@[::1]:2222/org/lib.git" +test_submodule_relative_url "(null)" "ssh://example.com/~user/super.git" "^/org/lib.git" "ssh://example.com/org/lib.git" +test_submodule_relative_url "(null)" "git@example.com:me/super.git" "^/org/lib.git" "git@example.com:org/lib.git" +test_submodule_relative_url "(null)" "git@example.com:a/b/super.git" "^/org/lib.git" "git@example.com:org/lib.git" +test_submodule_relative_url "(null)" "git@example.com:/srv/git/super.git" "^/org/lib.git" "git@example.com:/org/lib.git" +test_submodule_relative_url "(null)" "example.com:~user/super.git" "^/org/lib.git" "example.com:org/lib.git" +test_submodule_relative_url "(null)" "git@[::1]:me/super.git" "^/org/lib.git" "git@[::1]:org/lib.git" +test_submodule_relative_url "(null)" "[::1]:me/super.git" "^/org/lib.git" "[::1]:org/lib.git" + +test_expect_success 'root-relative submodule url needs a remote with a host' ' + for remote in /srv/git/super.git ../super.git file:///srv/git/super.git \ + helper::https://example.com/super.git "[::1]" \ + https:///srv/git/super.git https://user@/super.git \ + ssh://:22/super.git :super.git git@:super.git "[]:super.git" + do + test_must_fail test-tool submodule resolve-relative-url \ + "(null)" "$remote" "^/org/lib.git" 2>err && + test_grep "cannot resolve" err || return 1 + done +' + +test_expect_success 'root-relative submodule url cannot change the kind of url' ' + for url in "^//evil.example.com/x.git" "^/:evil" + do + test_must_fail test-tool submodule resolve-relative-url \ + "(null)" host:/srv/super.git "$url" 2>err && + test_grep "must not start with" err || return 1 + done +' + test_expect_success 'match .gitmodules' ' test-tool path-utils is_dotgitmodules \ .gitmodules \
diff --git a/t/t7427-submodule-root-relative-url.sh b/t/t7427-submodule-root-relative-url.sh
new file mode 100755
index 0000000000..526dfec8d2
--- /dev/null
+++ b/t/t7427-submodule-root-relative-url.sh@@ -0,0 +1,190 @@ +#!/bin/sh + +test_description='submodule urls relative to the server root (^/)' + +. ./test-lib.sh + +# Keep MSYS2 from turning "^/..." arguments into Windows paths. The value +# must not look like a path itself, or MSYS2 rewrites it for child processes. +MSYS2_ARG_CONV_EXCL='^' +export MSYS2_ARG_CONV_EXCL + +test_expect_success 'setup' ' + git config --global protocol.file.allow always && + git config --global url."$(pwd)/server/".insteadOf https://example.com/ && + git config --global --add url."$(pwd)/server/".insteadOf git@example.com: && + + git init --bare server/org/dep.git && + git init --bare server/org/lib.git && + git init --bare server/org/team/super.git && + + git clone https://example.com/org/dep.git dep && + test_commit -C dep dep && + git -C dep push origin HEAD && + + git clone https://example.com/org/lib.git lib && + test_commit -C lib lib && + git -C lib submodule add ^/org/dep.git dep && + git -C lib commit -m "add dep" && + git -C lib push origin HEAD && + + git clone https://example.com/org/team/super.git super && + test_commit -C super super && + git -C super submodule add ^/org/lib.git lib && + git -C super commit -m "add lib" && + git -C super push origin HEAD && + + git clone --bare server/org/team/super.git server/me/super.git +' + +test_expect_success 'add records the url as given' ' + test_cmp_config -C super "^/org/lib.git" -f .gitmodules submodule.lib.url && + test_cmp_config -C super https://example.com/org/lib.git submodule.lib.url +' + +test_expect_success 'clone of the upstream in a subgroup' ' + git clone --recurse-submodules https://example.com/org/team/super.git upstream && + test_cmp_config -C upstream https://example.com/org/lib.git submodule.lib.url && + test_path_is_file upstream/lib/lib.t && + test_path_is_file upstream/lib/dep/dep.t +' + +test_expect_success 'clone of a fork at a different depth' ' + git clone --recurse-submodules https://example.com/me/super.git fork && + test_cmp_config -C fork https://example.com/org/lib.git submodule.lib.url && + test_cmp_config -C fork/lib https://example.com/org/dep.git submodule.dep.url && + test_path_is_file fork/lib/dep/dep.t +' + +test_expect_success 'clone over scp-like ssh uses ssh for submodules' ' + git clone --recurse-submodules git@example.com:me/super.git fork-ssh && + test_cmp_config -C fork-ssh git@example.com:org/lib.git submodule.lib.url && + test_cmp_config -C fork-ssh/lib git@example.com:org/dep.git submodule.dep.url && + test_path_is_file fork-ssh/lib/dep/dep.t +' + +test_expect_success 'sync follows a changed superproject remote' ' + git -C fork remote set-url origin ssh://git@example.com:2222/me/super.git && + git -C fork submodule sync && + test_cmp_config -C fork ssh://git@example.com:2222/org/lib.git submodule.lib.url && + test_cmp_config -C fork/lib ssh://git@example.com:2222/org/lib.git remote.origin.url +' + +test_expect_success 'get-default-remote finds the submodule remote by its ^/ url' ' + git -C fork/lib remote rename origin upstream && + git -C fork/lib remote add other https://example.com/org/dep.git && + echo upstream >expect && + git -C fork submodule--helper get-default-remote lib >actual && + test_cmp expect actual +' + +test_expect_success 'init fails when the remote has no host' ' + git clone server/me/super.git local && + test_must_fail git -C local submodule init 2>err && + test_grep "cannot resolve .* without a remote url that has a host" err +' + +test_expect_success 'init without a remote does not suggest the cwd fallback' ' + git init noremote && + git -C noremote config -f .gitmodules submodule.lib.path lib && + git -C noremote config -f .gitmodules submodule.lib.url "^/org/lib.git" && + git -C noremote update-index --add --cacheinfo \ + 160000,$(git -C lib rev-parse HEAD),lib && + test_must_fail git -C noremote submodule init 2>err && + test_grep "cannot resolve" err && + test_grep ! "authoritative upstream" err +' + +test_expect_success 'update fails when the remote has no host' ' + git -C noremote config submodule.lib.active true && + test_must_fail git -C noremote submodule update 2>err && + test_grep "cannot resolve .* without a remote url that has a host" err +' + +test_expect_success 'update --init fails even if the submodule is not updated' ' + test_must_fail git -C noremote -c submodule.lib.update=none \ + submodule update --init 2>err && + test_grep "cannot resolve" err +' + +test_expect_success 'sync fails when the remote has no host' ' + git -C local config submodule.lib.active true && + test_must_fail git -C local submodule sync 2>err && + test_grep "cannot resolve .* without a remote url that has a host" err +' + +test_expect_success 'add fails when the remote has no host' ' + test_must_fail git -C local submodule add "^/org/dep.git" dep 2>err && + test_grep "cannot resolve .* without a remote url that has a host" err && + test_path_is_missing local/dep +' + +test_expect_success 'clone --recurse-submodules stops when the remote has no host' ' + git init --bare server/org/mixed.git && + git init mixed && + git -C mixed config -f .gitmodules submodule.lib.path lib && + git -C mixed config -f .gitmodules submodule.lib.url "^/org/lib.git" && + git -C mixed config -f .gitmodules submodule.zz.path zz && + git -C mixed config -f .gitmodules submodule.zz.url ../dep.git && + git -C mixed update-index --add \ + --cacheinfo 160000,$(git -C lib rev-parse HEAD),lib \ + --cacheinfo 160000,$(git -C dep rev-parse HEAD),zz && + git -C mixed add .gitmodules && + git -C mixed commit -m mixed && + git -C mixed push "$(pwd)/server/org/mixed.git" HEAD && + + test_must_fail git clone --recurse-submodules server/org/mixed.git \ + mixed-clone 2>err && + test_grep "cannot resolve .* without a remote url that has a host" err && + test_path_is_missing mixed-clone/zz/dep.t +' + +test_expect_success 'nested ^/ fails when the submodule remote has no host' ' + git init --bare server/org/rel.git && + git init rel && + git -C rel config -f .gitmodules submodule.lib.path lib && + git -C rel config -f .gitmodules submodule.lib.url ../lib.git && + git -C rel update-index --add --cacheinfo \ + 160000,$(git -C lib rev-parse HEAD),lib && + git -C rel add .gitmodules && + git -C rel commit -m rel && + git -C rel push "$(pwd)/server/org/rel.git" HEAD && + + test_must_fail git clone --recurse-submodules server/org/rel.git \ + rel-clone 2>err && + test_grep "cannot resolve .*dep.git. without a remote url that has a host" err +' + +test_expect_success 'explicit url overrides a root-relative one' ' + git -C local config submodule.lib.url "$(pwd)/server/org/lib.git" && + git -C local submodule update --init && + test_path_is_file local/lib/lib.t && + git -C local submodule update --remote && + test_path_is_file local/lib/lib.t +' + +test_expect_success '^/ means the server root even if a local ^ directory exists' ' + git init literal && + mkdir -p "literal/^/org" && + git clone --bare server/org/lib.git "literal/^/org/lib.git" && + git -C literal config -f .gitmodules submodule.lib.path lib && + git -C literal config -f .gitmodules submodule.lib.url "^/org/lib.git" && + git -C literal update-index --add --cacheinfo \ + 160000,$(git -C lib rev-parse HEAD),lib && + + git -C literal remote add origin https://example.com/me/super.git && + git -C literal submodule init && + test_cmp_config -C literal https://example.com/org/lib.git submodule.lib.url && + + git -C literal config --unset submodule.lib.url && + git -C literal remote set-url origin "$(pwd)/server/me/super.git" && + test_must_fail git -C literal submodule init +' + +test_expect_success 'add from a subdirectory is refused' ' + mkdir super/sub && + test_must_fail git -C super/sub submodule add ^/org/dep.git dep 2>err && + test_grep "Relative path can only be used from the toplevel" err +' + +test_done
diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh
index 72c7f6f73b..0df7cb4a92 100755
--- a/t/t7450-bad-git-dotfiles.sh
+++ b/t/t7450-bad-git-dotfiles.sh@@ -49,12 +49,17 @@ test_expect_success 'check urls' ' ./bar/baz/foo.git https://example.com/foo.git http://example.com:80/deeper/foo.git + ^/org/foo.git EOF test-tool submodule check-url >actual <<-\EOF && ./bar/baz/foo.git https://example.com/foo.git http://example.com:80/deeper/foo.git + ^/org/foo.git + ^/%0ahost=example.com/foo.git + ^//evil.example.com/foo.git + ^/:foo.git -a./foo ../../..//test/foo.git ../../../../../:localhost:8080/foo.git
--
2.55.0