project: fold normal stash detection into status

When Git 2.35 or newer supplies a stash header, use the same status
snapshot for stateless dirty and stash checks. Retain rev-parse for
earlier clients or when the status snapshot is unavailable.

Bug: 543851900
Bug: 553599402
Change-Id: I6e310459d9c1a3ba7978f7907ae6b7a9bc611749
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/632146
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
This commit is contained in:
Gavin Mak
2026-09-21 13:39:51 -07:00
committed by gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent aaadd5da35
commit 7bffc72b5b
2 changed files with 60 additions and 2 deletions
+16 -1
View File
@@ -930,6 +930,21 @@ class Project:
)
return p.Wait() == 0
def _HasDirtyOrStash(self) -> bool:
"""Check dirty and normal stash state with one status when possible."""
has_status_stash = git_require((2, 35, 0))
status = self._GetStatusSnapshot(
untracked_files="normal",
show_stash=has_status_stash,
)
if status is not None:
if status.is_dirty(consider_untracked=True):
return True
if has_status_stash:
return bool(status.stash_count)
return self.HasStash()
return self.IsDirty(consider_untracked=True) or self.HasStash()
_userident_name = None
_userident_email = None
@@ -1670,7 +1685,7 @@ class Project:
except (GitError, IndexError, ValueError):
return False
if self.IsDirty(consider_untracked=True) or self.HasStash():
if self._HasDirtyOrStash():
return False
return True
+44 -1
View File
@@ -713,6 +713,49 @@ class ProjectTests(unittest.TestCase):
):
self.assertIsNone(proj._GetStatusSnapshot())
def test_dirty_or_stash_uses_status_stash_header(self) -> None:
"""A normal stash is detected without a second Git process."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
status = git_status.StatusSnapshot()
status.stash_count = 1
proj._GetStatusSnapshot = mock.MagicMock(return_value=status)
proj.HasStash = mock.MagicMock()
with mock.patch.object(project, "git_require", return_value=True):
self.assertTrue(proj._HasDirtyOrStash())
proj.HasStash.assert_not_called()
def test_dirty_or_stash_clean_no_stash_on_git_2_35(self) -> None:
"""A clean tree requires no second Git process on Git 2.35+."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
status = git_status.StatusSnapshot()
proj._GetStatusSnapshot = mock.MagicMock(return_value=status)
proj.HasStash = mock.MagicMock()
with mock.patch.object(project, "git_require", return_value=True):
self.assertFalse(proj._HasDirtyOrStash())
proj.HasStash.assert_not_called()
def test_dirty_or_stash_before_2_35_checks_stash_ref(self) -> None:
"""Older porcelain v2 output is not assumed to contain stash data."""
with utils_for_test.TempGitTree() as tempdir:
proj = _create_mock_project(tempdir)
status = git_status.StatusSnapshot()
proj._GetStatusSnapshot = mock.MagicMock(return_value=status)
proj.HasStash = mock.MagicMock(return_value=True)
with mock.patch.object(project, "git_require", return_value=False):
self.assertTrue(proj._HasDirtyOrStash())
proj._GetStatusSnapshot.assert_called_once_with(
untracked_files="normal", show_stash=False
)
proj.HasStash.assert_called_once_with()
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:
@@ -2186,7 +2229,7 @@ class StatelessSyncTests(unittest.TestCase):
"""Test stateless sync skips if stash exists."""
with utils_for_test.TempGitTree() as tempdir:
proj = self._get_project(tempdir)
proj.HasStash = mock.MagicMock(return_value=True)
proj._HasDirtyOrStash = mock.MagicMock(return_value=True)
res = proj.Sync_NetworkHalf()