diff options
| author | Brian Gan <brgan@google.com> | 2026-09-14 14:14:24 +0000 |
|---|---|---|
| committer | gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2026-09-14 13:27:19 -0700 |
| commit | c2c330ba4a92c915f0180457f0d7ee29031bb253 (patch) | |
| tree | bb3ec8743022d0c220b0093d0f42c4fb6c384abf /tests | |
| parent | cc88be34d214418511c13f2284ecdb22245fb650 (diff) | |
| download | git-repo-c2c330ba4a92c915f0180457f0d7ee29031bb253.tar.gz | |
project: report why refreshing the index failed
"repo status" aborts the whole tree when one project fails, without
saying why. The three callers that refresh the index stat cache pass -q,
which suppresses git's diagnosis when it cannot create the index lock;
git then exits 128 with both streams empty. A stale lock, a permissions
problem on the gitdir and a full disk all reach that same silent exit,
yet each needs a different remedy.
Route the three identical call sites through one helper that keeps -q
for the refresh itself. When that fails, it repeats the command without
-q to obtain git's diagnosis. The command still fails, which is correct,
but the user is now told why.
Keeping -q on the common path matters: without it git exits 1 whenever
paths hold uncommitted changes, which would report a failure to
telemetry for every modified project, even if we treat it as benign in
the code. The retry accepts that 1, since --unmerged makes git skip
conflicted paths rather than count them, so nothing else yields it.
The similar call in subcmds/sync.py is unchanged: it passes different
flags, without --unmerged, so exit 1 cannot be assumed benign there.
Bug: 560289756
Change-Id: I2e1024c84454f86076081f2d7f155728b1aa35c6
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/630641
Tested-by: Brian Gan <brgan@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Brian Gan <brgan@google.com>
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_project.py | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/tests/test_project.py b/tests/test_project.py index af296c2fb..911bf7285 100644 --- a/tests/test_project.py +++ b/tests/test_project.py | |||
| @@ -28,6 +28,7 @@ import pytest | |||
| 28 | import utils_for_test | 28 | import utils_for_test |
| 29 | 29 | ||
| 30 | import error | 30 | import error |
| 31 | import git_command | ||
| 31 | import git_config | 32 | import git_config |
| 32 | import git_trace2_event_log | 33 | import git_trace2_event_log |
| 33 | import manifest_xml | 34 | import manifest_xml |
| @@ -1794,6 +1795,73 @@ def test_metaproject_has_changes_bounds_revision_walk() -> None: | |||
| 1794 | meta._revlist.assert_called_once_with("-1", "^HEAD", "remote") | 1795 | meta._revlist.assert_called_once_with("-1", "^HEAD", "remote") |
| 1795 | 1796 | ||
| 1796 | 1797 | ||
| 1798 | @pytest.mark.parametrize( | ||
| 1799 | "state,expect_failure", | ||
| 1800 | [ | ||
| 1801 | ("clean", False), | ||
| 1802 | # -q makes git exit 0 here rather than 1, so the repeat is never | ||
| 1803 | # reached. That is the point of it: an ordinary modified tree must | ||
| 1804 | # not be reported as a failure. | ||
| 1805 | ("modified", False), | ||
| 1806 | ("locked", True), | ||
| 1807 | ], | ||
| 1808 | ) | ||
| 1809 | def test_refresh_index_stat_cache(state: str, expect_failure: bool) -> None: | ||
| 1810 | """Refreshing tolerates local changes but reports a real failure.""" | ||
| 1811 | with utils_for_test.TempGitTree() as tempdir: | ||
| 1812 | proj = _create_mock_project(tempdir) | ||
| 1813 | readme = Path(tempdir) / "readme" | ||
| 1814 | readme.write_text("hello") | ||
| 1815 | proj.work_git.add("readme") | ||
| 1816 | proj.work_git.commit("-m", "initial commit") | ||
| 1817 | |||
| 1818 | if state == "modified": | ||
| 1819 | readme.write_text("different contents") | ||
| 1820 | elif state == "locked": | ||
| 1821 | # git only needs the lock when the index has to be rewritten, so | ||
| 1822 | # age the cached stat data while leaving the contents alone. | ||
| 1823 | os.utime(readme, (1, 1)) | ||
| 1824 | (Path(proj.gitdir) / "index.lock").write_text("") | ||
| 1825 | |||
| 1826 | if not expect_failure: | ||
| 1827 | proj._RefreshIndexStatCache() | ||
| 1828 | return | ||
| 1829 | |||
| 1830 | with pytest.raises(error.GitError) as excinfo: | ||
| 1831 | proj._RefreshIndexStatCache() | ||
| 1832 | # Omitting -q is what lets git name the file it could not create. | ||
| 1833 | assert "index.lock" in str(excinfo.value) | ||
| 1834 | |||
| 1835 | |||
| 1836 | @pytest.mark.parametrize( | ||
| 1837 | "repeat_rc,expect_raise", | ||
| 1838 | [ | ||
| 1839 | # Whatever blocked the quiet attempt cleared in between, leaving the | ||
| 1840 | # repeat to report nothing worse than uncommitted changes. | ||
| 1841 | (1, False), | ||
| 1842 | (128, True), | ||
| 1843 | ], | ||
| 1844 | ) | ||
| 1845 | def test_refresh_index_stat_cache_repeat( | ||
| 1846 | repeat_rc: int, expect_raise: bool | ||
| 1847 | ) -> None: | ||
| 1848 | """The repeat re-raises unless it failed with an ordinary exit 1.""" | ||
| 1849 | proj = mock.MagicMock() | ||
| 1850 | proj.work_git.update_index.side_effect = [ | ||
| 1851 | git_command.GitCommandError("quiet attempt failed", git_rc=128), | ||
| 1852 | git_command.GitCommandError("repeat failed", git_rc=repeat_rc), | ||
| 1853 | ] | ||
| 1854 | |||
| 1855 | if expect_raise: | ||
| 1856 | with pytest.raises(git_command.GitCommandError): | ||
| 1857 | project.Project._RefreshIndexStatCache(proj) | ||
| 1858 | else: | ||
| 1859 | project.Project._RefreshIndexStatCache(proj) | ||
| 1860 | |||
| 1861 | # The quiet attempt ran, then the repeat that explains it. | ||
| 1862 | assert proj.work_git.update_index.call_count == 2 | ||
| 1863 | |||
| 1864 | |||
| 1797 | def _create_mock_project( | 1865 | def _create_mock_project( |
| 1798 | tempdir, | 1866 | tempdir, |
| 1799 | use_local_gitdirs=False, | 1867 | use_local_gitdirs=False, |
