From 1db50e49fb7f99a61f7a3ceee3e1f6289208e249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Malmstr=C3=B6m?= Date: Mon, 4 May 2026 14:16:40 +0200 Subject: [PATCH] Add support for self referencing submodules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When working with relative submodule paths, The "./" needs special handling similar to "../". See information on: https://git-scm.com/docs/git-submodule Which currently states: " 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 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 when following the rules for relative URLs - because the evaluation of relative URLs in Git is identical to that of relative directories)." The implementation also was not handling file/directory names starting with "." or "..". Explicitly look for "./" and "../" instead. Change-Id: I8ae68d61fb0cbb1624183b175236e98a36e4afdb Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/579182 Reviewed-by: Mike Frysinger Reviewed-by: Gavin Mak Commit-Queue: Josef Malmstrom Tested-by: Josef Malmstrom --- project.py | 2 +- tests/test_project.py | 82 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/project.py b/project.py index 5f2a98e18..b86367317 100644 --- a/project.py +++ b/project.py @@ -2448,7 +2448,7 @@ class Project: result.extend(project.GetDerivedSubprojects()) continue - if url.startswith(".."): + if url.startswith(("./", "../")): url = urllib.parse.urljoin("%s/" % self.remote.url, url) remote = RemoteSpec( self.remote.name, diff --git a/tests/test_project.py b/tests/test_project.py index 728d5a54c..131dccd9f 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -127,6 +127,88 @@ class ProjectTests(unittest.TestCase): ).strip() self.assertEqual(expected, fakeproj.work_git.GetHead()) + def _get_derived_subproject_url(self, submodule_url): + with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir: + + class FakeManifest: + def __init__(self, topdir): + self.topdir = topdir + self.globalConfig = None + self.is_multimanifest = False + self.path_prefix = "" + self.paths = {} + + def GetSubprojectName(self, parent, path): + return path + + def GetSubprojectPaths(self, parent, name, path): + relpath = path + worktree = os.path.join(self.topdir, path) + gitdir = os.path.join(self.topdir, f"{path}.git") + objdir = os.path.join(self.topdir, f"{path}.obj") + os.makedirs(worktree, exist_ok=True) + os.makedirs(gitdir, exist_ok=True) + os.makedirs(objdir, exist_ok=True) + return relpath, worktree, gitdir, objdir + + manifest = FakeManifest(tempdir) + worktree = os.path.join(tempdir, "parent") + gitdir = os.path.join(tempdir, "parent.git") + objdir = os.path.join(tempdir, "parent.obj") + os.makedirs(worktree) + os.makedirs(gitdir) + os.makedirs(objdir) + + parent = project.Project( + manifest=manifest, + name="parent", + remote=project.RemoteSpec( + "origin", url="https://example.com/platform/superproject" + ), + gitdir=gitdir, + objdir=objdir, + worktree=worktree, + relpath="parent", + revisionExpr="refs/heads/main", + revisionId=None, + ) + + def fake_get_submodules(current): + if current is parent: + return [("subrev", "child", submodule_url, "false")] + return [] + + with mock.patch.object( + project.Project, "_GetSubmodules", autospec=True + ) as get_submodules: + get_submodules.side_effect = fake_get_submodules + result = parent.GetDerivedSubprojects() + + self.assertEqual(1, len(result)) + return result[0].remote.url + + def test_derived_subproject_joins_only_git_relative_urls(self): + tests = ( + ( + "./submodule", + "https://example.com/platform/superproject/submodule", + ), + ("../sibling", "https://example.com/platform/sibling"), + ) + for submodule_url, expected in tests: + with self.subTest(submodule_url=submodule_url): + self.assertEqual( + expected, self._get_derived_subproject_url(submodule_url) + ) + + def test_derived_subproject_leaves_dot_prefixed_names_unchanged(self): + for submodule_url in (".foo", "..bar"): + with self.subTest(submodule_url=submodule_url): + self.assertEqual( + submodule_url, + self._get_derived_subproject_url(submodule_url), + ) + class CopyLinkTestCase(unittest.TestCase): """TestCase for stub repo client checkouts.