project: reuse ref OIDs in branch maintenance

Load refs before resolving the current branch, compare cached HEAD and
manifest OIDs instead of walking their symmetric difference, and reuse
the snapshot when preserving bare HEAD. Apply the same HEAD reuse to
abandon and MetaProject checks, and bound the latter revision walk to
one commit.

Bug: 553599402
Change-Id: I715e65dcfa02c0df4aaa08da5b7bfbb04ca17725
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/623183
Reviewed-by: Brian Gan <brgan@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
This commit is contained in:
Gavin Mak authored and gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com committed 2026-08-31 13:25:40 -07:00
1 parent d27034bf62
commit 5f378458d2
2 files changed
+133 -11

No files matched your search

+27 -11
View File
@@ -1754,7 +1754,9 @@ class Project:
for linkfile in self.linkfiles:
linkfile._Link()
def GetCommitRevisionId(self):
def GetCommitRevisionId(
self, all_refs: Optional[Dict[str, str]] = None
) -> str:
"""Get revisionId of a commit.
Use this method instead of GetRevisionId to get the id of the commit
@@ -1764,7 +1766,9 @@ class Project:
if self.revisionId:
return self.revisionId
if not self.revisionExpr.startswith(R_TAGS):
return self.GetRevisionId(self._allrefs)
if all_refs is None:
all_refs = self._allrefs
return self.GetRevisionId(all_refs)
try:
return self.bare_git.ResolveCommit(self.revisionExpr)
@@ -2428,7 +2432,7 @@ class Project:
# Doesn't exist
return None
head = self.work_git.GetHead()
head = self._GetHead()
if head == rev:
# We can't destroy the branch while we are sitting
# on it. Switch to a detached HEAD.
@@ -2450,9 +2454,9 @@ class Project:
def PruneHeads(self):
"""Prune any topic branches already merged into upstream."""
cb = self.CurrentBranch
kill = []
left = self._allrefs
cb = self.CurrentBranch
for name in left.keys():
if name.startswith(R_HEADS):
name = name[len(R_HEADS) :]
@@ -2464,17 +2468,25 @@ class Project:
if not kill and not cb:
return []
rev = self.GetRevisionId(left)
rev = self.GetCommitRevisionId(left)
head = left.get(R_HEADS + cb) if cb is not None else None
if (
cb is not None
and not self._revlist(HEAD + "..." + rev)
and head == rev
and not self.IsDirty(consider_untracked=False)
):
self.work_git.DetachHead(HEAD)
kill.append(cb)
if kill:
old = self.bare_git.GetHead()
if not self.use_git_worktrees:
old = (
head
if cb in kill
else (self.bare_ref.head or self.bare_git.GetHead())
)
else:
old = self.bare_git.GetHead()
try:
self.bare_git.DetachHead(rev)
@@ -2489,7 +2501,11 @@ class Project:
if IsId(old):
self.bare_git.DetachHead(old)
else:
self.bare_git.SetHead(old)
branch = (
old[len(R_HEADS) :] if old.startswith(R_HEADS) else old
)
if branch not in kill:
self.bare_git.SetHead(old)
left = self._allrefs
for branch in kill:
@@ -4930,8 +4946,8 @@ class MetaProject(Project):
all_refs = self.bare_ref.all
revid = self.GetRevisionId(all_refs)
head = self.work_git.GetHead()
if head.startswith(R_HEADS):
head = self._GetHead()
if head and head.startswith(R_HEADS):
try:
head = all_refs[head]
except KeyError:
@@ -4939,7 +4955,7 @@ class MetaProject(Project):
if revid == head:
return False
elif self._revlist(not_rev(HEAD), revid):
elif self._revlist("-1", not_rev(HEAD), revid):
return True
return False
+106
View File
@@ -501,6 +501,97 @@ class ProjectTests(unittest.TestCase):
with mock.patch.object(proj, "_GetHead", return_value=None):
self.assertIsNone(proj.CurrentBranch)
def test_prune_heads_reuses_refs_and_avoids_revision_walk(self) -> None:
"""Pruning uses snapshot OIDs to recognize an exact manifest HEAD."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
Path(tempdir, "tracked").write_text("initial")
proj.work_git.add("tracked")
proj.work_git.commit("-m", "initial")
revision = proj.work_git.rev_parse("HEAD")
proj.work_git.branch("merged")
proj.work_git.checkout("-b", "unmerged")
Path(tempdir, "tracked").write_text("topic")
proj.work_git.commit("-am", "topic")
proj.work_git.checkout("main")
proj.work_git.config("branch.unmerged.remote", ".")
proj.work_git.config("branch.unmerged.merge", "refs/heads/main")
proj.revisionId = revision
proj.bare_git = project.Project._GitGetByExec(
proj, bare=True, gitdir=proj.gitdir
)
proj._revlist = mock.MagicMock(
side_effect=AssertionError("unexpected revision walk")
)
kept = proj.PruneHeads()
self.assertEqual(["unmerged"], [branch.name for branch in kept])
self.assertEqual("", proj.bare_ref.get("refs/heads/merged"))
proj._revlist.assert_not_called()
def test_prune_heads_compares_peeled_tag_commit(self) -> None:
"""A branch at an annotated manifest tag can be pruned."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(
tempdir, revisionExpr="refs/tags/release"
)
Path(tempdir, "tracked").write_text("initial")
proj.work_git.add("tracked")
proj.work_git.commit("-m", "initial")
proj.work_git.tag("-a", "release", "-m", "release")
proj.bare_git = project.Project._GitGetByExec(
proj, bare=True, gitdir=proj.gitdir
)
kept = proj.PruneHeads()
self.assertEqual([], kept)
self.assertIsNone(proj.CurrentBranch)
self.assertEqual("", proj.bare_ref.get("refs/heads/main"))
def test_prune_heads_preserves_detached_head_when_current_branch_pruned(
self,
) -> None:
"""Pruning current branch keeps HEAD detached at target commit."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
Path(tempdir, "tracked").write_text("initial")
proj.work_git.add("tracked")
proj.work_git.commit("-m", "initial")
revision = proj.work_git.rev_parse("HEAD")
proj.revisionId = revision
proj.bare_git = project.Project._GitGetByExec(
proj, bare=True, gitdir=proj.gitdir
)
proj.bare_ref.head
kept = proj.PruneHeads()
self.assertEqual([], kept)
self.assertIsNone(proj.CurrentBranch)
self.assertEqual("", proj.bare_ref.get("refs/heads/main"))
self.assertEqual(revision, proj.bare_git.rev_parse("HEAD"))
def test_prune_heads_with_git_worktrees_preserves_bare_head(self) -> None:
"""Worktree pruning does not detach the shared bare repository HEAD."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
Path(tempdir, "tracked").write_text("initial")
proj.work_git.add("tracked")
proj.work_git.commit("-m", "initial")
revision = proj.work_git.rev_parse("HEAD")
proj.revisionId = revision
proj.use_git_worktrees = True
proj.bare_git = mock.MagicMock()
proj.bare_git.GetHead.return_value = "refs/heads/manifest"
kept = proj.PruneHeads()
self.assertEqual([], kept)
proj.bare_git.SetHead.assert_called_once_with("refs/heads/manifest")
proj.bare_git.DetachHead.assert_called_once_with(revision)
@unittest.skipUnless(
utils_for_test.supports_reftable(),
"git reftable support is required for this test",
@@ -1605,6 +1696,21 @@ def test_default_branch_fallback(
project._DefaultBranchFallback.cache_clear()
def test_metaproject_has_changes_bounds_revision_walk() -> None:
"""MetaProject only asks whether one remote-only commit exists."""
meta = mock.MagicMock()
meta.remote = mock.sentinel.remote
meta.revisionExpr = "refs/remotes/origin/main"
meta.bare_ref.all = {"HEAD": "local"}
meta.GetRevisionId.return_value = "remote"
meta._GetHead.return_value = "local"
meta._revlist.return_value = ["remote"]
assert project.MetaProject.HasChanges.fget(meta)
meta._revlist.assert_called_once_with("-1", "^HEAD", "remote")
def _create_mock_project(
tempdir,
use_local_gitdirs=False,