##// END OF EJS Templates
upgrade: introduce a _copyrevlog method...
marmoute -
r42918:5535a220 default
parent child Browse files
Show More
@@ -1,982 +1,1011
1 1 # upgrade.py - functions for in place upgrade of Mercurial repository
2 2 #
3 3 # Copyright (c) 2016-present, Gregory Szorc
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import stat
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 changelog,
15 15 error,
16 16 filelog,
17 17 hg,
18 18 localrepo,
19 19 manifest,
20 20 pycompat,
21 21 revlog,
22 22 scmutil,
23 23 util,
24 24 vfs as vfsmod,
25 25 )
26 26
27 27 from .utils import (
28 28 compression,
29 29 )
30 30
31 31 def requiredsourcerequirements(repo):
32 32 """Obtain requirements required to be present to upgrade a repo.
33 33
34 34 An upgrade will not be allowed if the repository doesn't have the
35 35 requirements returned by this function.
36 36 """
37 37 return {
38 38 # Introduced in Mercurial 0.9.2.
39 39 'revlogv1',
40 40 # Introduced in Mercurial 0.9.2.
41 41 'store',
42 42 }
43 43
44 44 def blocksourcerequirements(repo):
45 45 """Obtain requirements that will prevent an upgrade from occurring.
46 46
47 47 An upgrade cannot be performed if the source repository contains a
48 48 requirements in the returned set.
49 49 """
50 50 return {
51 51 # The upgrade code does not yet support these experimental features.
52 52 # This is an artificial limitation.
53 53 'treemanifest',
54 54 # This was a precursor to generaldelta and was never enabled by default.
55 55 # It should (hopefully) not exist in the wild.
56 56 'parentdelta',
57 57 # Upgrade should operate on the actual store, not the shared link.
58 58 'shared',
59 59 }
60 60
61 61 def supportremovedrequirements(repo):
62 62 """Obtain requirements that can be removed during an upgrade.
63 63
64 64 If an upgrade were to create a repository that dropped a requirement,
65 65 the dropped requirement must appear in the returned set for the upgrade
66 66 to be allowed.
67 67 """
68 68 supported = {
69 69 localrepo.SPARSEREVLOG_REQUIREMENT,
70 70 }
71 71 for name in compression.compengines:
72 72 engine = compression.compengines[name]
73 73 if engine.available() and engine.revlogheader():
74 74 supported.add(b'exp-compression-%s' % name)
75 75 if engine.name() == 'zstd':
76 76 supported.add(b'revlog-compression-zstd')
77 77 return supported
78 78
79 79 def supporteddestrequirements(repo):
80 80 """Obtain requirements that upgrade supports in the destination.
81 81
82 82 If the result of the upgrade would create requirements not in this set,
83 83 the upgrade is disallowed.
84 84
85 85 Extensions should monkeypatch this to add their custom requirements.
86 86 """
87 87 supported = {
88 88 'dotencode',
89 89 'fncache',
90 90 'generaldelta',
91 91 'revlogv1',
92 92 'store',
93 93 localrepo.SPARSEREVLOG_REQUIREMENT,
94 94 }
95 95 for name in compression.compengines:
96 96 engine = compression.compengines[name]
97 97 if engine.available() and engine.revlogheader():
98 98 supported.add(b'exp-compression-%s' % name)
99 99 if engine.name() == 'zstd':
100 100 supported.add(b'revlog-compression-zstd')
101 101 return supported
102 102
103 103 def allowednewrequirements(repo):
104 104 """Obtain requirements that can be added to a repository during upgrade.
105 105
106 106 This is used to disallow proposed requirements from being added when
107 107 they weren't present before.
108 108
109 109 We use a list of allowed requirement additions instead of a list of known
110 110 bad additions because the whitelist approach is safer and will prevent
111 111 future, unknown requirements from accidentally being added.
112 112 """
113 113 supported = {
114 114 'dotencode',
115 115 'fncache',
116 116 'generaldelta',
117 117 localrepo.SPARSEREVLOG_REQUIREMENT,
118 118 }
119 119 for name in compression.compengines:
120 120 engine = compression.compengines[name]
121 121 if engine.available() and engine.revlogheader():
122 122 supported.add(b'exp-compression-%s' % name)
123 123 if engine.name() == 'zstd':
124 124 supported.add(b'revlog-compression-zstd')
125 125 return supported
126 126
127 127 def preservedrequirements(repo):
128 128 return set()
129 129
130 130 deficiency = 'deficiency'
131 131 optimisation = 'optimization'
132 132
133 133 class improvement(object):
134 134 """Represents an improvement that can be made as part of an upgrade.
135 135
136 136 The following attributes are defined on each instance:
137 137
138 138 name
139 139 Machine-readable string uniquely identifying this improvement. It
140 140 will be mapped to an action later in the upgrade process.
141 141
142 142 type
143 143 Either ``deficiency`` or ``optimisation``. A deficiency is an obvious
144 144 problem. An optimization is an action (sometimes optional) that
145 145 can be taken to further improve the state of the repository.
146 146
147 147 description
148 148 Message intended for humans explaining the improvement in more detail,
149 149 including the implications of it. For ``deficiency`` types, should be
150 150 worded in the present tense. For ``optimisation`` types, should be
151 151 worded in the future tense.
152 152
153 153 upgrademessage
154 154 Message intended for humans explaining what an upgrade addressing this
155 155 issue will do. Should be worded in the future tense.
156 156 """
157 157 def __init__(self, name, type, description, upgrademessage):
158 158 self.name = name
159 159 self.type = type
160 160 self.description = description
161 161 self.upgrademessage = upgrademessage
162 162
163 163 def __eq__(self, other):
164 164 if not isinstance(other, improvement):
165 165 # This is what python tell use to do
166 166 return NotImplemented
167 167 return self.name == other.name
168 168
169 169 def __ne__(self, other):
170 170 return not (self == other)
171 171
172 172 def __hash__(self):
173 173 return hash(self.name)
174 174
175 175 allformatvariant = []
176 176
177 177 def registerformatvariant(cls):
178 178 allformatvariant.append(cls)
179 179 return cls
180 180
181 181 class formatvariant(improvement):
182 182 """an improvement subclass dedicated to repository format"""
183 183 type = deficiency
184 184 ### The following attributes should be defined for each class:
185 185
186 186 # machine-readable string uniquely identifying this improvement. it will be
187 187 # mapped to an action later in the upgrade process.
188 188 name = None
189 189
190 190 # message intended for humans explaining the improvement in more detail,
191 191 # including the implications of it ``deficiency`` types, should be worded
192 192 # in the present tense.
193 193 description = None
194 194
195 195 # message intended for humans explaining what an upgrade addressing this
196 196 # issue will do. should be worded in the future tense.
197 197 upgrademessage = None
198 198
199 199 # value of current Mercurial default for new repository
200 200 default = None
201 201
202 202 def __init__(self):
203 203 raise NotImplementedError()
204 204
205 205 @staticmethod
206 206 def fromrepo(repo):
207 207 """current value of the variant in the repository"""
208 208 raise NotImplementedError()
209 209
210 210 @staticmethod
211 211 def fromconfig(repo):
212 212 """current value of the variant in the configuration"""
213 213 raise NotImplementedError()
214 214
215 215 class requirementformatvariant(formatvariant):
216 216 """formatvariant based on a 'requirement' name.
217 217
218 218 Many format variant are controlled by a 'requirement'. We define a small
219 219 subclass to factor the code.
220 220 """
221 221
222 222 # the requirement that control this format variant
223 223 _requirement = None
224 224
225 225 @staticmethod
226 226 def _newreporequirements(ui):
227 227 return localrepo.newreporequirements(
228 228 ui, localrepo.defaultcreateopts(ui))
229 229
230 230 @classmethod
231 231 def fromrepo(cls, repo):
232 232 assert cls._requirement is not None
233 233 return cls._requirement in repo.requirements
234 234
235 235 @classmethod
236 236 def fromconfig(cls, repo):
237 237 assert cls._requirement is not None
238 238 return cls._requirement in cls._newreporequirements(repo.ui)
239 239
240 240 @registerformatvariant
241 241 class fncache(requirementformatvariant):
242 242 name = 'fncache'
243 243
244 244 _requirement = 'fncache'
245 245
246 246 default = True
247 247
248 248 description = _('long and reserved filenames may not work correctly; '
249 249 'repository performance is sub-optimal')
250 250
251 251 upgrademessage = _('repository will be more resilient to storing '
252 252 'certain paths and performance of certain '
253 253 'operations should be improved')
254 254
255 255 @registerformatvariant
256 256 class dotencode(requirementformatvariant):
257 257 name = 'dotencode'
258 258
259 259 _requirement = 'dotencode'
260 260
261 261 default = True
262 262
263 263 description = _('storage of filenames beginning with a period or '
264 264 'space may not work correctly')
265 265
266 266 upgrademessage = _('repository will be better able to store files '
267 267 'beginning with a space or period')
268 268
269 269 @registerformatvariant
270 270 class generaldelta(requirementformatvariant):
271 271 name = 'generaldelta'
272 272
273 273 _requirement = 'generaldelta'
274 274
275 275 default = True
276 276
277 277 description = _('deltas within internal storage are unable to '
278 278 'choose optimal revisions; repository is larger and '
279 279 'slower than it could be; interaction with other '
280 280 'repositories may require extra network and CPU '
281 281 'resources, making "hg push" and "hg pull" slower')
282 282
283 283 upgrademessage = _('repository storage will be able to create '
284 284 'optimal deltas; new repository data will be '
285 285 'smaller and read times should decrease; '
286 286 'interacting with other repositories using this '
287 287 'storage model should require less network and '
288 288 'CPU resources, making "hg push" and "hg pull" '
289 289 'faster')
290 290
291 291 @registerformatvariant
292 292 class sparserevlog(requirementformatvariant):
293 293 name = 'sparserevlog'
294 294
295 295 _requirement = localrepo.SPARSEREVLOG_REQUIREMENT
296 296
297 297 default = True
298 298
299 299 description = _('in order to limit disk reading and memory usage on older '
300 300 'version, the span of a delta chain from its root to its '
301 301 'end is limited, whatever the relevant data in this span. '
302 302 'This can severly limit Mercurial ability to build good '
303 303 'chain of delta resulting is much more storage space being '
304 304 'taken and limit reusability of on disk delta during '
305 305 'exchange.'
306 306 )
307 307
308 308 upgrademessage = _('Revlog supports delta chain with more unused data '
309 309 'between payload. These gaps will be skipped at read '
310 310 'time. This allows for better delta chains, making a '
311 311 'better compression and faster exchange with server.')
312 312
313 313 @registerformatvariant
314 314 class removecldeltachain(formatvariant):
315 315 name = 'plain-cl-delta'
316 316
317 317 default = True
318 318
319 319 description = _('changelog storage is using deltas instead of '
320 320 'raw entries; changelog reading and any '
321 321 'operation relying on changelog data are slower '
322 322 'than they could be')
323 323
324 324 upgrademessage = _('changelog storage will be reformated to '
325 325 'store raw entries; changelog reading will be '
326 326 'faster; changelog size may be reduced')
327 327
328 328 @staticmethod
329 329 def fromrepo(repo):
330 330 # Mercurial 4.0 changed changelogs to not use delta chains. Search for
331 331 # changelogs with deltas.
332 332 cl = repo.changelog
333 333 chainbase = cl.chainbase
334 334 return all(rev == chainbase(rev) for rev in cl)
335 335
336 336 @staticmethod
337 337 def fromconfig(repo):
338 338 return True
339 339
340 340 @registerformatvariant
341 341 class compressionengine(formatvariant):
342 342 name = 'compression'
343 343 default = 'zlib'
344 344
345 345 description = _('Compresion algorithm used to compress data. '
346 346 'Some engine are faster than other')
347 347
348 348 upgrademessage = _('revlog content will be recompressed with the new '
349 349 'algorithm.')
350 350
351 351 @classmethod
352 352 def fromrepo(cls, repo):
353 353 # we allow multiple compression engine requirement to co-exist because
354 354 # strickly speaking, revlog seems to support mixed compression style.
355 355 #
356 356 # The compression used for new entries will be "the last one"
357 357 compression = 'zlib'
358 358 for req in repo.requirements:
359 359 prefix = req.startswith
360 360 if prefix('revlog-compression-') or prefix('exp-compression-'):
361 361 compression = req.split('-', 2)[2]
362 362 return compression
363 363
364 364 @classmethod
365 365 def fromconfig(cls, repo):
366 366 return repo.ui.config('format', 'revlog-compression')
367 367
368 368 @registerformatvariant
369 369 class compressionlevel(formatvariant):
370 370 name = 'compression-level'
371 371 default = 'default'
372 372
373 373 description = _('compression level')
374 374
375 375 upgrademessage = _('revlog content will be recompressed')
376 376
377 377 @classmethod
378 378 def fromrepo(cls, repo):
379 379 comp = compressionengine.fromrepo(repo)
380 380 level = None
381 381 if comp == 'zlib':
382 382 level = repo.ui.configint('storage', 'revlog.zlib.level')
383 383 elif comp == 'zstd':
384 384 level = repo.ui.configint('storage', 'revlog.zstd.level')
385 385 if level is None:
386 386 return 'default'
387 387 return bytes(level)
388 388
389 389 @classmethod
390 390 def fromconfig(cls, repo):
391 391 comp = compressionengine.fromconfig(repo)
392 392 level = None
393 393 if comp == 'zlib':
394 394 level = repo.ui.configint('storage', 'revlog.zlib.level')
395 395 elif comp == 'zstd':
396 396 level = repo.ui.configint('storage', 'revlog.zstd.level')
397 397 if level is None:
398 398 return 'default'
399 399 return bytes(level)
400 400
401 401 def finddeficiencies(repo):
402 402 """returns a list of deficiencies that the repo suffer from"""
403 403 deficiencies = []
404 404
405 405 # We could detect lack of revlogv1 and store here, but they were added
406 406 # in 0.9.2 and we don't support upgrading repos without these
407 407 # requirements, so let's not bother.
408 408
409 409 for fv in allformatvariant:
410 410 if not fv.fromrepo(repo):
411 411 deficiencies.append(fv)
412 412
413 413 return deficiencies
414 414
415 415 # search without '-' to support older form on newer client.
416 416 #
417 417 # We don't enforce backward compatibility for debug command so this
418 418 # might eventually be dropped. However, having to use two different
419 419 # forms in script when comparing result is anoying enough to add
420 420 # backward compatibility for a while.
421 421 legacy_opts_map = {
422 422 'redeltaparent': 're-delta-parent',
423 423 'redeltamultibase': 're-delta-multibase',
424 424 'redeltaall': 're-delta-all',
425 425 'redeltafulladd': 're-delta-fulladd',
426 426 }
427 427
428 428 def findoptimizations(repo):
429 429 """Determine optimisation that could be used during upgrade"""
430 430 # These are unconditionally added. There is logic later that figures out
431 431 # which ones to apply.
432 432 optimizations = []
433 433
434 434 optimizations.append(improvement(
435 435 name='re-delta-parent',
436 436 type=optimisation,
437 437 description=_('deltas within internal storage will be recalculated to '
438 438 'choose an optimal base revision where this was not '
439 439 'already done; the size of the repository may shrink and '
440 440 'various operations may become faster; the first time '
441 441 'this optimization is performed could slow down upgrade '
442 442 'execution considerably; subsequent invocations should '
443 443 'not run noticeably slower'),
444 444 upgrademessage=_('deltas within internal storage will choose a new '
445 445 'base revision if needed')))
446 446
447 447 optimizations.append(improvement(
448 448 name='re-delta-multibase',
449 449 type=optimisation,
450 450 description=_('deltas within internal storage will be recalculated '
451 451 'against multiple base revision and the smallest '
452 452 'difference will be used; the size of the repository may '
453 453 'shrink significantly when there are many merges; this '
454 454 'optimization will slow down execution in proportion to '
455 455 'the number of merges in the repository and the amount '
456 456 'of files in the repository; this slow down should not '
457 457 'be significant unless there are tens of thousands of '
458 458 'files and thousands of merges'),
459 459 upgrademessage=_('deltas within internal storage will choose an '
460 460 'optimal delta by computing deltas against multiple '
461 461 'parents; may slow down execution time '
462 462 'significantly')))
463 463
464 464 optimizations.append(improvement(
465 465 name='re-delta-all',
466 466 type=optimisation,
467 467 description=_('deltas within internal storage will always be '
468 468 'recalculated without reusing prior deltas; this will '
469 469 'likely make execution run several times slower; this '
470 470 'optimization is typically not needed'),
471 471 upgrademessage=_('deltas within internal storage will be fully '
472 472 'recomputed; this will likely drastically slow down '
473 473 'execution time')))
474 474
475 475 optimizations.append(improvement(
476 476 name='re-delta-fulladd',
477 477 type=optimisation,
478 478 description=_('every revision will be re-added as if it was new '
479 479 'content. It will go through the full storage '
480 480 'mechanism giving extensions a chance to process it '
481 481 '(eg. lfs). This is similar to "re-delta-all" but even '
482 482 'slower since more logic is involved.'),
483 483 upgrademessage=_('each revision will be added as new content to the '
484 484 'internal storage; this will likely drastically slow '
485 485 'down execution time, but some extensions might need '
486 486 'it')))
487 487
488 488 return optimizations
489 489
490 490 def determineactions(repo, deficiencies, sourcereqs, destreqs):
491 491 """Determine upgrade actions that will be performed.
492 492
493 493 Given a list of improvements as returned by ``finddeficiencies`` and
494 494 ``findoptimizations``, determine the list of upgrade actions that
495 495 will be performed.
496 496
497 497 The role of this function is to filter improvements if needed, apply
498 498 recommended optimizations from the improvements list that make sense,
499 499 etc.
500 500
501 501 Returns a list of action names.
502 502 """
503 503 newactions = []
504 504
505 505 knownreqs = supporteddestrequirements(repo)
506 506
507 507 for d in deficiencies:
508 508 name = d.name
509 509
510 510 # If the action is a requirement that doesn't show up in the
511 511 # destination requirements, prune the action.
512 512 if name in knownreqs and name not in destreqs:
513 513 continue
514 514
515 515 newactions.append(d)
516 516
517 517 # FUTURE consider adding some optimizations here for certain transitions.
518 518 # e.g. adding generaldelta could schedule parent redeltas.
519 519
520 520 return newactions
521 521
522 522 def _revlogfrompath(repo, path):
523 523 """Obtain a revlog from a repo path.
524 524
525 525 An instance of the appropriate class is returned.
526 526 """
527 527 if path == '00changelog.i':
528 528 return changelog.changelog(repo.svfs)
529 529 elif path.endswith('00manifest.i'):
530 530 mandir = path[:-len('00manifest.i')]
531 531 return manifest.manifestrevlog(repo.svfs, tree=mandir)
532 532 else:
533 533 #reverse of "/".join(("data", path + ".i"))
534 534 return filelog.filelog(repo.svfs, path[5:-2])
535 535
536 def _copyrevlog(tr, destrepo, oldrl, unencodedname):
537 """copy all relevant files for `oldrl` into `destrepo` store
538
539 Files are copied "as is" without any transformation. The copy is performed
540 without extra checks. Callers are responsible for making sure the copied
541 content is compatible with format of the destination repository.
542 """
543 oldrl = getattr(oldrl, '_revlog', oldrl)
544 newrl = _revlogfrompath(destrepo, unencodedname)
545 newrl = getattr(newrl, '_revlog', newrl)
546
547 oldvfs = oldrl.opener
548 newvfs = newrl.opener
549 oldindex = oldvfs.join(oldrl.indexfile)
550 newindex = newvfs.join(newrl.indexfile)
551 olddata = oldvfs.join(oldrl.datafile)
552 newdata = newvfs.join(newrl.datafile)
553
554 newdir = newvfs.dirname(newrl.indexfile)
555 newvfs.makedirs(newdir)
556
557 util.copyfile(oldindex, newindex)
558 if oldrl.opener.exists(olddata):
559 util.copyfile(olddata, newdata)
560
561 if not (unencodedname.endswith('00changelog.i')
562 or unencodedname.endswith('00manifest.i')):
563 destrepo.svfs.fncache.add(unencodedname)
564
536 565 def _clonerevlogs(ui, srcrepo, dstrepo, tr, deltareuse, forcedeltabothparents):
537 566 """Copy revlogs between 2 repos."""
538 567 revcount = 0
539 568 srcsize = 0
540 569 srcrawsize = 0
541 570 dstsize = 0
542 571 fcount = 0
543 572 frevcount = 0
544 573 fsrcsize = 0
545 574 frawsize = 0
546 575 fdstsize = 0
547 576 mcount = 0
548 577 mrevcount = 0
549 578 msrcsize = 0
550 579 mrawsize = 0
551 580 mdstsize = 0
552 581 crevcount = 0
553 582 csrcsize = 0
554 583 crawsize = 0
555 584 cdstsize = 0
556 585
557 586 alldatafiles = list(srcrepo.store.walk())
558 587
559 588 # Perform a pass to collect metadata. This validates we can open all
560 589 # source files and allows a unified progress bar to be displayed.
561 590 for unencoded, encoded, size in alldatafiles:
562 591 if unencoded.endswith('.d'):
563 592 continue
564 593
565 594 rl = _revlogfrompath(srcrepo, unencoded)
566 595
567 596 info = rl.storageinfo(exclusivefiles=True, revisionscount=True,
568 597 trackedsize=True, storedsize=True)
569 598
570 599 revcount += info['revisionscount'] or 0
571 600 datasize = info['storedsize'] or 0
572 601 rawsize = info['trackedsize'] or 0
573 602
574 603 srcsize += datasize
575 604 srcrawsize += rawsize
576 605
577 606 # This is for the separate progress bars.
578 607 if isinstance(rl, changelog.changelog):
579 608 crevcount += len(rl)
580 609 csrcsize += datasize
581 610 crawsize += rawsize
582 611 elif isinstance(rl, manifest.manifestrevlog):
583 612 mcount += 1
584 613 mrevcount += len(rl)
585 614 msrcsize += datasize
586 615 mrawsize += rawsize
587 616 elif isinstance(rl, filelog.filelog):
588 617 fcount += 1
589 618 frevcount += len(rl)
590 619 fsrcsize += datasize
591 620 frawsize += rawsize
592 621 else:
593 622 error.ProgrammingError('unknown revlog type')
594 623
595 624 if not revcount:
596 625 return
597 626
598 627 ui.write(_('migrating %d total revisions (%d in filelogs, %d in manifests, '
599 628 '%d in changelog)\n') %
600 629 (revcount, frevcount, mrevcount, crevcount))
601 630 ui.write(_('migrating %s in store; %s tracked data\n') % (
602 631 (util.bytecount(srcsize), util.bytecount(srcrawsize))))
603 632
604 633 # Used to keep track of progress.
605 634 progress = None
606 635 def oncopiedrevision(rl, rev, node):
607 636 progress.increment()
608 637
609 638 # Do the actual copying.
610 639 # FUTURE this operation can be farmed off to worker processes.
611 640 seen = set()
612 641 for unencoded, encoded, size in alldatafiles:
613 642 if unencoded.endswith('.d'):
614 643 continue
615 644
616 645 oldrl = _revlogfrompath(srcrepo, unencoded)
617 646 newrl = _revlogfrompath(dstrepo, unencoded)
618 647
619 648 if isinstance(oldrl, changelog.changelog) and 'c' not in seen:
620 649 ui.write(_('finished migrating %d manifest revisions across %d '
621 650 'manifests; change in size: %s\n') %
622 651 (mrevcount, mcount, util.bytecount(mdstsize - msrcsize)))
623 652
624 653 ui.write(_('migrating changelog containing %d revisions '
625 654 '(%s in store; %s tracked data)\n') %
626 655 (crevcount, util.bytecount(csrcsize),
627 656 util.bytecount(crawsize)))
628 657 seen.add('c')
629 658 progress = srcrepo.ui.makeprogress(_('changelog revisions'),
630 659 total=crevcount)
631 660 elif isinstance(oldrl, manifest.manifestrevlog) and 'm' not in seen:
632 661 ui.write(_('finished migrating %d filelog revisions across %d '
633 662 'filelogs; change in size: %s\n') %
634 663 (frevcount, fcount, util.bytecount(fdstsize - fsrcsize)))
635 664
636 665 ui.write(_('migrating %d manifests containing %d revisions '
637 666 '(%s in store; %s tracked data)\n') %
638 667 (mcount, mrevcount, util.bytecount(msrcsize),
639 668 util.bytecount(mrawsize)))
640 669 seen.add('m')
641 670 if progress:
642 671 progress.complete()
643 672 progress = srcrepo.ui.makeprogress(_('manifest revisions'),
644 673 total=mrevcount)
645 674 elif 'f' not in seen:
646 675 ui.write(_('migrating %d filelogs containing %d revisions '
647 676 '(%s in store; %s tracked data)\n') %
648 677 (fcount, frevcount, util.bytecount(fsrcsize),
649 678 util.bytecount(frawsize)))
650 679 seen.add('f')
651 680 if progress:
652 681 progress.complete()
653 682 progress = srcrepo.ui.makeprogress(_('file revisions'),
654 683 total=frevcount)
655 684
656 685
657 686 ui.note(_('cloning %d revisions from %s\n') % (len(oldrl), unencoded))
658 687 oldrl.clone(tr, newrl, addrevisioncb=oncopiedrevision,
659 688 deltareuse=deltareuse,
660 689 forcedeltabothparents=forcedeltabothparents)
661 690
662 691 info = newrl.storageinfo(storedsize=True)
663 692 datasize = info['storedsize'] or 0
664 693
665 694 dstsize += datasize
666 695
667 696 if isinstance(newrl, changelog.changelog):
668 697 cdstsize += datasize
669 698 elif isinstance(newrl, manifest.manifestrevlog):
670 699 mdstsize += datasize
671 700 else:
672 701 fdstsize += datasize
673 702
674 703 progress.complete()
675 704
676 705 ui.write(_('finished migrating %d changelog revisions; change in size: '
677 706 '%s\n') % (crevcount, util.bytecount(cdstsize - csrcsize)))
678 707
679 708 ui.write(_('finished migrating %d total revisions; total change in store '
680 709 'size: %s\n') % (revcount, util.bytecount(dstsize - srcsize)))
681 710
682 711 def _filterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
683 712 """Determine whether to copy a store file during upgrade.
684 713
685 714 This function is called when migrating store files from ``srcrepo`` to
686 715 ``dstrepo`` as part of upgrading a repository.
687 716
688 717 Args:
689 718 srcrepo: repo we are copying from
690 719 dstrepo: repo we are copying to
691 720 requirements: set of requirements for ``dstrepo``
692 721 path: store file being examined
693 722 mode: the ``ST_MODE`` file type of ``path``
694 723 st: ``stat`` data structure for ``path``
695 724
696 725 Function should return ``True`` if the file is to be copied.
697 726 """
698 727 # Skip revlogs.
699 728 if path.endswith(('.i', '.d')):
700 729 return False
701 730 # Skip transaction related files.
702 731 if path.startswith('undo'):
703 732 return False
704 733 # Only copy regular files.
705 734 if mode != stat.S_IFREG:
706 735 return False
707 736 # Skip other skipped files.
708 737 if path in ('lock', 'fncache'):
709 738 return False
710 739
711 740 return True
712 741
713 742 def _finishdatamigration(ui, srcrepo, dstrepo, requirements):
714 743 """Hook point for extensions to perform additional actions during upgrade.
715 744
716 745 This function is called after revlogs and store files have been copied but
717 746 before the new store is swapped into the original location.
718 747 """
719 748
720 749 def _upgraderepo(ui, srcrepo, dstrepo, requirements, actions):
721 750 """Do the low-level work of upgrading a repository.
722 751
723 752 The upgrade is effectively performed as a copy between a source
724 753 repository and a temporary destination repository.
725 754
726 755 The source repository is unmodified for as long as possible so the
727 756 upgrade can abort at any time without causing loss of service for
728 757 readers and without corrupting the source repository.
729 758 """
730 759 assert srcrepo.currentwlock()
731 760 assert dstrepo.currentwlock()
732 761
733 762 ui.write(_('(it is safe to interrupt this process any time before '
734 763 'data migration completes)\n'))
735 764
736 765 if 're-delta-all' in actions:
737 766 deltareuse = revlog.revlog.DELTAREUSENEVER
738 767 elif 're-delta-parent' in actions:
739 768 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
740 769 elif 're-delta-multibase' in actions:
741 770 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
742 771 elif 're-delta-fulladd' in actions:
743 772 deltareuse = revlog.revlog.DELTAREUSEFULLADD
744 773 else:
745 774 deltareuse = revlog.revlog.DELTAREUSEALWAYS
746 775
747 776 with dstrepo.transaction('upgrade') as tr:
748 777 _clonerevlogs(ui, srcrepo, dstrepo, tr, deltareuse,
749 778 're-delta-multibase' in actions)
750 779
751 780 # Now copy other files in the store directory.
752 781 # The sorted() makes execution deterministic.
753 782 for p, kind, st in sorted(srcrepo.store.vfs.readdir('', stat=True)):
754 783 if not _filterstorefile(srcrepo, dstrepo, requirements,
755 784 p, kind, st):
756 785 continue
757 786
758 787 srcrepo.ui.write(_('copying %s\n') % p)
759 788 src = srcrepo.store.rawvfs.join(p)
760 789 dst = dstrepo.store.rawvfs.join(p)
761 790 util.copyfile(src, dst, copystat=True)
762 791
763 792 _finishdatamigration(ui, srcrepo, dstrepo, requirements)
764 793
765 794 ui.write(_('data fully migrated to temporary repository\n'))
766 795
767 796 backuppath = pycompat.mkdtemp(prefix='upgradebackup.', dir=srcrepo.path)
768 797 backupvfs = vfsmod.vfs(backuppath)
769 798
770 799 # Make a backup of requires file first, as it is the first to be modified.
771 800 util.copyfile(srcrepo.vfs.join('requires'), backupvfs.join('requires'))
772 801
773 802 # We install an arbitrary requirement that clients must not support
774 803 # as a mechanism to lock out new clients during the data swap. This is
775 804 # better than allowing a client to continue while the repository is in
776 805 # an inconsistent state.
777 806 ui.write(_('marking source repository as being upgraded; clients will be '
778 807 'unable to read from repository\n'))
779 808 scmutil.writerequires(srcrepo.vfs,
780 809 srcrepo.requirements | {'upgradeinprogress'})
781 810
782 811 ui.write(_('starting in-place swap of repository data\n'))
783 812 ui.write(_('replaced files will be backed up at %s\n') %
784 813 backuppath)
785 814
786 815 # Now swap in the new store directory. Doing it as a rename should make
787 816 # the operation nearly instantaneous and atomic (at least in well-behaved
788 817 # environments).
789 818 ui.write(_('replacing store...\n'))
790 819 tstart = util.timer()
791 820 util.rename(srcrepo.spath, backupvfs.join('store'))
792 821 util.rename(dstrepo.spath, srcrepo.spath)
793 822 elapsed = util.timer() - tstart
794 823 ui.write(_('store replacement complete; repository was inconsistent for '
795 824 '%0.1fs\n') % elapsed)
796 825
797 826 # We first write the requirements file. Any new requirements will lock
798 827 # out legacy clients.
799 828 ui.write(_('finalizing requirements file and making repository readable '
800 829 'again\n'))
801 830 scmutil.writerequires(srcrepo.vfs, requirements)
802 831
803 832 # The lock file from the old store won't be removed because nothing has a
804 833 # reference to its new location. So clean it up manually. Alternatively, we
805 834 # could update srcrepo.svfs and other variables to point to the new
806 835 # location. This is simpler.
807 836 backupvfs.unlink('store/lock')
808 837
809 838 return backuppath
810 839
811 840 def upgraderepo(ui, repo, run=False, optimize=None, backup=True):
812 841 """Upgrade a repository in place."""
813 842 if optimize is None:
814 843 optimize = []
815 844 optimize = set(legacy_opts_map.get(o, o) for o in optimize)
816 845 repo = repo.unfiltered()
817 846
818 847 # Ensure the repository can be upgraded.
819 848 missingreqs = requiredsourcerequirements(repo) - repo.requirements
820 849 if missingreqs:
821 850 raise error.Abort(_('cannot upgrade repository; requirement '
822 851 'missing: %s') % _(', ').join(sorted(missingreqs)))
823 852
824 853 blockedreqs = blocksourcerequirements(repo) & repo.requirements
825 854 if blockedreqs:
826 855 raise error.Abort(_('cannot upgrade repository; unsupported source '
827 856 'requirement: %s') %
828 857 _(', ').join(sorted(blockedreqs)))
829 858
830 859 # FUTURE there is potentially a need to control the wanted requirements via
831 860 # command arguments or via an extension hook point.
832 861 newreqs = localrepo.newreporequirements(
833 862 repo.ui, localrepo.defaultcreateopts(repo.ui))
834 863 newreqs.update(preservedrequirements(repo))
835 864
836 865 noremovereqs = (repo.requirements - newreqs -
837 866 supportremovedrequirements(repo))
838 867 if noremovereqs:
839 868 raise error.Abort(_('cannot upgrade repository; requirement would be '
840 869 'removed: %s') % _(', ').join(sorted(noremovereqs)))
841 870
842 871 noaddreqs = (newreqs - repo.requirements -
843 872 allowednewrequirements(repo))
844 873 if noaddreqs:
845 874 raise error.Abort(_('cannot upgrade repository; do not support adding '
846 875 'requirement: %s') %
847 876 _(', ').join(sorted(noaddreqs)))
848 877
849 878 unsupportedreqs = newreqs - supporteddestrequirements(repo)
850 879 if unsupportedreqs:
851 880 raise error.Abort(_('cannot upgrade repository; do not support '
852 881 'destination requirement: %s') %
853 882 _(', ').join(sorted(unsupportedreqs)))
854 883
855 884 # Find and validate all improvements that can be made.
856 885 alloptimizations = findoptimizations(repo)
857 886
858 887 # Apply and Validate arguments.
859 888 optimizations = []
860 889 for o in alloptimizations:
861 890 if o.name in optimize:
862 891 optimizations.append(o)
863 892 optimize.discard(o.name)
864 893
865 894 if optimize: # anything left is unknown
866 895 raise error.Abort(_('unknown optimization action requested: %s') %
867 896 ', '.join(sorted(optimize)),
868 897 hint=_('run without arguments to see valid '
869 898 'optimizations'))
870 899
871 900 deficiencies = finddeficiencies(repo)
872 901 actions = determineactions(repo, deficiencies, repo.requirements, newreqs)
873 902 actions.extend(o for o in sorted(optimizations)
874 903 # determineactions could have added optimisation
875 904 if o not in actions)
876 905
877 906 def printrequirements():
878 907 ui.write(_('requirements\n'))
879 908 ui.write(_(' preserved: %s\n') %
880 909 _(', ').join(sorted(newreqs & repo.requirements)))
881 910
882 911 if repo.requirements - newreqs:
883 912 ui.write(_(' removed: %s\n') %
884 913 _(', ').join(sorted(repo.requirements - newreqs)))
885 914
886 915 if newreqs - repo.requirements:
887 916 ui.write(_(' added: %s\n') %
888 917 _(', ').join(sorted(newreqs - repo.requirements)))
889 918
890 919 ui.write('\n')
891 920
892 921 def printupgradeactions():
893 922 for a in actions:
894 923 ui.write('%s\n %s\n\n' % (a.name, a.upgrademessage))
895 924
896 925 if not run:
897 926 fromconfig = []
898 927 onlydefault = []
899 928
900 929 for d in deficiencies:
901 930 if d.fromconfig(repo):
902 931 fromconfig.append(d)
903 932 elif d.default:
904 933 onlydefault.append(d)
905 934
906 935 if fromconfig or onlydefault:
907 936
908 937 if fromconfig:
909 938 ui.write(_('repository lacks features recommended by '
910 939 'current config options:\n\n'))
911 940 for i in fromconfig:
912 941 ui.write('%s\n %s\n\n' % (i.name, i.description))
913 942
914 943 if onlydefault:
915 944 ui.write(_('repository lacks features used by the default '
916 945 'config options:\n\n'))
917 946 for i in onlydefault:
918 947 ui.write('%s\n %s\n\n' % (i.name, i.description))
919 948
920 949 ui.write('\n')
921 950 else:
922 951 ui.write(_('(no feature deficiencies found in existing '
923 952 'repository)\n'))
924 953
925 954 ui.write(_('performing an upgrade with "--run" will make the following '
926 955 'changes:\n\n'))
927 956
928 957 printrequirements()
929 958 printupgradeactions()
930 959
931 960 unusedoptimize = [i for i in alloptimizations if i not in actions]
932 961
933 962 if unusedoptimize:
934 963 ui.write(_('additional optimizations are available by specifying '
935 964 '"--optimize <name>":\n\n'))
936 965 for i in unusedoptimize:
937 966 ui.write(_('%s\n %s\n\n') % (i.name, i.description))
938 967 return
939 968
940 969 # Else we're in the run=true case.
941 970 ui.write(_('upgrade will perform the following actions:\n\n'))
942 971 printrequirements()
943 972 printupgradeactions()
944 973
945 974 upgradeactions = [a.name for a in actions]
946 975
947 976 ui.write(_('beginning upgrade...\n'))
948 977 with repo.wlock(), repo.lock():
949 978 ui.write(_('repository locked and read-only\n'))
950 979 # Our strategy for upgrading the repository is to create a new,
951 980 # temporary repository, write data to it, then do a swap of the
952 981 # data. There are less heavyweight ways to do this, but it is easier
953 982 # to create a new repo object than to instantiate all the components
954 983 # (like the store) separately.
955 984 tmppath = pycompat.mkdtemp(prefix='upgrade.', dir=repo.path)
956 985 backuppath = None
957 986 try:
958 987 ui.write(_('creating temporary repository to stage migrated '
959 988 'data: %s\n') % tmppath)
960 989
961 990 # clone ui without using ui.copy because repo.ui is protected
962 991 repoui = repo.ui.__class__(repo.ui)
963 992 dstrepo = hg.repository(repoui, path=tmppath, create=True)
964 993
965 994 with dstrepo.wlock(), dstrepo.lock():
966 995 backuppath = _upgraderepo(ui, repo, dstrepo, newreqs,
967 996 upgradeactions)
968 997 if not (backup or backuppath is None):
969 998 ui.write(_('removing old repository content%s\n') % backuppath)
970 999 repo.vfs.rmtree(backuppath, forcibly=True)
971 1000 backuppath = None
972 1001
973 1002 finally:
974 1003 ui.write(_('removing temporary repository %s\n') % tmppath)
975 1004 repo.vfs.rmtree(tmppath, forcibly=True)
976 1005
977 1006 if backuppath:
978 1007 ui.warn(_('copy of old repository backed up at %s\n') %
979 1008 backuppath)
980 1009 ui.warn(_('the old repository will not be deleted; remove '
981 1010 'it to free up disk space once the upgraded '
982 1011 'repository is verified\n'))
General Comments 0
You need to be logged in to leave comments. Login now