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()