mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-09-22 23:00:29 +00:00
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:
committed by
gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
b85e76a86a
commit
6541729a18
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user