mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-09-24 15:50:35 +00:00
project: use one status snapshot for dirty checks
On Git 2.11 and newer, replace update-index, diff-index, diff-files, and ls-files fan-out with one porcelain-v2 snapshot for IsDirty, UncommittedFiles, and HasChanges. Preserve category ordering and duplicate staged-plus-unstaged paths, and keep the exact legacy path for older Git or a failed snapshot. Bug: 543851900 Bug: 553599402 Change-Id: I309e39977e1f1d4ee6115f488d6aabb83ec3d83c Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/632143 Tested-by: Gavin Mak <gavinmak@google.com> Reviewed-by: Brian Gan <brgan@google.com> Commit-Queue: Gavin Mak <gavinmak@google.com>
This commit is contained in:
committed by
gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
c1566487a8
commit
530258d08e
+80
-1
@@ -57,6 +57,7 @@ from git_refs import R_M
|
||||
from git_refs import R_PUB
|
||||
from git_refs import R_TAGS
|
||||
from git_refs import R_WORKTREE_M
|
||||
import git_status
|
||||
import git_superproject
|
||||
from git_trace2_event_log import EventLog
|
||||
import platform_utils
|
||||
@@ -868,8 +869,46 @@ class Project:
|
||||
if e.git_rc != 1:
|
||||
raise
|
||||
|
||||
def IsDirty(self, consider_untracked=True):
|
||||
def IsDirty(self, consider_untracked: bool = True) -> bool:
|
||||
"""Is the working directory modified in some way?"""
|
||||
status = self._GetStatusSnapshot(
|
||||
untracked_files="normal" if consider_untracked else "no"
|
||||
)
|
||||
if status is not None:
|
||||
return status.is_dirty(consider_untracked=consider_untracked)
|
||||
|
||||
return self._IsDirtyLegacy(consider_untracked=consider_untracked)
|
||||
|
||||
def _GetStatusSnapshot(
|
||||
self,
|
||||
untracked_files: str = "all",
|
||||
branch: bool = False,
|
||||
ahead_behind: bool = False,
|
||||
show_stash: bool = False,
|
||||
) -> Optional[git_status.StatusSnapshot]:
|
||||
"""Read one porcelain-v2 snapshot, or select the legacy path."""
|
||||
if not git_require((2, 11, 0)):
|
||||
return None
|
||||
try:
|
||||
return git_status.GetStatus(
|
||||
self,
|
||||
self.gitdir,
|
||||
untracked_files=untracked_files,
|
||||
branch=branch,
|
||||
ahead_behind=ahead_behind,
|
||||
show_stash=show_stash,
|
||||
)
|
||||
except (GitError, ValueError, git_status.UnsupportedStatusError) as e:
|
||||
logger.warning(
|
||||
"project %s: porcelain v2 status failed; using legacy "
|
||||
"status: %s",
|
||||
self.RelPath(local=False),
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
def _IsDirtyLegacy(self, consider_untracked: bool = True) -> bool:
|
||||
"""Check dirty state with plumbing supported by older Git."""
|
||||
self._RefreshIndexStatCache()
|
||||
if self.work_git.DiffZ("diff-index", "-M", "--cached", HEAD):
|
||||
return True
|
||||
@@ -994,6 +1033,46 @@ class Project:
|
||||
uncommitted files. If False - return as soon as any kind of
|
||||
uncommitted files is detected.
|
||||
"""
|
||||
status = self._GetStatusSnapshot(untracked_files="all")
|
||||
if status is not None:
|
||||
return self._UncommittedFilesFromStatus(status, get_all=get_all)
|
||||
|
||||
return self._UncommittedFilesLegacy(get_all=get_all)
|
||||
|
||||
def _UncommittedFilesFromStatus(
|
||||
self, status: git_status.StatusSnapshot, get_all: bool = True
|
||||
) -> List[str]:
|
||||
"""Format uncommitted paths from a porcelain-v2 snapshot."""
|
||||
details = []
|
||||
if self.IsRebaseInProgress():
|
||||
details.append("rebase in progress")
|
||||
if not get_all:
|
||||
return details
|
||||
|
||||
changes = []
|
||||
for path, entry in status.index_changes.items():
|
||||
changes.append(path)
|
||||
# The legacy diff-index call did not enable rename detection, so
|
||||
# it reported a staged rename as delete(source) plus add(target).
|
||||
if entry.status == "R" and entry.src_path:
|
||||
changes.append(entry.src_path)
|
||||
changes.sort()
|
||||
if changes:
|
||||
details.extend(changes)
|
||||
if not get_all:
|
||||
return details
|
||||
|
||||
changes = list(status.worktree_changes)
|
||||
if changes:
|
||||
details.extend(changes)
|
||||
if not get_all:
|
||||
return details
|
||||
|
||||
details.extend(status.untracked)
|
||||
return details
|
||||
|
||||
def _UncommittedFilesLegacy(self, get_all: bool = True) -> List[str]:
|
||||
"""List uncommitted paths with plumbing supported by older Git."""
|
||||
details = []
|
||||
self._RefreshIndexStatCache()
|
||||
if self.IsRebaseInProgress():
|
||||
|
||||
@@ -30,6 +30,7 @@ import utils_for_test
|
||||
import error
|
||||
import git_command
|
||||
import git_config
|
||||
import git_status
|
||||
import git_trace2_event_log
|
||||
import manifest_xml
|
||||
import platform_utils
|
||||
@@ -602,6 +603,84 @@ class ProjectTests(unittest.TestCase):
|
||||
proj.bare_git.SetHead.assert_called_once_with("refs/heads/manifest")
|
||||
proj.bare_git.DetachHead.assert_called_once_with(revision)
|
||||
|
||||
def test_dirty_status_snapshot(self) -> None:
|
||||
"""Dirty checks cover staged, unstaged, and optional untracked files."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = _create_mock_project(tempdir)
|
||||
Path(tempdir, "tracked").write_text("initial")
|
||||
proj.work_git.add("tracked")
|
||||
proj.work_git.commit("-m", "initial")
|
||||
|
||||
self.assertFalse(proj.IsDirty())
|
||||
|
||||
Path(tempdir, "tracked").write_text("staged")
|
||||
proj.work_git.add("tracked")
|
||||
self.assertTrue(proj.IsDirty(consider_untracked=False))
|
||||
|
||||
Path(tempdir, "tracked").write_text("unstaged")
|
||||
self.assertEqual(
|
||||
["tracked", "tracked"], proj.UncommittedFiles(get_all=True)
|
||||
)
|
||||
|
||||
proj.work_git.reset("--hard", "HEAD")
|
||||
Path(tempdir, "untracked").write_text("new")
|
||||
self.assertTrue(proj.IsDirty())
|
||||
self.assertFalse(proj.IsDirty(consider_untracked=False))
|
||||
|
||||
def test_uncommitted_files_preserve_staged_rename_paths(self) -> None:
|
||||
"""The snapshot returns both paths reported by legacy diff-index."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = _create_mock_project(tempdir)
|
||||
Path(tempdir, "old").write_text("tracked")
|
||||
proj.work_git.add("old")
|
||||
proj.work_git.commit("-m", "initial")
|
||||
proj.work_git.mv("old", "new")
|
||||
|
||||
self.assertEqual(["new", "old"], proj.UncommittedFiles())
|
||||
|
||||
def test_has_changes_includes_rebase_from_status_snapshot(self) -> None:
|
||||
"""HasChanges keeps treating an in-progress rebase as a change."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = _create_mock_project(tempdir)
|
||||
status = git_status.StatusSnapshot()
|
||||
proj._GetStatusSnapshot = mock.MagicMock(return_value=status)
|
||||
proj.IsRebaseInProgress = mock.MagicMock(return_value=True)
|
||||
|
||||
self.assertTrue(proj.HasChanges())
|
||||
self.assertEqual(
|
||||
["rebase in progress"], proj.UncommittedFiles(get_all=False)
|
||||
)
|
||||
|
||||
def test_get_status_snapshot_handles_unsupported_status_error(self) -> None:
|
||||
"""UnsupportedStatusError triggers the legacy status fallback."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = _create_mock_project(tempdir)
|
||||
with mock.patch.object(
|
||||
git_status,
|
||||
"GetStatus",
|
||||
side_effect=git_status.UnsupportedStatusError,
|
||||
):
|
||||
self.assertIsNone(proj._GetStatusSnapshot())
|
||||
|
||||
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:
|
||||
proj = _create_mock_project(tempdir)
|
||||
proj.work_git = mock.MagicMock()
|
||||
proj.work_git.DiffZ.side_effect = [{}, {"tracked": mock.sentinel}]
|
||||
|
||||
with mock.patch.object(project, "git_require", return_value=False):
|
||||
self.assertTrue(proj.IsDirty())
|
||||
|
||||
proj.work_git.update_index.assert_called_once_with(
|
||||
"-q",
|
||||
"--unmerged",
|
||||
"--ignore-missing",
|
||||
"--refresh",
|
||||
log_as_error=False,
|
||||
)
|
||||
self.assertEqual(2, proj.work_git.DiffZ.call_count)
|
||||
|
||||
@unittest.skipUnless(
|
||||
utils_for_test.supports_reftable(),
|
||||
"git reftable support is required for this test",
|
||||
|
||||
Reference in New Issue
Block a user