diff options
| author | Gavin Mak <gavinmak@google.com> | 2023-08-08 04:43:36 +0000 |
|---|---|---|
| committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2023-08-10 18:13:14 +0000 |
| commit | f0aeb220def22edfac9838288ad251f86da782c1 (patch) | |
| tree | 7ef6ac75fc6bf83abf5a060a69bd6bed9b716685 | |
| parent | f1ddaaa553521c5c659271dd52c8d33866a51936 (diff) | |
| download | git-repo-f0aeb220def22edfac9838288ad251f86da782c1.tar.gz | |
sync: Warn if partial sync state is detected
Partial syncs are not supported and can lead to strange behavior like
deleting files. Explicitly warn users on partial sync.
Bug: b/286126621, b/271507654
Change-Id: I471f78ac5942eb855bc34c80af47aa561dfa61e8
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/382154
Reviewed-by: Jason Chang <jasonnc@google.com>
Reviewed-by: Aravind Vasudevan <aravindvasudev@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@google.com>
| -rw-r--r-- | subcmds/sync.py | 41 | ||||
| -rw-r--r-- | tests/test_subcmds_sync.py | 61 |
2 files changed, 101 insertions, 1 deletions
diff --git a/subcmds/sync.py b/subcmds/sync.py index eaca50c9..3fa6efa5 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py | |||
| @@ -1866,6 +1866,14 @@ later is required to fix a server side protocol bug. | |||
| 1866 | mp.config.GetSyncAnalysisStateData(), "current_sync_state" | 1866 | mp.config.GetSyncAnalysisStateData(), "current_sync_state" |
| 1867 | ) | 1867 | ) |
| 1868 | 1868 | ||
| 1869 | self._local_sync_state.PruneRemovedProjects() | ||
| 1870 | if self._local_sync_state.IsPartiallySynced(): | ||
| 1871 | print( | ||
| 1872 | "warning: Partial syncs are not supported. For the best " | ||
| 1873 | "experience, sync the entire tree.", | ||
| 1874 | file=sys.stderr, | ||
| 1875 | ) | ||
| 1876 | |||
| 1869 | if not opt.quiet: | 1877 | if not opt.quiet: |
| 1870 | print("repo sync has finished successfully.") | 1878 | print("repo sync has finished successfully.") |
| 1871 | 1879 | ||
| @@ -1975,7 +1983,10 @@ class _LocalSyncState(object): | |||
| 1975 | _LAST_CHECKOUT = "last_checkout" | 1983 | _LAST_CHECKOUT = "last_checkout" |
| 1976 | 1984 | ||
| 1977 | def __init__(self, manifest): | 1985 | def __init__(self, manifest): |
| 1978 | self._path = os.path.join(manifest.repodir, ".repo_localsyncstate.json") | 1986 | self._manifest = manifest |
| 1987 | self._path = os.path.join( | ||
| 1988 | self._manifest.repodir, ".repo_localsyncstate.json" | ||
| 1989 | ) | ||
| 1979 | self._time = time.time() | 1990 | self._time = time.time() |
| 1980 | self._state = None | 1991 | self._state = None |
| 1981 | self._Load() | 1992 | self._Load() |
| @@ -2023,6 +2034,34 @@ class _LocalSyncState(object): | |||
| 2023 | except (IOError, TypeError): | 2034 | except (IOError, TypeError): |
| 2024 | platform_utils.remove(self._path, missing_ok=True) | 2035 | platform_utils.remove(self._path, missing_ok=True) |
| 2025 | 2036 | ||
| 2037 | def PruneRemovedProjects(self): | ||
| 2038 | """Remove entries don't exist on disk and save.""" | ||
| 2039 | if not self._state: | ||
| 2040 | return | ||
| 2041 | delete = set() | ||
| 2042 | for path in self._state: | ||
| 2043 | gitdir = os.path.join(self._manifest.topdir, path, ".git") | ||
| 2044 | if not os.path.exists(gitdir): | ||
| 2045 | delete.add(path) | ||
| 2046 | if not delete: | ||
| 2047 | return | ||
| 2048 | for path in delete: | ||
| 2049 | del self._state[path] | ||
| 2050 | self.Save() | ||
| 2051 | |||
| 2052 | def IsPartiallySynced(self): | ||
| 2053 | """Return whether a partial sync state is detected.""" | ||
| 2054 | self._Load() | ||
| 2055 | prev_checkout_t = None | ||
| 2056 | for data in self._state.values(): | ||
| 2057 | checkout_t = data.get(self._LAST_CHECKOUT) | ||
| 2058 | if not checkout_t: | ||
| 2059 | return True | ||
| 2060 | prev_checkout_t = prev_checkout_t or checkout_t | ||
| 2061 | if prev_checkout_t != checkout_t: | ||
| 2062 | return True | ||
| 2063 | return False | ||
| 2064 | |||
| 2026 | 2065 | ||
| 2027 | # This is a replacement for xmlrpc.client.Transport using urllib2 | 2066 | # This is a replacement for xmlrpc.client.Transport using urllib2 |
| 2028 | # and supporting persistent-http[s]. It cannot change hosts from | 2067 | # and supporting persistent-http[s]. It cannot change hosts from |
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index 00c34852..7cc93e39 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py | |||
| @@ -175,12 +175,73 @@ class LocalSyncState(unittest.TestCase): | |||
| 175 | os.listdir(self.repodir), [".repo_localsyncstate.json"] | 175 | os.listdir(self.repodir), [".repo_localsyncstate.json"] |
| 176 | ) | 176 | ) |
| 177 | 177 | ||
| 178 | def test_partial_sync(self): | ||
| 179 | """Partial sync state is detected.""" | ||
| 180 | with open(self.state._path, "w") as f: | ||
| 181 | f.write( | ||
| 182 | """ | ||
| 183 | { | ||
| 184 | "projA": { | ||
| 185 | "last_fetch": 5, | ||
| 186 | "last_checkout": 5 | ||
| 187 | }, | ||
| 188 | "projB": { | ||
| 189 | "last_fetch": 5, | ||
| 190 | "last_checkout": 5 | ||
| 191 | } | ||
| 192 | } | ||
| 193 | """ | ||
| 194 | ) | ||
| 195 | |||
| 196 | # Initialize state to read from the new file. | ||
| 197 | self.state = self._new_state() | ||
| 198 | projB = mock.MagicMock(relpath="projB") | ||
| 199 | self.assertEqual(self.state.IsPartiallySynced(), False) | ||
| 200 | |||
| 201 | self.state.SetFetchTime(projB) | ||
| 202 | self.state.SetCheckoutTime(projB) | ||
| 203 | self.assertEqual(self.state.IsPartiallySynced(), True) | ||
| 204 | |||
| 178 | def test_nonexistent_project(self): | 205 | def test_nonexistent_project(self): |
| 179 | """Unsaved projects don't have data.""" | 206 | """Unsaved projects don't have data.""" |
| 180 | p = mock.MagicMock(relpath="projC") | 207 | p = mock.MagicMock(relpath="projC") |
| 181 | self.assertEqual(self.state.GetFetchTime(p), None) | 208 | self.assertEqual(self.state.GetFetchTime(p), None) |
| 182 | self.assertEqual(self.state.GetCheckoutTime(p), None) | 209 | self.assertEqual(self.state.GetCheckoutTime(p), None) |
| 183 | 210 | ||
| 211 | def test_prune_removed_projects(self): | ||
| 212 | """Removed projects are pruned.""" | ||
| 213 | with open(self.state._path, "w") as f: | ||
| 214 | f.write( | ||
| 215 | """ | ||
| 216 | { | ||
| 217 | "projA": { | ||
| 218 | "last_fetch": 5 | ||
| 219 | }, | ||
| 220 | "projB": { | ||
| 221 | "last_fetch": 7 | ||
| 222 | } | ||
| 223 | } | ||
| 224 | """ | ||
| 225 | ) | ||
| 226 | |||
| 227 | def mock_exists(path): | ||
| 228 | if "projA" in path: | ||
| 229 | return False | ||
| 230 | return True | ||
| 231 | |||
| 232 | projA = mock.MagicMock(relpath="projA") | ||
| 233 | projB = mock.MagicMock(relpath="projB") | ||
| 234 | self.state = self._new_state() | ||
| 235 | self.assertEqual(self.state.GetFetchTime(projA), 5) | ||
| 236 | self.assertEqual(self.state.GetFetchTime(projB), 7) | ||
| 237 | with mock.patch("os.path.exists", side_effect=mock_exists): | ||
| 238 | self.state.PruneRemovedProjects() | ||
| 239 | self.assertIsNone(self.state.GetFetchTime(projA)) | ||
| 240 | |||
| 241 | self.state = self._new_state() | ||
| 242 | self.assertIsNone(self.state.GetFetchTime(projA)) | ||
| 243 | self.assertEqual(self.state.GetFetchTime(projB), 7) | ||
| 244 | |||
| 184 | 245 | ||
| 185 | class GetPreciousObjectsState(unittest.TestCase): | 246 | class GetPreciousObjectsState(unittest.TestCase): |
| 186 | """Tests for _GetPreciousObjectsState.""" | 247 | """Tests for _GetPreciousObjectsState.""" |
