From 7bffc72b5bf6bc8b6065ef6712e589457a3f7f20 Mon Sep 17 00:00:00 2001 From: Gavin Mak Date: Tue, 25 Aug 2026 09:01:50 -0700 Subject: [PATCH] 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 Commit-Queue: Gavin Mak Reviewed-by: Brian Gan --- project.py | 17 +++++++++++++++- tests/test_project.py | 45 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/project.py b/project.py index 5d7f32d61..26e7845ca 100644 --- a/project.py +++ b/project.py @@ -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 diff --git a/tests/test_project.py b/tests/test_project.py index 7c57ff086..e7a3bec60 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -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()