project: skip manifest default fallback for MetaProjects

Avoid loading manifest.xml during repo init when syncing a manifest
commit SHA. Override _GetUpstreamFallback and _SharingProjectHasShallow
in MetaProject to prevent premature manifest parsing.

Bug: 544041102
Change-Id: I7aa54a7c1282e5bfe811d59e977b44163a37653c
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/617081
Commit-Queue: Gavin Mak <gavinmak@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
diff --git a/project.py b/project.py
index 7186e58..67e3fe6 100644
--- a/project.py
+++ b/project.py
@@ -2859,12 +2859,17 @@
 
     def _GetUpstreamFallback(self) -> Optional[str]:
         """Resolve a fallback upstream ref when revisionExpr is a SHA-1."""
-        for cand in (
-            self.dest_branch,
-            self.manifest.default.upstreamExpr,
-            self.manifest.default.destBranchExpr,
-            self.manifest.default.revisionExpr,
-        ):
+        default = self.manifest.default
+        candidates = [self.dest_branch]
+        if default:
+            candidates.extend(
+                (
+                    default.upstreamExpr,
+                    default.destBranchExpr,
+                    default.revisionExpr,
+                )
+            )
+        for cand in candidates:
             if cand and not IsId(cand):
                 return cand
         return None
@@ -4737,6 +4742,20 @@
         # before manifest.xml has been linked into .repo/.
         return False
 
+    def _GetUpstreamFallback(self) -> Optional[str]:
+        # MetaProjects (the manifest repo and repo itself) do not have
+        # defaults in a manifest. Returning None here also avoids
+        # loading the manifest during `repo init`, before manifest.xml
+        # has been linked into .repo/.
+        return None
+
+    def _SharingProjectHasShallow(self) -> bool:
+        # MetaProjects (the manifest repo and repo itself) are never
+        # shared with other projects in the manifest. Returning False
+        # here also avoids loading the manifest during `repo init`,
+        # before manifest.xml has been linked into .repo/.
+        return False
+
     @property
     def HasChanges(self):
         """Has the remote received new commits not yet checked out?"""
diff --git a/tests/test_project.py b/tests/test_project.py
index 19da4f0..cbffec8 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -742,6 +742,35 @@
             )
             self.assertFalse(os.path.exists(manifest_path))
 
+    def test_get_upstream_fallback_metaproject_skips_manifest_load(
+        self,
+    ) -> None:
+        """MetaProjects must not parse manifest.xml during upstream fallback."""
+        with utils_for_test.TempGitTree() as tempdir:
+            fakeproj = self.setUpManifest(tempdir)
+            manifest_path = os.path.join(
+                tempdir, ".repo", manifest_xml.MANIFEST_FILE_NAME
+            )
+            self.assertFalse(os.path.exists(manifest_path))
+
+            self.assertIsNone(fakeproj._GetUpstreamFallback())
+            self.assertFalse(os.path.exists(manifest_path))
+
+    def test_sharing_project_has_shallow_metaproject_skips_manifest_load(
+        self,
+    ) -> None:
+        """MetaProjects must not parse manifest.xml during sharing shallow
+        check."""
+        with utils_for_test.TempGitTree() as tempdir:
+            fakeproj = self.setUpManifest(tempdir)
+            manifest_path = os.path.join(
+                tempdir, ".repo", manifest_xml.MANIFEST_FILE_NAME
+            )
+            self.assertFalse(os.path.exists(manifest_path))
+
+            self.assertFalse(fakeproj._SharingProjectHasShallow())
+            self.assertFalse(os.path.exists(manifest_path))
+
     def test_sync_use_local_gitdirs_worktree_conflict(self):
         """Test that --use-local-gitdirs conflicts with --worktree."""
         with utils_for_test.TempGitTree() as tempdir:
@@ -1403,6 +1432,61 @@
                     "+refs/heads/*:refs/remotes/origin/*", cmd_args
                 )
 
+    def test_remote_fetch_sha1_metaproject_without_manifest_xml(self) -> None:
+        """Test MetaProject _RemoteFetch with SHA-1 fetches all branches."""
+        sha = "4f8a3c0000000000000000000000000000000000"
+        with utils_for_test.TempGitTree() as tempdir:
+            repodir = os.path.join(tempdir, ".repo")
+            manifest_dir = os.path.join(repodir, "manifests")
+            manifest_file = os.path.join(
+                repodir, manifest_xml.MANIFEST_FILE_NAME
+            )
+            os.mkdir(repodir)
+            os.mkdir(manifest_dir)
+            manifest = manifest_xml.XmlManifest(repodir, manifest_file)
+            proj = project.ManifestProject(
+                manifest,
+                "test/manifest",
+                os.path.join(tempdir, ".git"),
+                tempdir,
+            )
+            proj.revisionExpr = sha
+            proj.upstream = None
+            proj._CheckForImmutableRevision = mock.MagicMock(return_value=False)
+
+            mock_remote = mock.MagicMock()
+            mock_remote.name = "origin"
+
+            def _to_local(r: str) -> str:
+                if r.startswith("refs/heads/"):
+                    return "refs/remotes/origin/" + r[11:]
+                return r
+
+            mock_remote.ToLocal.side_effect = _to_local
+            mock_remote.PreConnectFetch.return_value = True
+            proj.GetRemote = mock.MagicMock(return_value=mock_remote)
+
+            with mock.patch("project.GitCommand") as mock_git_cmd:
+                mock_cmd_instance = mock.MagicMock()
+                mock_cmd_instance.Wait.return_value = 0
+                mock_git_cmd.return_value = mock_cmd_instance
+
+                res = proj._RemoteFetch(current_branch_only=True)
+
+                self.assertTrue(res)
+                mock_git_cmd.assert_called_once()
+                cmd_args = mock_git_cmd.call_args[0][1]
+                self.assertIn("+refs/heads/*:refs/remotes/origin/*", cmd_args)
+
+    def test_remote_fetch_sha1_none_manifest_default(self) -> None:
+        """Test _GetUpstreamFallback when manifest.default is None."""
+        sha = "4f8a3c0000000000000000000000000000000000"
+        with utils_for_test.TempGitTree() as tempdir:
+            proj = self._get_project(tempdir, revisionExpr=sha)
+            proj.dest_branch = None
+            proj.manifest.default = None
+            self.assertIsNone(proj._GetUpstreamFallback())
+
 
 class GetEnvVarsTests(unittest.TestCase):
     """Tests for GetEnvVars project environment variable generation."""