summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGavin Mak <gavinmak@google.com>2023-08-08 04:43:36 +0000
committerLUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2023-08-10 18:13:14 +0000
commitf0aeb220def22edfac9838288ad251f86da782c1 (patch)
tree7ef6ac75fc6bf83abf5a060a69bd6bed9b716685
parentf1ddaaa553521c5c659271dd52c8d33866a51936 (diff)
downloadgit-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.py41
-rw-r--r--tests/test_subcmds_sync.py61
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
185class GetPreciousObjectsState(unittest.TestCase): 246class GetPreciousObjectsState(unittest.TestCase):
186 """Tests for _GetPreciousObjectsState.""" 247 """Tests for _GetPreciousObjectsState."""