mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-20 20:28:25 +00:00
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:
1 parent
d27034bf62
commit
5f378458d2
2 files changed
+133
-11
No files matched your search
+27
-11
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user