From f6f5946422097f2559cf3bc39a69d5fddc838ab8 Mon Sep 17 00:00:00 2001 From: Gavin Mak Date: Tue, 25 Aug 2026 08:57:34 -0700 Subject: [PATCH] project: avoid unnecessary sync revision enumeration Skip the detached-history walk when its count will not be printed, ask rev-list --count for upstream gain, and cap the published-but-unmerged existence probe at one commit. These preserve the decisions while reducing traversal and output work. Bug: 553599402 Change-Id: I30f769c9aa5269428ece15ffcb45c50993d7c4b8 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/634004 Commit-Queue: Gavin Mak Tested-by: Gavin Mak Reviewed-by: Brian Gan --- project.py | 29 +++++++------ tests/test_project.py | 97 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/project.py b/project.py index a10de9904..75d722db7 100644 --- a/project.py +++ b/project.py @@ -2053,13 +2053,13 @@ class Project: def Sync_LocalHalf( self, - syncbuf, - force_sync=False, - force_checkout=False, - force_rebase=False, - submodules=False, - verbose=False, - ): + syncbuf: Any, + force_sync: bool = False, + force_checkout: bool = False, + force_rebase: bool = False, + submodules: bool = False, + verbose: bool = False, + ) -> None: """Perform only the local IO portion of the sync process. Network access is not required. @@ -2203,9 +2203,11 @@ class Project: self._CopyAndLinkFiles() return else: - lost = self._revlist(not_rev(revid), HEAD) - if lost and verbose: - syncbuf.info(self, "discarding %d commits", len(lost)) + if verbose: + lost_output = self._revlist("--count", not_rev(revid), HEAD) + lost = int(lost_output[0]) if lost_output else 0 + if lost: + syncbuf.info(self, "discarding %d commits", lost) try: _checkout() @@ -2244,7 +2246,8 @@ class Project: self._CopyAndLinkFiles() return - upstream_gain = self._revlist(not_rev(HEAD), revid) + gain_output = self._revlist("--count", not_rev(HEAD), revid) + upstream_gain = int(gain_output[0]) if gain_output else 0 # See if we can perform a fast forward merge. This can happen if our # branch isn't in the exact same state as we last published. @@ -2258,7 +2261,7 @@ class Project: pub = self.WasPublished(branch.name, all_refs) if pub: - not_merged = self._revlist(not_rev(revid), pub) + not_merged = self._revlist("-1", not_rev(revid), pub) if not_merged: if upstream_gain: if force_rebase: @@ -2274,7 +2277,7 @@ class Project: "branch %s is published (but not merged) and " "is now %d commits behind. Fix this manually " "or rerun with the --rebase option to force a " - "rebase." % (branch.name, len(upstream_gain)), + "rebase." % (branch.name, upstream_gain), project=self.name, ) ) diff --git a/tests/test_project.py b/tests/test_project.py index ab20105fd..d02dabf3b 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -2248,6 +2248,97 @@ class StatelessSyncTests(unittest.TestCase): ) proj._CopyAndLinkFiles.assert_called_once_with() + def test_sync_local_half_skips_unused_detached_history_walk(self) -> None: + """Non-verbose detached sync does not enumerate discarded commits.""" + with utils_for_test.TempGitTree() as tempdir: + proj = self._get_project(tempdir) + proj._InitWorkTree = mock.MagicMock() + proj.CleanPublishedCache = mock.MagicMock() + proj.GetRevisionId = mock.MagicMock(return_value="new") + proj._Checkout = mock.MagicMock() + proj._CopyAndLinkFiles = mock.MagicMock() + proj.IsRebaseInProgress = mock.MagicMock(return_value=False) + proj.IsCherryPickInProgress = mock.MagicMock(return_value=False) + proj._revlist = mock.MagicMock() + proj.bare_ref = mock.MagicMock() + proj.bare_ref.all = {"HEAD": "old"} + proj.bare_ref.head = "old" + proj.work_git = mock.MagicMock() + proj.work_git.GetHead.return_value = "old" + + syncbuf = project.SyncBuffer(proj.config) + proj.Sync_LocalHalf(syncbuf, verbose=False) + + proj._revlist.assert_not_called() + proj._Checkout.assert_called_once_with( + "new", force_checkout=False, quiet=True + ) + + def test_sync_local_half_verbose_detached_uses_count(self) -> None: + """Verbose detached sync queries commit count with --count.""" + with utils_for_test.TempGitTree() as tempdir: + proj = self._get_project(tempdir) + proj._InitWorkTree = mock.MagicMock() + proj.CleanPublishedCache = mock.MagicMock() + proj.GetRevisionId = mock.MagicMock(return_value="new") + proj._Checkout = mock.MagicMock() + proj._CopyAndLinkFiles = mock.MagicMock() + proj.IsRebaseInProgress = mock.MagicMock(return_value=False) + proj.IsCherryPickInProgress = mock.MagicMock(return_value=False) + proj._revlist = mock.MagicMock(return_value=["3"]) + proj.bare_ref = mock.MagicMock() + proj.bare_ref.all = {"HEAD": "old"} + proj.bare_ref.head = "old" + proj.work_git = mock.MagicMock() + proj.work_git.GetHead.return_value = "old" + + syncbuf = mock.MagicMock() + syncbuf.detach_head = False + proj.Sync_LocalHalf(syncbuf, verbose=True) + + proj._revlist.assert_called_once_with("--count", "^new", "HEAD") + syncbuf.info.assert_called_once_with( + proj, "discarding %d commits", 3 + ) + + def test_sync_local_half_uses_count_and_limit_for_published_branch( + self, + ) -> None: + """Published sync uses --count for gain and -1 for probe.""" + with utils_for_test.TempGitTree() as tempdir: + proj = self._get_project(tempdir) + proj._InitWorkTree = mock.MagicMock() + proj.CleanPublishedCache = mock.MagicMock() + proj.GetRevisionId = mock.MagicMock(return_value="new") + proj._CopyAndLinkFiles = mock.MagicMock() + proj.IsRebaseInProgress = mock.MagicMock(return_value=False) + proj.IsCherryPickInProgress = mock.MagicMock(return_value=False) + proj._revlist = mock.MagicMock(side_effect=[["2"], ["pub-sha"]]) + proj.bare_ref = mock.MagicMock() + proj.bare_ref.all = {"refs/heads/topic": "old"} + proj.bare_ref.head = "refs/heads/topic" + proj.work_git = mock.MagicMock() + proj.work_git.GetHead.return_value = "refs/heads/topic" + proj.work_git.merge_base.side_effect = project.GitError("diverged") + branch = mock.MagicMock() + branch.name = "topic" + branch.LocalMerge = "refs/remotes/origin/main" + proj.GetBranch = mock.MagicMock(return_value=branch) + proj.WasPublished = mock.MagicMock(return_value="pub-sha") + + syncbuf = mock.MagicMock() + syncbuf.detach_head = False + proj.Sync_LocalHalf(syncbuf, force_rebase=False) + + self.assertEqual( + proj._revlist.call_args_list, + [ + mock.call("--count", "^HEAD", "new"), + mock.call("-1", "^new", "pub-sha"), + ], + ) + syncbuf.fail.assert_called_once() + def test_sync_network_half_stateless_skips_if_stash(self): """Test stateless sync skips if stash exists.""" with utils_for_test.TempGitTree() as tempdir: @@ -3351,9 +3442,11 @@ class ReprojectCmdTests(unittest.TestCase): def _revlist(*args: Any, **kwargs: Any) -> List[str]: if kwargs.get("format"): return list(local_changes) - if args[0] == project.not_rev(project.HEAD): + if project.not_rev(project.HEAD) in args: + if "--count" in args: + return [str(len(upstream_gain))] return list(upstream_gain) - if args[1] == self.PUB_ID: + if self.PUB_ID in args: return [self.PUB_ID] return []