project: don't rerun git status after a failed snapshot

When a status snapshot fails, fall back directly to _IsDirtyLegacy()
instead of IsDirty() to avoid running git status twice.

Expose Project.GetDirtyAndHead() so sync bloat checks can inspect
worktree state without invoking private Project methods.

Bug: 565047698
Change-Id: I6432ed82da88778eec9d151fd5b70bedb1efc62b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/634424
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-09-23 13:26:55 -07:00
1 parent e7d4d97ffc
commit 310c99571f
4 files changed
+102 -26

No files matched your search

+58
View File
@@ -756,6 +756,64 @@ class ProjectTests(unittest.TestCase):
)
proj.HasStash.assert_called_once_with()
def test_dirty_and_head_agree_across_status_paths(self) -> None:
"""Porcelain v2 and legacy plumbing report the same state."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
def check(expected: Tuple[bool, Optional[str]]) -> None:
for use_status in (True, False):
with self.subTest(expected=expected, status=use_status):
with mock.patch.object(
project, "git_require", return_value=use_status
):
self.assertEqual(expected, proj.GetDirtyAndHead())
check((False, None))
Path(tempdir, "untracked").write_text("new")
check((True, None))
Path(tempdir, "tracked").write_text("initial")
proj.work_git.add("tracked")
proj.work_git.commit("-m", "initial")
head = proj.work_git.rev_parse("HEAD")
check((True, head))
os.remove(os.path.join(tempdir, "untracked"))
check((False, head))
def test_dirty_and_head_fallback_skips_second_snapshot(self) -> None:
"""A failed snapshot goes straight to the legacy plumbing."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
proj._GetStatusSnapshot = mock.MagicMock(return_value=None)
proj._IsDirtyLegacy = mock.MagicMock(return_value=False)
proj.work_git = mock.MagicMock()
proj.work_git.rev_parse.return_value = "head"
self.assertEqual((False, "head"), proj.GetDirtyAndHead())
proj._GetStatusSnapshot.assert_called_once_with(
untracked_files="normal", branch=True
)
proj._IsDirtyLegacy.assert_called_once_with(consider_untracked=True)
proj.work_git.rev_parse.assert_called_once_with("HEAD")
def test_dirty_or_stash_fallback_skips_second_snapshot(self) -> None:
"""A failed snapshot goes straight to the legacy dirty check."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
proj._GetStatusSnapshot = mock.MagicMock(return_value=None)
proj._IsDirtyLegacy = mock.MagicMock(return_value=False)
proj.HasStash = mock.MagicMock(return_value=False)
with mock.patch.object(project, "git_require", return_value=True):
self.assertFalse(proj._HasDirtyOrStash())
proj._GetStatusSnapshot.assert_called_once_with(
untracked_files="normal", show_stash=True
)
proj._IsDirtyLegacy.assert_called_once_with(consider_untracked=True)
def test_old_git_dirty_check_uses_legacy_plumbing(self) -> None:
"""Git clients before 2.11 retain the existing dirty-check path."""
with utils_for_test.TempGitTree() as tempdir:
+22 -12
View File
@@ -30,7 +30,6 @@ import pytest
import command
from error import GitError
from error import RepoExitError
import git_status
import manifest_xml
from project import SyncNetworkHalfResult
from subcmds import sync
@@ -990,11 +989,9 @@ class CheckForBloatedProjects(unittest.TestCase):
self.cmd.git_event_log = mock.MagicMock()
self.cmd._bloated_projects = []
def test_one_project_reuses_status_head_oid(self) -> None:
"""The bloat scan gets dirty state and HEAD from one snapshot."""
status = git_status.StatusSnapshot()
status.branch_oid = "local"
self.project._GetStatusSnapshot.return_value = status
def test_one_project_uses_dirty_and_head(self) -> None:
"""A project whose HEAD left the manifest revision is measured."""
self.project.GetDirtyAndHead.return_value = (False, "local")
self.project.GetRevisionId.return_value = "manifest"
self.project.bare_git.count_objects.return_value = (
"packs: 0\nsize-pack: 0\nsize-garbage: 0\n"
@@ -1006,15 +1003,28 @@ class CheckForBloatedProjects(unittest.TestCase):
):
self.assertIsNone(self.cmd._CheckOneBloatedProject(0))
self.project.IsDirty.assert_not_called()
self.project.work_git.rev_parse.assert_not_called()
self.project.GetDirtyAndHead.assert_called_once_with()
self.project.bare_git.count_objects.assert_called_once_with("-v")
def test_one_dirty_project_is_measured(self) -> None:
"""A dirty project is measured even if HEAD matches the manifest."""
self.project.GetDirtyAndHead.return_value = (True, "local")
self.project.GetRevisionId.return_value = "local"
self.project.bare_git.count_objects.return_value = (
"packs: 0\nsize-pack: 0\nsize-garbage: 0\n"
)
with mock.patch.object(
sync.Sync,
"get_parallel_context",
return_value={"projects": [self.project]},
):
self.assertIsNone(self.cmd._CheckOneBloatedProject(0))
self.project.bare_git.count_objects.assert_called_once_with("-v")
def test_one_unborn_project_skips_bloat_check(self) -> None:
"""A porcelain initial branch behaves like failed rev-parse HEAD."""
status = git_status.StatusSnapshot()
status.index_changes["staged"] = git_status.StatusEntry("staged", "M")
self.project._GetStatusSnapshot.return_value = status
"""A project without a resolvable HEAD is skipped."""
self.project.GetDirtyAndHead.return_value = (True, None)
with mock.patch.object(
sync.Sync,