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>
diff --git a/project.py b/project.py index 5d7f32d..26e7845 100644 --- a/project.py +++ b/project.py
@@ -930,6 +930,21 @@ ) 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 @@ 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 7c57ff0..e7a3bec 100644 --- a/tests/test_project.py +++ b/tests/test_project.py
@@ -713,6 +713,49 @@ ): 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 @@ """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()