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>
This commit is contained in:
Gavin Mak
2026-08-31 12:33:39 -07:00
committed by gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent b85e76a86a
commit 6541729a18
4 changed files with 101 additions and 7 deletions
+5
View File
@@ -48,6 +48,11 @@ class GitRefs:
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]
+22 -5
View File
@@ -770,15 +770,28 @@ class Project:
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 @@ class Project:
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 @@ class Project:
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 @@ class Project:
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]
+1
View File
@@ -59,6 +59,7 @@ def test_reads_refs(tmp_path, reftable):
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
+73 -2
View File
@@ -386,6 +386,78 @@ class ProjectTests(unittest.TestCase):
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 @@ class StatelessSyncTests(unittest.TestCase):
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: