project: allow re-reading submodule gitlinks

A discovered submodule becomes a project whose revision is the gitlink
read from its parent at manifest load time, which is before the parent
has been fetched. Remember which gitlink a submodule was derived from,
and add a way to read those gitlinks again, so that callers can resolve
a submodule revision once its parent is up-to-date.

A revision that is set does not have to be fetched, so verify its
objects are really there. Without them a submodule that was removed
cannot be told apart from one that was never fetched, which the caller
has to know about.

Bug: 550074864
Change-Id: I4f35d53607126508eec6f02158a3fb604917a199
Signed-off-by: kimhappy <hwanhee.kim@laplacian.cc>
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/621661
Reviewed-by: Brian Gan <brgan@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
This commit is contained in:
kimhappy authored and gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com committed 2026-08-25 19:39:51 -07:00
1 parent b85886fa9f
commit 3f1775607f
2 files changed
+75

No files matched your search

+36
View File
@@ -568,6 +568,7 @@ class Project:
parent=None,
use_git_worktrees=False,
is_derived=False,
gitlink_path: Optional[str] = None,
dest_branch=None,
optimized_fetch=False,
retry_fetches=0,
@@ -597,6 +598,8 @@ class Project:
use_git_worktrees: Whether to use `git worktree` for this project.
is_derived: False if the project was explicitly defined in the
manifest; True if the project is a discovered submodule.
gitlink_path: For a discovered submodule, its path inside the
parent project.
dest_branch: The branch to which to push changes for review by
default.
optimized_fetch: If True, when a project is set to a sha1 revision,
@@ -624,6 +627,7 @@ class Project:
# See the XmlManifest init code for more info.
self.use_git_worktrees = use_git_worktrees
self.is_derived = is_derived
self.gitlink_path = gitlink_path
self.optimized_fetch = optimized_fetch
self.retry_fetches = max(0, retry_fetches)
self.subprojects = []
@@ -2573,6 +2577,37 @@ class Project:
return []
return get_submodules(self.gitdir, rev)
def GetSubmoduleRevisions(self) -> Optional[Dict[str, str]]:
"""Read the gitlinks of our submodules at our current revision.
Discovered submodules are derived from the revision their parent was
at when the manifest was loaded, which is before it gets fetched.
Once it is up-to-date, its gitlinks have to be read again, otherwise
the submodules would be synced to the revisions of the previous sync.
Returns:
The revision of every submodule, keyed by its path inside this
project, or None if our revision is not available locally. A
revision that is merely set does not have to be fetched yet, and
without its objects a removed submodule cannot be told apart from
one that was never fetched.
"""
try:
rev = self.GetRevisionId()
self.bare_git.rev_list(
"-1",
"--missing=allow-any",
f"{rev}^0",
"--",
log_as_error=False,
)
except (GitError, ManifestInvalidRevisionError):
return None
return {
path: sha for sha, path, _url, _shallow in self._GetSubmodules()
}
def GetDerivedSubprojects(self):
result = []
if not self.Exists:
@@ -2620,6 +2655,7 @@ class Project:
parent=self,
clone_depth=clone_depth,
is_derived=True,
gitlink_path=path,
)
result.append(subproject)
result.extend(subproject.GetDerivedSubprojects())
+39
View File
@@ -948,6 +948,45 @@ def _create_mock_project(
return proj
class GetSubmoduleRevisions(unittest.TestCase):
"""Tests for Project.GetSubmoduleRevisions."""
def test_gitlinks_are_keyed_by_their_path(self) -> None:
with utils_for_test.TempGitTree() as tempdir:
parent = _create_mock_project(tempdir)
parent.GetRevisionId = mock.MagicMock(return_value="cafe1234")
parent._GetSubmodules = mock.MagicMock(
return_value=[("1234abcd", "sub", "http://example.com/sub", "")]
)
self.assertEqual(
parent.GetSubmoduleRevisions(), {"sub": "1234abcd"}
)
def test_none_when_the_revision_is_not_fetched(self) -> None:
with utils_for_test.TempGitTree() as tempdir:
parent = _create_mock_project(tempdir)
parent.GetRevisionId = mock.MagicMock(return_value="cafe1234")
parent.bare_git.rev_list = mock.MagicMock(
side_effect=error.GitError("missing object")
)
parent._GetSubmodules = mock.MagicMock()
self.assertIsNone(parent.GetSubmoduleRevisions())
parent._GetSubmodules.assert_not_called()
def test_none_without_a_revision(self) -> None:
with utils_for_test.TempGitTree() as tempdir:
parent = _create_mock_project(tempdir)
parent.GetRevisionId = mock.MagicMock(
side_effect=error.ManifestInvalidRevisionError("no revision")
)
parent._GetSubmodules = mock.MagicMock()
self.assertIsNone(parent.GetSubmoduleRevisions())
parent._GetSubmodules.assert_not_called()
class StatelessSyncTests(unittest.TestCase):
"""Tests for stateless sync strategy."""