project: reuse loaded refs when resolving HEAD

When the regular checkout has already loaded GitRefs, derive its current
branch and HEAD object from that snapshot instead of spawning
symbolic-ref or rev-parse again. Keep linked Git worktrees on their
worktree-specific HEAD path because the shared repository HEAD is not
authoritative for them.

Bug: 543851900
Bug: 553599402
Change-Id: Iec456be5f3c279865b4165a75d1c6df36e331dbc
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/623181
Commit-Queue: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
diff --git a/git_refs.py b/git_refs.py
index 42af8e0..0811377 100644
--- a/git_refs.py
+++ b/git_refs.py
@@ -48,6 +48,11 @@
         self._EnsureLoaded()
         return self._symref.get(HEAD) or self._phyref.get(HEAD, "")
 
+    @property
+    def is_loaded(self) -> bool:
+        """Whether a ref snapshot has already been loaded."""
+        return self._phyref is not None
+
     def get(self, name):
         try:
             return self.all[name]
diff --git a/project.py b/project.py
index b4d77fe..f4b4d5d 100644
--- a/project.py
+++ b/project.py
@@ -770,15 +770,28 @@
         work_git is otheriwse inaccessible (e.g. an incomplete sync).
         """
         try:
-            b = self.work_git.GetHead()
+            b = self._GetHead()
         except NoManifestException:
             # If the local checkout is in a bad state, don't barf.  Let the
             # callers process this like the head is unreadable.
             return None
-        if b.startswith(R_HEADS):
+        if b and b.startswith(R_HEADS):
             return b[len(R_HEADS) :]
         return None
 
+    def _GetHead(self) -> Optional[str]:
+        """Return worktree HEAD, reusing a compatible loaded ref snapshot."""
+        if not self.work_git:
+            return None
+        # Git worktrees keep the checkout's HEAD in the worktree admin dir,
+        # while bare_ref reads the shared repository.  Its HEAD is not the
+        # checked-out worktree's HEAD and must not be reused here.
+        if not self.use_git_worktrees and self.bare_ref.is_loaded:
+            head = self.bare_ref.head
+            if head:
+                return head
+        return self.work_git.GetHead()
+
     def IsRebaseInProgress(self):
         """Returns true if a rebase or "am" is in progress"""
         # "rebase-apply" is used for "git rebase".
@@ -877,8 +890,8 @@
 
     def GetBranches(self):
         """Get all existing local branches."""
-        current = self.CurrentBranch
         all_refs = self._allrefs
+        current = self.CurrentBranch
         heads = {}
 
         for name, ref_id in all_refs.items():
@@ -1766,6 +1779,10 @@
         Returns None if worktree is not checked out or HEAD cannot be resolved.
         """
         if self.work_git:
+            if not self.use_git_worktrees and self.bare_ref.is_loaded:
+                head = self.bare_ref.get(HEAD)
+                if head:
+                    return head
             try:
                 return self.work_git.rev_parse("HEAD")
             except GitError:
@@ -1879,8 +1896,8 @@
             if p.Wait() != 0:
                 logger.warning("warn: %s: stateless gc failed", self.name)
 
-        head = self.work_git.GetHead()
-        if head.startswith(R_HEADS):
+        head = self._GetHead()
+        if head and head.startswith(R_HEADS):
             branch = head[len(R_HEADS) :]
             try:
                 head = all_refs[head]
diff --git a/tests/test_git_refs.py b/tests/test_git_refs.py
index 183ed9d..ab7bdbe 100644
--- a/tests/test_git_refs.py
+++ b/tests/test_git_refs.py
@@ -59,6 +59,7 @@
     branch = _run(repo, "symbolic-ref", "--short", "HEAD")
     head = _run(repo, "rev-parse", "HEAD")
     assert refs.symref("HEAD") == f"refs/heads/{branch}"
+    assert refs.head == f"refs/heads/{branch}"
     assert refs.get("HEAD") == head
     assert refs.get(f"refs/heads/{branch}") == head
 
diff --git a/tests/test_project.py b/tests/test_project.py
index e750a88..08d2806 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -386,6 +386,78 @@
             proj.work_git.checkout("HEAD~0")
             self.assertEqual(commit_sha, proj.GetHeadRevisionId())
 
+    def test_get_branches_reuses_ref_snapshot_for_current_branch(self) -> None:
+        """GetBranches derives the current branch from the loaded refs."""
+        with utils_for_test.TempGitTree() as tempdir:
+            proj = _create_mock_project(tempdir)
+            proj.work_git = mock.MagicMock()
+            proj.bare_ref = mock.MagicMock()
+            proj.bare_ref.all = {
+                "HEAD": "1" * 40,
+                "refs/heads/topic": "1" * 40,
+            }
+            proj.bare_ref.head = "refs/heads/topic"
+
+            branches = proj.GetBranches()
+
+            self.assertTrue(branches["topic"].current)
+            proj.work_git.GetHead.assert_not_called()
+
+    def test_get_branches_reads_worktree_head_for_git_worktrees(self) -> None:
+        """A shared repository HEAD is not a linked worktree's HEAD."""
+        with utils_for_test.TempGitTree() as tempdir:
+            proj = _create_mock_project(tempdir)
+            proj.use_git_worktrees = True
+            proj.work_git = mock.MagicMock()
+            proj.work_git.GetHead.return_value = "refs/heads/worktree"
+            proj.bare_ref = mock.MagicMock()
+            proj.bare_ref.all = {
+                "HEAD": "1" * 40,
+                "refs/heads/common": "1" * 40,
+                "refs/heads/worktree": "1" * 40,
+            }
+            proj.bare_ref.head = "refs/heads/common"
+
+            branches = proj.GetBranches()
+
+            self.assertFalse(branches["common"].current)
+            self.assertTrue(branches["worktree"].current)
+            proj.work_git.GetHead.assert_called_once_with()
+
+    def test_get_branches_tolerates_unreadable_head(self) -> None:
+        """An incomplete checkout still reports its local branches."""
+        with utils_for_test.TempGitTree() as tempdir:
+            proj = _create_mock_project(tempdir)
+            proj.bare_ref = mock.MagicMock()
+            proj.bare_ref.all = {"refs/heads/topic": "1" * 40}
+
+            with mock.patch.object(
+                proj,
+                "_GetHead",
+                side_effect=error.NoManifestException("HEAD", "unreadable"),
+            ):
+                branches = proj.GetBranches()
+
+            self.assertFalse(branches["topic"].current)
+
+    def test_get_head_returns_none_when_work_git_is_none(self) -> None:
+        """Bare and mirror checkouts without work_git do not crash."""
+        with utils_for_test.TempGitTree() as tempdir:
+            proj = _create_mock_project(tempdir)
+            proj.work_git = None
+
+            self.assertIsNone(proj._GetHead())
+            self.assertIsNone(proj.CurrentBranch)
+
+    def test_current_branch_returns_none_when_get_head_returns_none(
+        self,
+    ) -> None:
+        """CurrentBranch safely returns None when _GetHead returns None."""
+        with utils_for_test.TempGitTree() as tempdir:
+            proj = _create_mock_project(tempdir)
+            with mock.patch.object(proj, "_GetHead", return_value=None):
+                self.assertIsNone(proj.CurrentBranch)
+
     @unittest.skipUnless(
         utils_for_test.supports_reftable(),
         "git reftable support is required for this test",
@@ -1621,12 +1693,11 @@
             proj.IsCherryPickInProgress = mock.MagicMock(return_value=False)
             proj.bare_ref = mock.MagicMock()
             proj.bare_ref.all = {}
+            proj.bare_ref.head = "5678abcd"
             proj.GetRevisionId = mock.MagicMock(return_value="1234abcd")
             proj._CopyAndLinkFiles = mock.MagicMock()
 
             proj.work_git = mock.MagicMock()
-            proj.work_git.GetHead.return_value = "5678abcd"
-
             syncbuf = project.SyncBuffer(proj.config)
 
             with mock.patch("project.GitCommand") as mock_git_cmd: