##// END OF EJS Templates
upgrade: walk the source store file only once...
marmoute -
r42916:896fb9de default
parent child Browse files
Show More
@@ -1,980 +1,982 b''
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 536 def _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse, forcedeltabothparents):
537 537 """Copy revlogs between 2 repos."""
538 538 revcount = 0
539 539 srcsize = 0
540 540 srcrawsize = 0
541 541 dstsize = 0
542 542 fcount = 0
543 543 frevcount = 0
544 544 fsrcsize = 0
545 545 frawsize = 0
546 546 fdstsize = 0
547 547 mcount = 0
548 548 mrevcount = 0
549 549 msrcsize = 0
550 550 mrawsize = 0
551 551 mdstsize = 0
552 552 crevcount = 0
553 553 csrcsize = 0
554 554 crawsize = 0
555 555 cdstsize = 0
556 556
557 alldatafiles = list(srcrepo.store.walk())
558
557 559 # Perform a pass to collect metadata. This validates we can open all
558 560 # source files and allows a unified progress bar to be displayed.
559 for unencoded, encoded, size in srcrepo.store.walk():
561 for unencoded, encoded, size in alldatafiles:
560 562 if unencoded.endswith('.d'):
561 563 continue
562 564
563 565 rl = _revlogfrompath(srcrepo, unencoded)
564 566
565 567 info = rl.storageinfo(exclusivefiles=True, revisionscount=True,
566 568 trackedsize=True, storedsize=True)
567 569
568 570 revcount += info['revisionscount'] or 0
569 571 datasize = info['storedsize'] or 0
570 572 rawsize = info['trackedsize'] or 0
571 573
572 574 srcsize += datasize
573 575 srcrawsize += rawsize
574 576
575 577 # This is for the separate progress bars.
576 578 if isinstance(rl, changelog.changelog):
577 579 crevcount += len(rl)
578 580 csrcsize += datasize
579 581 crawsize += rawsize
580 582 elif isinstance(rl, manifest.manifestrevlog):
581 583 mcount += 1
582 584 mrevcount += len(rl)
583 585 msrcsize += datasize
584 586 mrawsize += rawsize
585 587 elif isinstance(rl, filelog.filelog):
586 588 fcount += 1
587 589 frevcount += len(rl)
588 590 fsrcsize += datasize
589 591 frawsize += rawsize
590 592 else:
591 593 error.ProgrammingError('unknown revlog type')
592 594
593 595 if not revcount:
594 596 return
595 597
596 598 ui.write(_('migrating %d total revisions (%d in filelogs, %d in manifests, '
597 599 '%d in changelog)\n') %
598 600 (revcount, frevcount, mrevcount, crevcount))
599 601 ui.write(_('migrating %s in store; %s tracked data\n') % (
600 602 (util.bytecount(srcsize), util.bytecount(srcrawsize))))
601 603
602 604 # Used to keep track of progress.
603 605 progress = None
604 606 def oncopiedrevision(rl, rev, node):
605 607 progress.increment()
606 608
607 609 # Do the actual copying.
608 610 # FUTURE this operation can be farmed off to worker processes.
609 611 seen = set()
610 for unencoded, encoded, size in srcrepo.store.walk():
612 for unencoded, encoded, size in alldatafiles:
611 613 if unencoded.endswith('.d'):
612 614 continue
613 615
614 616 oldrl = _revlogfrompath(srcrepo, unencoded)
615 617 newrl = _revlogfrompath(dstrepo, unencoded)
616 618
617 619 if isinstance(oldrl, changelog.changelog) and 'c' not in seen:
618 620 ui.write(_('finished migrating %d manifest revisions across %d '
619 621 'manifests; change in size: %s\n') %
620 622 (mrevcount, mcount, util.bytecount(mdstsize - msrcsize)))
621 623
622 624 ui.write(_('migrating changelog containing %d revisions '
623 625 '(%s in store; %s tracked data)\n') %
624 626 (crevcount, util.bytecount(csrcsize),
625 627 util.bytecount(crawsize)))
626 628 seen.add('c')
627 629 progress = srcrepo.ui.makeprogress(_('changelog revisions'),
628 630 total=crevcount)
629 631 elif isinstance(oldrl, manifest.manifestrevlog) and 'm' not in seen:
630 632 ui.write(_('finished migrating %d filelog revisions across %d '
631 633 'filelogs; change in size: %s\n') %
632 634 (frevcount, fcount, util.bytecount(fdstsize - fsrcsize)))
633 635
634 636 ui.write(_('migrating %d manifests containing %d revisions '
635 637 '(%s in store; %s tracked data)\n') %
636 638 (mcount, mrevcount, util.bytecount(msrcsize),
637 639 util.bytecount(mrawsize)))
638 640 seen.add('m')
639 641 if progress:
640 642 progress.complete()
641 643 progress = srcrepo.ui.makeprogress(_('manifest revisions'),
642 644 total=mrevcount)
643 645 elif 'f' not in seen:
644 646 ui.write(_('migrating %d filelogs containing %d revisions '
645 647 '(%s in store; %s tracked data)\n') %
646 648 (fcount, frevcount, util.bytecount(fsrcsize),
647 649 util.bytecount(frawsize)))
648 650 seen.add('f')
649 651 if progress:
650 652 progress.complete()
651 653 progress = srcrepo.ui.makeprogress(_('file revisions'),
652 654 total=frevcount)
653 655
654 656
655 657 ui.note(_('cloning %d revisions from %s\n') % (len(oldrl), unencoded))
656 658 oldrl.clone(tr, newrl, addrevisioncb=oncopiedrevision,
657 659 deltareuse=deltareuse,
658 660 forcedeltabothparents=forcedeltabothparents)
659 661
660 662 info = newrl.storageinfo(storedsize=True)
661 663 datasize = info['storedsize'] or 0
662 664
663 665 dstsize += datasize
664 666
665 667 if isinstance(newrl, changelog.changelog):
666 668 cdstsize += datasize
667 669 elif isinstance(newrl, manifest.manifestrevlog):
668 670 mdstsize += datasize
669 671 else:
670 672 fdstsize += datasize
671 673
672 674 progress.complete()
673 675
674 676 ui.write(_('finished migrating %d changelog revisions; change in size: '
675 677 '%s\n') % (crevcount, util.bytecount(cdstsize - csrcsize)))
676 678
677 679 ui.write(_('finished migrating %d total revisions; total change in store '
678 680 'size: %s\n') % (revcount, util.bytecount(dstsize - srcsize)))
679 681
680 682 def _filterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
681 683 """Determine whether to copy a store file during upgrade.
682 684
683 685 This function is called when migrating store files from ``srcrepo`` to
684 686 ``dstrepo`` as part of upgrading a repository.
685 687
686 688 Args:
687 689 srcrepo: repo we are copying from
688 690 dstrepo: repo we are copying to
689 691 requirements: set of requirements for ``dstrepo``
690 692 path: store file being examined
691 693 mode: the ``ST_MODE`` file type of ``path``
692 694 st: ``stat`` data structure for ``path``
693 695
694 696 Function should return ``True`` if the file is to be copied.
695 697 """
696 698 # Skip revlogs.
697 699 if path.endswith(('.i', '.d')):
698 700 return False
699 701 # Skip transaction related files.
700 702 if path.startswith('undo'):
701 703 return False
702 704 # Only copy regular files.
703 705 if mode != stat.S_IFREG:
704 706 return False
705 707 # Skip other skipped files.
706 708 if path in ('lock', 'fncache'):
707 709 return False
708 710
709 711 return True
710 712
711 713 def _finishdatamigration(ui, srcrepo, dstrepo, requirements):
712 714 """Hook point for extensions to perform additional actions during upgrade.
713 715
714 716 This function is called after revlogs and store files have been copied but
715 717 before the new store is swapped into the original location.
716 718 """
717 719
718 720 def _upgraderepo(ui, srcrepo, dstrepo, requirements, actions):
719 721 """Do the low-level work of upgrading a repository.
720 722
721 723 The upgrade is effectively performed as a copy between a source
722 724 repository and a temporary destination repository.
723 725
724 726 The source repository is unmodified for as long as possible so the
725 727 upgrade can abort at any time without causing loss of service for
726 728 readers and without corrupting the source repository.
727 729 """
728 730 assert srcrepo.currentwlock()
729 731 assert dstrepo.currentwlock()
730 732
731 733 ui.write(_('(it is safe to interrupt this process any time before '
732 734 'data migration completes)\n'))
733 735
734 736 if 're-delta-all' in actions:
735 737 deltareuse = revlog.revlog.DELTAREUSENEVER
736 738 elif 're-delta-parent' in actions:
737 739 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
738 740 elif 're-delta-multibase' in actions:
739 741 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
740 742 elif 're-delta-fulladd' in actions:
741 743 deltareuse = revlog.revlog.DELTAREUSEFULLADD
742 744 else:
743 745 deltareuse = revlog.revlog.DELTAREUSEALWAYS
744 746
745 747 with dstrepo.transaction('upgrade') as tr:
746 748 _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse,
747 749 're-delta-multibase' in actions)
748 750
749 751 # Now copy other files in the store directory.
750 752 # The sorted() makes execution deterministic.
751 753 for p, kind, st in sorted(srcrepo.store.vfs.readdir('', stat=True)):
752 754 if not _filterstorefile(srcrepo, dstrepo, requirements,
753 755 p, kind, st):
754 756 continue
755 757
756 758 srcrepo.ui.write(_('copying %s\n') % p)
757 759 src = srcrepo.store.rawvfs.join(p)
758 760 dst = dstrepo.store.rawvfs.join(p)
759 761 util.copyfile(src, dst, copystat=True)
760 762
761 763 _finishdatamigration(ui, srcrepo, dstrepo, requirements)
762 764
763 765 ui.write(_('data fully migrated to temporary repository\n'))
764 766
765 767 backuppath = pycompat.mkdtemp(prefix='upgradebackup.', dir=srcrepo.path)
766 768 backupvfs = vfsmod.vfs(backuppath)
767 769
768 770 # Make a backup of requires file first, as it is the first to be modified.
769 771 util.copyfile(srcrepo.vfs.join('requires'), backupvfs.join('requires'))
770 772
771 773 # We install an arbitrary requirement that clients must not support
772 774 # as a mechanism to lock out new clients during the data swap. This is
773 775 # better than allowing a client to continue while the repository is in
774 776 # an inconsistent state.
775 777 ui.write(_('marking source repository as being upgraded; clients will be '
776 778 'unable to read from repository\n'))
777 779 scmutil.writerequires(srcrepo.vfs,
778 780 srcrepo.requirements | {'upgradeinprogress'})
779 781
780 782 ui.write(_('starting in-place swap of repository data\n'))
781 783 ui.write(_('replaced files will be backed up at %s\n') %
782 784 backuppath)
783 785
784 786 # Now swap in the new store directory. Doing it as a rename should make
785 787 # the operation nearly instantaneous and atomic (at least in well-behaved
786 788 # environments).
787 789 ui.write(_('replacing store...\n'))
788 790 tstart = util.timer()
789 791 util.rename(srcrepo.spath, backupvfs.join('store'))
790 792 util.rename(dstrepo.spath, srcrepo.spath)
791 793 elapsed = util.timer() - tstart
792 794 ui.write(_('store replacement complete; repository was inconsistent for '
793 795 '%0.1fs\n') % elapsed)
794 796
795 797 # We first write the requirements file. Any new requirements will lock
796 798 # out legacy clients.
797 799 ui.write(_('finalizing requirements file and making repository readable '
798 800 'again\n'))
799 801 scmutil.writerequires(srcrepo.vfs, requirements)
800 802
801 803 # The lock file from the old store won't be removed because nothing has a
802 804 # reference to its new location. So clean it up manually. Alternatively, we
803 805 # could update srcrepo.svfs and other variables to point to the new
804 806 # location. This is simpler.
805 807 backupvfs.unlink('store/lock')
806 808
807 809 return backuppath
808 810
809 811 def upgraderepo(ui, repo, run=False, optimize=None, backup=True):
810 812 """Upgrade a repository in place."""
811 813 if optimize is None:
812 814 optimize = []
813 815 optimize = set(legacy_opts_map.get(o, o) for o in optimize)
814 816 repo = repo.unfiltered()
815 817
816 818 # Ensure the repository can be upgraded.
817 819 missingreqs = requiredsourcerequirements(repo) - repo.requirements
818 820 if missingreqs:
819 821 raise error.Abort(_('cannot upgrade repository; requirement '
820 822 'missing: %s') % _(', ').join(sorted(missingreqs)))
821 823
822 824 blockedreqs = blocksourcerequirements(repo) & repo.requirements
823 825 if blockedreqs:
824 826 raise error.Abort(_('cannot upgrade repository; unsupported source '
825 827 'requirement: %s') %
826 828 _(', ').join(sorted(blockedreqs)))
827 829
828 830 # FUTURE there is potentially a need to control the wanted requirements via
829 831 # command arguments or via an extension hook point.
830 832 newreqs = localrepo.newreporequirements(
831 833 repo.ui, localrepo.defaultcreateopts(repo.ui))
832 834 newreqs.update(preservedrequirements(repo))
833 835
834 836 noremovereqs = (repo.requirements - newreqs -
835 837 supportremovedrequirements(repo))
836 838 if noremovereqs:
837 839 raise error.Abort(_('cannot upgrade repository; requirement would be '
838 840 'removed: %s') % _(', ').join(sorted(noremovereqs)))
839 841
840 842 noaddreqs = (newreqs - repo.requirements -
841 843 allowednewrequirements(repo))
842 844 if noaddreqs:
843 845 raise error.Abort(_('cannot upgrade repository; do not support adding '
844 846 'requirement: %s') %
845 847 _(', ').join(sorted(noaddreqs)))
846 848
847 849 unsupportedreqs = newreqs - supporteddestrequirements(repo)
848 850 if unsupportedreqs:
849 851 raise error.Abort(_('cannot upgrade repository; do not support '
850 852 'destination requirement: %s') %
851 853 _(', ').join(sorted(unsupportedreqs)))
852 854
853 855 # Find and validate all improvements that can be made.
854 856 alloptimizations = findoptimizations(repo)
855 857
856 858 # Apply and Validate arguments.
857 859 optimizations = []
858 860 for o in alloptimizations:
859 861 if o.name in optimize:
860 862 optimizations.append(o)
861 863 optimize.discard(o.name)
862 864
863 865 if optimize: # anything left is unknown
864 866 raise error.Abort(_('unknown optimization action requested: %s') %
865 867 ', '.join(sorted(optimize)),
866 868 hint=_('run without arguments to see valid '
867 869 'optimizations'))
868 870
869 871 deficiencies = finddeficiencies(repo)
870 872 actions = determineactions(repo, deficiencies, repo.requirements, newreqs)
871 873 actions.extend(o for o in sorted(optimizations)
872 874 # determineactions could have added optimisation
873 875 if o not in actions)
874 876
875 877 def printrequirements():
876 878 ui.write(_('requirements\n'))
877 879 ui.write(_(' preserved: %s\n') %
878 880 _(', ').join(sorted(newreqs & repo.requirements)))
879 881
880 882 if repo.requirements - newreqs:
881 883 ui.write(_(' removed: %s\n') %
882 884 _(', ').join(sorted(repo.requirements - newreqs)))
883 885
884 886 if newreqs - repo.requirements:
885 887 ui.write(_(' added: %s\n') %
886 888 _(', ').join(sorted(newreqs - repo.requirements)))
887 889
888 890 ui.write('\n')
889 891
890 892 def printupgradeactions():
891 893 for a in actions:
892 894 ui.write('%s\n %s\n\n' % (a.name, a.upgrademessage))
893 895
894 896 if not run:
895 897 fromconfig = []
896 898 onlydefault = []
897 899
898 900 for d in deficiencies:
899 901 if d.fromconfig(repo):
900 902 fromconfig.append(d)
901 903 elif d.default:
902 904 onlydefault.append(d)
903 905
904 906 if fromconfig or onlydefault:
905 907
906 908 if fromconfig:
907 909 ui.write(_('repository lacks features recommended by '
908 910 'current config options:\n\n'))
909 911 for i in fromconfig:
910 912 ui.write('%s\n %s\n\n' % (i.name, i.description))
911 913
912 914 if onlydefault:
913 915 ui.write(_('repository lacks features used by the default '
914 916 'config options:\n\n'))
915 917 for i in onlydefault:
916 918 ui.write('%s\n %s\n\n' % (i.name, i.description))
917 919
918 920 ui.write('\n')
919 921 else:
920 922 ui.write(_('(no feature deficiencies found in existing '
921 923 'repository)\n'))
922 924
923 925 ui.write(_('performing an upgrade with "--run" will make the following '
924 926 'changes:\n\n'))
925 927
926 928 printrequirements()
927 929 printupgradeactions()
928 930
929 931 unusedoptimize = [i for i in alloptimizations if i not in actions]
930 932
931 933 if unusedoptimize:
932 934 ui.write(_('additional optimizations are available by specifying '
933 935 '"--optimize <name>":\n\n'))
934 936 for i in unusedoptimize:
935 937 ui.write(_('%s\n %s\n\n') % (i.name, i.description))
936 938 return
937 939
938 940 # Else we're in the run=true case.
939 941 ui.write(_('upgrade will perform the following actions:\n\n'))
940 942 printrequirements()
941 943 printupgradeactions()
942 944
943 945 upgradeactions = [a.name for a in actions]
944 946
945 947 ui.write(_('beginning upgrade...\n'))
946 948 with repo.wlock(), repo.lock():
947 949 ui.write(_('repository locked and read-only\n'))
948 950 # Our strategy for upgrading the repository is to create a new,
949 951 # temporary repository, write data to it, then do a swap of the
950 952 # data. There are less heavyweight ways to do this, but it is easier
951 953 # to create a new repo object than to instantiate all the components
952 954 # (like the store) separately.
953 955 tmppath = pycompat.mkdtemp(prefix='upgrade.', dir=repo.path)
954 956 backuppath = None
955 957 try:
956 958 ui.write(_('creating temporary repository to stage migrated '
957 959 'data: %s\n') % tmppath)
958 960
959 961 # clone ui without using ui.copy because repo.ui is protected
960 962 repoui = repo.ui.__class__(repo.ui)
961 963 dstrepo = hg.repository(repoui, path=tmppath, create=True)
962 964
963 965 with dstrepo.wlock(), dstrepo.lock():
964 966 backuppath = _upgraderepo(ui, repo, dstrepo, newreqs,
965 967 upgradeactions)
966 968 if not (backup or backuppath is None):
967 969 ui.write(_('removing old repository content%s\n') % backuppath)
968 970 repo.vfs.rmtree(backuppath, forcibly=True)
969 971 backuppath = None
970 972
971 973 finally:
972 974 ui.write(_('removing temporary repository %s\n') % tmppath)
973 975 repo.vfs.rmtree(tmppath, forcibly=True)
974 976
975 977 if backuppath:
976 978 ui.warn(_('copy of old repository backed up at %s\n') %
977 979 backuppath)
978 980 ui.warn(_('the old repository will not be deleted; remove '
979 981 'it to free up disk space once the upgraded '
980 982 'repository is verified\n'))
General Comments 0
You need to be logged in to leave comments. Login now