Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/upgrade_utils/engine.py @ 46050:f105c49e89cd
upgrade: split actual upgrade code away from the main module
The main module is getting big and hard to follow. So we are splitting all the
logic to actually run an upgrade in a sub module. It nicely highlight that there
are very few actual call point to the code we just moved.
Differential Revision: https://phab.mercurial-scm.org/D9476
author | Pierre-Yves David <pierre-yves.david@octobus.net> |
---|---|
date | Tue, 01 Dec 2020 09:13:08 +0100 |
parents | mercurial/upgrade.py@6c960b708ac4 |
children | c407513a44a3 |
comparison
equal
deleted
inserted
replaced
46049:7905899c4f8f | 46050:f105c49e89cd |
---|---|
1 # upgrade.py - functions for in place upgrade of Mercurial repository | |
2 # | |
3 # Copyright (c) 2016-present, Gregory Szorc | |
4 # | |
5 # This software may be used and distributed according to the terms of the | |
6 # GNU General Public License version 2 or any later version. | |
7 | |
8 from __future__ import absolute_import | |
9 | |
10 import stat | |
11 | |
12 from ..i18n import _ | |
13 from ..pycompat import getattr | |
14 from .. import ( | |
15 changelog, | |
16 error, | |
17 filelog, | |
18 manifest, | |
19 metadata, | |
20 pycompat, | |
21 requirements, | |
22 revlog, | |
23 scmutil, | |
24 util, | |
25 vfs as vfsmod, | |
26 ) | |
27 | |
28 | |
29 def _revlogfrompath(repo, path): | |
30 """Obtain a revlog from a repo path. | |
31 | |
32 An instance of the appropriate class is returned. | |
33 """ | |
34 if path == b'00changelog.i': | |
35 return changelog.changelog(repo.svfs) | |
36 elif path.endswith(b'00manifest.i'): | |
37 mandir = path[: -len(b'00manifest.i')] | |
38 return manifest.manifestrevlog(repo.svfs, tree=mandir) | |
39 else: | |
40 # reverse of "/".join(("data", path + ".i")) | |
41 return filelog.filelog(repo.svfs, path[5:-2]) | |
42 | |
43 | |
44 def _copyrevlog(tr, destrepo, oldrl, unencodedname): | |
45 """copy all relevant files for `oldrl` into `destrepo` store | |
46 | |
47 Files are copied "as is" without any transformation. The copy is performed | |
48 without extra checks. Callers are responsible for making sure the copied | |
49 content is compatible with format of the destination repository. | |
50 """ | |
51 oldrl = getattr(oldrl, '_revlog', oldrl) | |
52 newrl = _revlogfrompath(destrepo, unencodedname) | |
53 newrl = getattr(newrl, '_revlog', newrl) | |
54 | |
55 oldvfs = oldrl.opener | |
56 newvfs = newrl.opener | |
57 oldindex = oldvfs.join(oldrl.indexfile) | |
58 newindex = newvfs.join(newrl.indexfile) | |
59 olddata = oldvfs.join(oldrl.datafile) | |
60 newdata = newvfs.join(newrl.datafile) | |
61 | |
62 with newvfs(newrl.indexfile, b'w'): | |
63 pass # create all the directories | |
64 | |
65 util.copyfile(oldindex, newindex) | |
66 copydata = oldrl.opener.exists(oldrl.datafile) | |
67 if copydata: | |
68 util.copyfile(olddata, newdata) | |
69 | |
70 if not ( | |
71 unencodedname.endswith(b'00changelog.i') | |
72 or unencodedname.endswith(b'00manifest.i') | |
73 ): | |
74 destrepo.svfs.fncache.add(unencodedname) | |
75 if copydata: | |
76 destrepo.svfs.fncache.add(unencodedname[:-2] + b'.d') | |
77 | |
78 | |
79 UPGRADE_CHANGELOG = b"changelog" | |
80 UPGRADE_MANIFEST = b"manifest" | |
81 UPGRADE_FILELOGS = b"all-filelogs" | |
82 | |
83 UPGRADE_ALL_REVLOGS = frozenset( | |
84 [UPGRADE_CHANGELOG, UPGRADE_MANIFEST, UPGRADE_FILELOGS] | |
85 ) | |
86 | |
87 | |
88 def getsidedatacompanion(srcrepo, dstrepo): | |
89 sidedatacompanion = None | |
90 removedreqs = srcrepo.requirements - dstrepo.requirements | |
91 addedreqs = dstrepo.requirements - srcrepo.requirements | |
92 if requirements.SIDEDATA_REQUIREMENT in removedreqs: | |
93 | |
94 def sidedatacompanion(rl, rev): | |
95 rl = getattr(rl, '_revlog', rl) | |
96 if rl.flags(rev) & revlog.REVIDX_SIDEDATA: | |
97 return True, (), {}, 0, 0 | |
98 return False, (), {}, 0, 0 | |
99 | |
100 elif requirements.COPIESSDC_REQUIREMENT in addedreqs: | |
101 sidedatacompanion = metadata.getsidedataadder(srcrepo, dstrepo) | |
102 elif requirements.COPIESSDC_REQUIREMENT in removedreqs: | |
103 sidedatacompanion = metadata.getsidedataremover(srcrepo, dstrepo) | |
104 return sidedatacompanion | |
105 | |
106 | |
107 def matchrevlog(revlogfilter, entry): | |
108 """check if a revlog is selected for cloning. | |
109 | |
110 In other words, are there any updates which need to be done on revlog | |
111 or it can be blindly copied. | |
112 | |
113 The store entry is checked against the passed filter""" | |
114 if entry.endswith(b'00changelog.i'): | |
115 return UPGRADE_CHANGELOG in revlogfilter | |
116 elif entry.endswith(b'00manifest.i'): | |
117 return UPGRADE_MANIFEST in revlogfilter | |
118 return UPGRADE_FILELOGS in revlogfilter | |
119 | |
120 | |
121 def _clonerevlogs( | |
122 ui, | |
123 srcrepo, | |
124 dstrepo, | |
125 tr, | |
126 deltareuse, | |
127 forcedeltabothparents, | |
128 revlogs=UPGRADE_ALL_REVLOGS, | |
129 ): | |
130 """Copy revlogs between 2 repos.""" | |
131 revcount = 0 | |
132 srcsize = 0 | |
133 srcrawsize = 0 | |
134 dstsize = 0 | |
135 fcount = 0 | |
136 frevcount = 0 | |
137 fsrcsize = 0 | |
138 frawsize = 0 | |
139 fdstsize = 0 | |
140 mcount = 0 | |
141 mrevcount = 0 | |
142 msrcsize = 0 | |
143 mrawsize = 0 | |
144 mdstsize = 0 | |
145 crevcount = 0 | |
146 csrcsize = 0 | |
147 crawsize = 0 | |
148 cdstsize = 0 | |
149 | |
150 alldatafiles = list(srcrepo.store.walk()) | |
151 | |
152 # Perform a pass to collect metadata. This validates we can open all | |
153 # source files and allows a unified progress bar to be displayed. | |
154 for unencoded, encoded, size in alldatafiles: | |
155 if unencoded.endswith(b'.d'): | |
156 continue | |
157 | |
158 rl = _revlogfrompath(srcrepo, unencoded) | |
159 | |
160 info = rl.storageinfo( | |
161 exclusivefiles=True, | |
162 revisionscount=True, | |
163 trackedsize=True, | |
164 storedsize=True, | |
165 ) | |
166 | |
167 revcount += info[b'revisionscount'] or 0 | |
168 datasize = info[b'storedsize'] or 0 | |
169 rawsize = info[b'trackedsize'] or 0 | |
170 | |
171 srcsize += datasize | |
172 srcrawsize += rawsize | |
173 | |
174 # This is for the separate progress bars. | |
175 if isinstance(rl, changelog.changelog): | |
176 crevcount += len(rl) | |
177 csrcsize += datasize | |
178 crawsize += rawsize | |
179 elif isinstance(rl, manifest.manifestrevlog): | |
180 mcount += 1 | |
181 mrevcount += len(rl) | |
182 msrcsize += datasize | |
183 mrawsize += rawsize | |
184 elif isinstance(rl, filelog.filelog): | |
185 fcount += 1 | |
186 frevcount += len(rl) | |
187 fsrcsize += datasize | |
188 frawsize += rawsize | |
189 else: | |
190 error.ProgrammingError(b'unknown revlog type') | |
191 | |
192 if not revcount: | |
193 return | |
194 | |
195 ui.status( | |
196 _( | |
197 b'migrating %d total revisions (%d in filelogs, %d in manifests, ' | |
198 b'%d in changelog)\n' | |
199 ) | |
200 % (revcount, frevcount, mrevcount, crevcount) | |
201 ) | |
202 ui.status( | |
203 _(b'migrating %s in store; %s tracked data\n') | |
204 % ((util.bytecount(srcsize), util.bytecount(srcrawsize))) | |
205 ) | |
206 | |
207 # Used to keep track of progress. | |
208 progress = None | |
209 | |
210 def oncopiedrevision(rl, rev, node): | |
211 progress.increment() | |
212 | |
213 sidedatacompanion = getsidedatacompanion(srcrepo, dstrepo) | |
214 | |
215 # Do the actual copying. | |
216 # FUTURE this operation can be farmed off to worker processes. | |
217 seen = set() | |
218 for unencoded, encoded, size in alldatafiles: | |
219 if unencoded.endswith(b'.d'): | |
220 continue | |
221 | |
222 oldrl = _revlogfrompath(srcrepo, unencoded) | |
223 | |
224 if isinstance(oldrl, changelog.changelog) and b'c' not in seen: | |
225 ui.status( | |
226 _( | |
227 b'finished migrating %d manifest revisions across %d ' | |
228 b'manifests; change in size: %s\n' | |
229 ) | |
230 % (mrevcount, mcount, util.bytecount(mdstsize - msrcsize)) | |
231 ) | |
232 | |
233 ui.status( | |
234 _( | |
235 b'migrating changelog containing %d revisions ' | |
236 b'(%s in store; %s tracked data)\n' | |
237 ) | |
238 % ( | |
239 crevcount, | |
240 util.bytecount(csrcsize), | |
241 util.bytecount(crawsize), | |
242 ) | |
243 ) | |
244 seen.add(b'c') | |
245 progress = srcrepo.ui.makeprogress( | |
246 _(b'changelog revisions'), total=crevcount | |
247 ) | |
248 elif isinstance(oldrl, manifest.manifestrevlog) and b'm' not in seen: | |
249 ui.status( | |
250 _( | |
251 b'finished migrating %d filelog revisions across %d ' | |
252 b'filelogs; change in size: %s\n' | |
253 ) | |
254 % (frevcount, fcount, util.bytecount(fdstsize - fsrcsize)) | |
255 ) | |
256 | |
257 ui.status( | |
258 _( | |
259 b'migrating %d manifests containing %d revisions ' | |
260 b'(%s in store; %s tracked data)\n' | |
261 ) | |
262 % ( | |
263 mcount, | |
264 mrevcount, | |
265 util.bytecount(msrcsize), | |
266 util.bytecount(mrawsize), | |
267 ) | |
268 ) | |
269 seen.add(b'm') | |
270 if progress: | |
271 progress.complete() | |
272 progress = srcrepo.ui.makeprogress( | |
273 _(b'manifest revisions'), total=mrevcount | |
274 ) | |
275 elif b'f' not in seen: | |
276 ui.status( | |
277 _( | |
278 b'migrating %d filelogs containing %d revisions ' | |
279 b'(%s in store; %s tracked data)\n' | |
280 ) | |
281 % ( | |
282 fcount, | |
283 frevcount, | |
284 util.bytecount(fsrcsize), | |
285 util.bytecount(frawsize), | |
286 ) | |
287 ) | |
288 seen.add(b'f') | |
289 if progress: | |
290 progress.complete() | |
291 progress = srcrepo.ui.makeprogress( | |
292 _(b'file revisions'), total=frevcount | |
293 ) | |
294 | |
295 if matchrevlog(revlogs, unencoded): | |
296 ui.note( | |
297 _(b'cloning %d revisions from %s\n') % (len(oldrl), unencoded) | |
298 ) | |
299 newrl = _revlogfrompath(dstrepo, unencoded) | |
300 oldrl.clone( | |
301 tr, | |
302 newrl, | |
303 addrevisioncb=oncopiedrevision, | |
304 deltareuse=deltareuse, | |
305 forcedeltabothparents=forcedeltabothparents, | |
306 sidedatacompanion=sidedatacompanion, | |
307 ) | |
308 else: | |
309 msg = _(b'blindly copying %s containing %i revisions\n') | |
310 ui.note(msg % (unencoded, len(oldrl))) | |
311 _copyrevlog(tr, dstrepo, oldrl, unencoded) | |
312 | |
313 newrl = _revlogfrompath(dstrepo, unencoded) | |
314 | |
315 info = newrl.storageinfo(storedsize=True) | |
316 datasize = info[b'storedsize'] or 0 | |
317 | |
318 dstsize += datasize | |
319 | |
320 if isinstance(newrl, changelog.changelog): | |
321 cdstsize += datasize | |
322 elif isinstance(newrl, manifest.manifestrevlog): | |
323 mdstsize += datasize | |
324 else: | |
325 fdstsize += datasize | |
326 | |
327 progress.complete() | |
328 | |
329 ui.status( | |
330 _( | |
331 b'finished migrating %d changelog revisions; change in size: ' | |
332 b'%s\n' | |
333 ) | |
334 % (crevcount, util.bytecount(cdstsize - csrcsize)) | |
335 ) | |
336 | |
337 ui.status( | |
338 _( | |
339 b'finished migrating %d total revisions; total change in store ' | |
340 b'size: %s\n' | |
341 ) | |
342 % (revcount, util.bytecount(dstsize - srcsize)) | |
343 ) | |
344 | |
345 | |
346 def _filterstorefile(srcrepo, dstrepo, requirements, path, mode, st): | |
347 """Determine whether to copy a store file during upgrade. | |
348 | |
349 This function is called when migrating store files from ``srcrepo`` to | |
350 ``dstrepo`` as part of upgrading a repository. | |
351 | |
352 Args: | |
353 srcrepo: repo we are copying from | |
354 dstrepo: repo we are copying to | |
355 requirements: set of requirements for ``dstrepo`` | |
356 path: store file being examined | |
357 mode: the ``ST_MODE`` file type of ``path`` | |
358 st: ``stat`` data structure for ``path`` | |
359 | |
360 Function should return ``True`` if the file is to be copied. | |
361 """ | |
362 # Skip revlogs. | |
363 if path.endswith((b'.i', b'.d', b'.n', b'.nd')): | |
364 return False | |
365 # Skip transaction related files. | |
366 if path.startswith(b'undo'): | |
367 return False | |
368 # Only copy regular files. | |
369 if mode != stat.S_IFREG: | |
370 return False | |
371 # Skip other skipped files. | |
372 if path in (b'lock', b'fncache'): | |
373 return False | |
374 | |
375 return True | |
376 | |
377 | |
378 def _finishdatamigration(ui, srcrepo, dstrepo, requirements): | |
379 """Hook point for extensions to perform additional actions during upgrade. | |
380 | |
381 This function is called after revlogs and store files have been copied but | |
382 before the new store is swapped into the original location. | |
383 """ | |
384 | |
385 | |
386 def upgrade( | |
387 ui, srcrepo, dstrepo, requirements, actions, revlogs=UPGRADE_ALL_REVLOGS | |
388 ): | |
389 """Do the low-level work of upgrading a repository. | |
390 | |
391 The upgrade is effectively performed as a copy between a source | |
392 repository and a temporary destination repository. | |
393 | |
394 The source repository is unmodified for as long as possible so the | |
395 upgrade can abort at any time without causing loss of service for | |
396 readers and without corrupting the source repository. | |
397 """ | |
398 assert srcrepo.currentwlock() | |
399 assert dstrepo.currentwlock() | |
400 | |
401 ui.status( | |
402 _( | |
403 b'(it is safe to interrupt this process any time before ' | |
404 b'data migration completes)\n' | |
405 ) | |
406 ) | |
407 | |
408 if b're-delta-all' in actions: | |
409 deltareuse = revlog.revlog.DELTAREUSENEVER | |
410 elif b're-delta-parent' in actions: | |
411 deltareuse = revlog.revlog.DELTAREUSESAMEREVS | |
412 elif b're-delta-multibase' in actions: | |
413 deltareuse = revlog.revlog.DELTAREUSESAMEREVS | |
414 elif b're-delta-fulladd' in actions: | |
415 deltareuse = revlog.revlog.DELTAREUSEFULLADD | |
416 else: | |
417 deltareuse = revlog.revlog.DELTAREUSEALWAYS | |
418 | |
419 with dstrepo.transaction(b'upgrade') as tr: | |
420 _clonerevlogs( | |
421 ui, | |
422 srcrepo, | |
423 dstrepo, | |
424 tr, | |
425 deltareuse, | |
426 b're-delta-multibase' in actions, | |
427 revlogs=revlogs, | |
428 ) | |
429 | |
430 # Now copy other files in the store directory. | |
431 # The sorted() makes execution deterministic. | |
432 for p, kind, st in sorted(srcrepo.store.vfs.readdir(b'', stat=True)): | |
433 if not _filterstorefile(srcrepo, dstrepo, requirements, p, kind, st): | |
434 continue | |
435 | |
436 srcrepo.ui.status(_(b'copying %s\n') % p) | |
437 src = srcrepo.store.rawvfs.join(p) | |
438 dst = dstrepo.store.rawvfs.join(p) | |
439 util.copyfile(src, dst, copystat=True) | |
440 | |
441 _finishdatamigration(ui, srcrepo, dstrepo, requirements) | |
442 | |
443 ui.status(_(b'data fully migrated to temporary repository\n')) | |
444 | |
445 backuppath = pycompat.mkdtemp(prefix=b'upgradebackup.', dir=srcrepo.path) | |
446 backupvfs = vfsmod.vfs(backuppath) | |
447 | |
448 # Make a backup of requires file first, as it is the first to be modified. | |
449 util.copyfile(srcrepo.vfs.join(b'requires'), backupvfs.join(b'requires')) | |
450 | |
451 # We install an arbitrary requirement that clients must not support | |
452 # as a mechanism to lock out new clients during the data swap. This is | |
453 # better than allowing a client to continue while the repository is in | |
454 # an inconsistent state. | |
455 ui.status( | |
456 _( | |
457 b'marking source repository as being upgraded; clients will be ' | |
458 b'unable to read from repository\n' | |
459 ) | |
460 ) | |
461 scmutil.writereporequirements( | |
462 srcrepo, srcrepo.requirements | {b'upgradeinprogress'} | |
463 ) | |
464 | |
465 ui.status(_(b'starting in-place swap of repository data\n')) | |
466 ui.status(_(b'replaced files will be backed up at %s\n') % backuppath) | |
467 | |
468 # Now swap in the new store directory. Doing it as a rename should make | |
469 # the operation nearly instantaneous and atomic (at least in well-behaved | |
470 # environments). | |
471 ui.status(_(b'replacing store...\n')) | |
472 tstart = util.timer() | |
473 util.rename(srcrepo.spath, backupvfs.join(b'store')) | |
474 util.rename(dstrepo.spath, srcrepo.spath) | |
475 elapsed = util.timer() - tstart | |
476 ui.status( | |
477 _( | |
478 b'store replacement complete; repository was inconsistent for ' | |
479 b'%0.1fs\n' | |
480 ) | |
481 % elapsed | |
482 ) | |
483 | |
484 # We first write the requirements file. Any new requirements will lock | |
485 # out legacy clients. | |
486 ui.status( | |
487 _( | |
488 b'finalizing requirements file and making repository readable ' | |
489 b'again\n' | |
490 ) | |
491 ) | |
492 scmutil.writereporequirements(srcrepo, requirements) | |
493 | |
494 # The lock file from the old store won't be removed because nothing has a | |
495 # reference to its new location. So clean it up manually. Alternatively, we | |
496 # could update srcrepo.svfs and other variables to point to the new | |
497 # location. This is simpler. | |
498 backupvfs.unlink(b'store/lock') | |
499 | |
500 return backuppath |