##// END OF EJS Templates
repair: use context manager for lock management...
Matt Harbison -
r31626:0febf8e4 default
parent child Browse files
Show More
@@ -1,1102 +1,1097 b''
1 1 # repair.py - functions for repository repair for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 # Copyright 2007 Matt Mackall
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import hashlib
13 13 import stat
14 14 import tempfile
15 15
16 16 from .i18n import _
17 17 from .node import short
18 18 from . import (
19 19 bundle2,
20 20 changegroup,
21 21 changelog,
22 22 error,
23 23 exchange,
24 24 manifest,
25 25 obsolete,
26 26 revlog,
27 27 scmutil,
28 28 util,
29 29 vfs as vfsmod,
30 30 )
31 31
32 32 def _bundle(repo, bases, heads, node, suffix, compress=True):
33 33 """create a bundle with the specified revisions as a backup"""
34 34 cgversion = changegroup.safeversion(repo)
35 35
36 36 cg = changegroup.changegroupsubset(repo, bases, heads, 'strip',
37 37 version=cgversion)
38 38 backupdir = "strip-backup"
39 39 vfs = repo.vfs
40 40 if not vfs.isdir(backupdir):
41 41 vfs.mkdir(backupdir)
42 42
43 43 # Include a hash of all the nodes in the filename for uniqueness
44 44 allcommits = repo.set('%ln::%ln', bases, heads)
45 45 allhashes = sorted(c.hex() for c in allcommits)
46 46 totalhash = hashlib.sha1(''.join(allhashes)).hexdigest()
47 47 name = "%s/%s-%s-%s.hg" % (backupdir, short(node), totalhash[:8], suffix)
48 48
49 49 comp = None
50 50 if cgversion != '01':
51 51 bundletype = "HG20"
52 52 if compress:
53 53 comp = 'BZ'
54 54 elif compress:
55 55 bundletype = "HG10BZ"
56 56 else:
57 57 bundletype = "HG10UN"
58 58 return bundle2.writebundle(repo.ui, cg, name, bundletype, vfs,
59 59 compression=comp)
60 60
61 61 def _collectfiles(repo, striprev):
62 62 """find out the filelogs affected by the strip"""
63 63 files = set()
64 64
65 65 for x in xrange(striprev, len(repo)):
66 66 files.update(repo[x].files())
67 67
68 68 return sorted(files)
69 69
70 70 def _collectbrokencsets(repo, files, striprev):
71 71 """return the changesets which will be broken by the truncation"""
72 72 s = set()
73 73 def collectone(revlog):
74 74 _, brokenset = revlog.getstrippoint(striprev)
75 75 s.update([revlog.linkrev(r) for r in brokenset])
76 76
77 77 collectone(repo.manifestlog._revlog)
78 78 for fname in files:
79 79 collectone(repo.file(fname))
80 80
81 81 return s
82 82
83 83 def strip(ui, repo, nodelist, backup=True, topic='backup'):
84 84 # This function operates within a transaction of its own, but does
85 85 # not take any lock on the repo.
86 86 # Simple way to maintain backwards compatibility for this
87 87 # argument.
88 88 if backup in ['none', 'strip']:
89 89 backup = False
90 90
91 91 repo = repo.unfiltered()
92 92 repo.destroying()
93 93
94 94 cl = repo.changelog
95 95 # TODO handle undo of merge sets
96 96 if isinstance(nodelist, str):
97 97 nodelist = [nodelist]
98 98 striplist = [cl.rev(node) for node in nodelist]
99 99 striprev = min(striplist)
100 100
101 101 files = _collectfiles(repo, striprev)
102 102 saverevs = _collectbrokencsets(repo, files, striprev)
103 103
104 104 # Some revisions with rev > striprev may not be descendants of striprev.
105 105 # We have to find these revisions and put them in a bundle, so that
106 106 # we can restore them after the truncations.
107 107 # To create the bundle we use repo.changegroupsubset which requires
108 108 # the list of heads and bases of the set of interesting revisions.
109 109 # (head = revision in the set that has no descendant in the set;
110 110 # base = revision in the set that has no ancestor in the set)
111 111 tostrip = set(striplist)
112 112 saveheads = set(saverevs)
113 113 for r in cl.revs(start=striprev + 1):
114 114 if any(p in tostrip for p in cl.parentrevs(r)):
115 115 tostrip.add(r)
116 116
117 117 if r not in tostrip:
118 118 saverevs.add(r)
119 119 saveheads.difference_update(cl.parentrevs(r))
120 120 saveheads.add(r)
121 121 saveheads = [cl.node(r) for r in saveheads]
122 122
123 123 # compute base nodes
124 124 if saverevs:
125 125 descendants = set(cl.descendants(saverevs))
126 126 saverevs.difference_update(descendants)
127 127 savebases = [cl.node(r) for r in saverevs]
128 128 stripbases = [cl.node(r) for r in tostrip]
129 129
130 130 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)), but
131 131 # is much faster
132 132 newbmtarget = repo.revs('max(parents(%ld) - (%ld))', tostrip, tostrip)
133 133 if newbmtarget:
134 134 newbmtarget = repo[newbmtarget.first()].node()
135 135 else:
136 136 newbmtarget = '.'
137 137
138 138 bm = repo._bookmarks
139 139 updatebm = []
140 140 for m in bm:
141 141 rev = repo[bm[m]].rev()
142 142 if rev in tostrip:
143 143 updatebm.append(m)
144 144
145 145 # create a changegroup for all the branches we need to keep
146 146 backupfile = None
147 147 vfs = repo.vfs
148 148 node = nodelist[-1]
149 149 if backup:
150 150 backupfile = _bundle(repo, stripbases, cl.heads(), node, topic)
151 151 repo.ui.status(_("saved backup bundle to %s\n") %
152 152 vfs.join(backupfile))
153 153 repo.ui.log("backupbundle", "saved backup bundle to %s\n",
154 154 vfs.join(backupfile))
155 155 tmpbundlefile = None
156 156 if saveheads:
157 157 # do not compress temporary bundle if we remove it from disk later
158 158 tmpbundlefile = _bundle(repo, savebases, saveheads, node, 'temp',
159 159 compress=False)
160 160
161 161 mfst = repo.manifestlog._revlog
162 162
163 163 curtr = repo.currenttransaction()
164 164 if curtr is not None:
165 165 del curtr # avoid carrying reference to transaction for nothing
166 166 msg = _('programming error: cannot strip from inside a transaction')
167 167 raise error.Abort(msg, hint=_('contact your extension maintainer'))
168 168
169 169 try:
170 170 with repo.transaction("strip") as tr:
171 171 offset = len(tr.entries)
172 172
173 173 tr.startgroup()
174 174 cl.strip(striprev, tr)
175 175 mfst.strip(striprev, tr)
176 176 if 'treemanifest' in repo.requirements: # safe but unnecessary
177 177 # otherwise
178 178 for unencoded, encoded, size in repo.store.datafiles():
179 179 if (unencoded.startswith('meta/') and
180 180 unencoded.endswith('00manifest.i')):
181 181 dir = unencoded[5:-12]
182 182 repo.manifestlog._revlog.dirlog(dir).strip(striprev, tr)
183 183 for fn in files:
184 184 repo.file(fn).strip(striprev, tr)
185 185 tr.endgroup()
186 186
187 187 for i in xrange(offset, len(tr.entries)):
188 188 file, troffset, ignore = tr.entries[i]
189 189 with repo.svfs(file, 'a', checkambig=True) as fp:
190 190 fp.truncate(troffset)
191 191 if troffset == 0:
192 192 repo.store.markremoved(file)
193 193
194 194 if tmpbundlefile:
195 195 ui.note(_("adding branch\n"))
196 196 f = vfs.open(tmpbundlefile, "rb")
197 197 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
198 198 if not repo.ui.verbose:
199 199 # silence internal shuffling chatter
200 200 repo.ui.pushbuffer()
201 201 if isinstance(gen, bundle2.unbundle20):
202 202 with repo.transaction('strip') as tr:
203 203 tr.hookargs = {'source': 'strip',
204 204 'url': 'bundle:' + vfs.join(tmpbundlefile)}
205 205 bundle2.applybundle(repo, gen, tr, source='strip',
206 206 url='bundle:' + vfs.join(tmpbundlefile))
207 207 else:
208 208 gen.apply(repo, 'strip', 'bundle:' + vfs.join(tmpbundlefile),
209 209 True)
210 210 if not repo.ui.verbose:
211 211 repo.ui.popbuffer()
212 212 f.close()
213 213 repo._phasecache.invalidate()
214 214
215 215 for m in updatebm:
216 216 bm[m] = repo[newbmtarget].node()
217 lock = tr = None
218 try:
219 lock = repo.lock()
220 tr = repo.transaction('repair')
221 bm.recordchange(tr)
222 tr.close()
223 finally:
224 tr.release()
225 lock.release()
217
218 with repo.lock():
219 with repo.transaction('repair') as tr:
220 bm.recordchange(tr)
226 221
227 222 # remove undo files
228 223 for undovfs, undofile in repo.undofiles():
229 224 try:
230 225 undovfs.unlink(undofile)
231 226 except OSError as e:
232 227 if e.errno != errno.ENOENT:
233 228 ui.warn(_('error removing %s: %s\n') %
234 229 (undovfs.join(undofile), str(e)))
235 230
236 231 except: # re-raises
237 232 if backupfile:
238 233 ui.warn(_("strip failed, backup bundle stored in '%s'\n")
239 234 % vfs.join(backupfile))
240 235 if tmpbundlefile:
241 236 ui.warn(_("strip failed, unrecovered changes stored in '%s'\n")
242 237 % vfs.join(tmpbundlefile))
243 238 ui.warn(_("(fix the problem, then recover the changesets with "
244 239 "\"hg unbundle '%s'\")\n") % vfs.join(tmpbundlefile))
245 240 raise
246 241 else:
247 242 if tmpbundlefile:
248 243 # Remove temporary bundle only if there were no exceptions
249 244 vfs.unlink(tmpbundlefile)
250 245
251 246 repo.destroyed()
252 247 # return the backup file path (or None if 'backup' was False) so
253 248 # extensions can use it
254 249 return backupfile
255 250
256 251 def rebuildfncache(ui, repo):
257 252 """Rebuilds the fncache file from repo history.
258 253
259 254 Missing entries will be added. Extra entries will be removed.
260 255 """
261 256 repo = repo.unfiltered()
262 257
263 258 if 'fncache' not in repo.requirements:
264 259 ui.warn(_('(not rebuilding fncache because repository does not '
265 260 'support fncache)\n'))
266 261 return
267 262
268 263 with repo.lock():
269 264 fnc = repo.store.fncache
270 265 # Trigger load of fncache.
271 266 if 'irrelevant' in fnc:
272 267 pass
273 268
274 269 oldentries = set(fnc.entries)
275 270 newentries = set()
276 271 seenfiles = set()
277 272
278 273 repolen = len(repo)
279 274 for rev in repo:
280 275 ui.progress(_('rebuilding'), rev, total=repolen,
281 276 unit=_('changesets'))
282 277
283 278 ctx = repo[rev]
284 279 for f in ctx.files():
285 280 # This is to minimize I/O.
286 281 if f in seenfiles:
287 282 continue
288 283 seenfiles.add(f)
289 284
290 285 i = 'data/%s.i' % f
291 286 d = 'data/%s.d' % f
292 287
293 288 if repo.store._exists(i):
294 289 newentries.add(i)
295 290 if repo.store._exists(d):
296 291 newentries.add(d)
297 292
298 293 ui.progress(_('rebuilding'), None)
299 294
300 295 if 'treemanifest' in repo.requirements: # safe but unnecessary otherwise
301 296 for dir in util.dirs(seenfiles):
302 297 i = 'meta/%s/00manifest.i' % dir
303 298 d = 'meta/%s/00manifest.d' % dir
304 299
305 300 if repo.store._exists(i):
306 301 newentries.add(i)
307 302 if repo.store._exists(d):
308 303 newentries.add(d)
309 304
310 305 addcount = len(newentries - oldentries)
311 306 removecount = len(oldentries - newentries)
312 307 for p in sorted(oldentries - newentries):
313 308 ui.write(_('removing %s\n') % p)
314 309 for p in sorted(newentries - oldentries):
315 310 ui.write(_('adding %s\n') % p)
316 311
317 312 if addcount or removecount:
318 313 ui.write(_('%d items added, %d removed from fncache\n') %
319 314 (addcount, removecount))
320 315 fnc.entries = newentries
321 316 fnc._dirty = True
322 317
323 318 with repo.transaction('fncache') as tr:
324 319 fnc.write(tr)
325 320 else:
326 321 ui.write(_('fncache already up to date\n'))
327 322
328 323 def stripbmrevset(repo, mark):
329 324 """
330 325 The revset to strip when strip is called with -B mark
331 326
332 327 Needs to live here so extensions can use it and wrap it even when strip is
333 328 not enabled or not present on a box.
334 329 """
335 330 return repo.revs("ancestors(bookmark(%s)) - "
336 331 "ancestors(head() and not bookmark(%s)) - "
337 332 "ancestors(bookmark() and not bookmark(%s))",
338 333 mark, mark, mark)
339 334
340 335 def deleteobsmarkers(obsstore, indices):
341 336 """Delete some obsmarkers from obsstore and return how many were deleted
342 337
343 338 'indices' is a list of ints which are the indices
344 339 of the markers to be deleted.
345 340
346 341 Every invocation of this function completely rewrites the obsstore file,
347 342 skipping the markers we want to be removed. The new temporary file is
348 343 created, remaining markers are written there and on .close() this file
349 344 gets atomically renamed to obsstore, thus guaranteeing consistency."""
350 345 if not indices:
351 346 # we don't want to rewrite the obsstore with the same content
352 347 return
353 348
354 349 left = []
355 350 current = obsstore._all
356 351 n = 0
357 352 for i, m in enumerate(current):
358 353 if i in indices:
359 354 n += 1
360 355 continue
361 356 left.append(m)
362 357
363 358 newobsstorefile = obsstore.svfs('obsstore', 'w', atomictemp=True)
364 359 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
365 360 newobsstorefile.write(bytes)
366 361 newobsstorefile.close()
367 362 return n
368 363
369 364 def upgraderequiredsourcerequirements(repo):
370 365 """Obtain requirements required to be present to upgrade a repo.
371 366
372 367 An upgrade will not be allowed if the repository doesn't have the
373 368 requirements returned by this function.
374 369 """
375 370 return set([
376 371 # Introduced in Mercurial 0.9.2.
377 372 'revlogv1',
378 373 # Introduced in Mercurial 0.9.2.
379 374 'store',
380 375 ])
381 376
382 377 def upgradeblocksourcerequirements(repo):
383 378 """Obtain requirements that will prevent an upgrade from occurring.
384 379
385 380 An upgrade cannot be performed if the source repository contains a
386 381 requirements in the returned set.
387 382 """
388 383 return set([
389 384 # The upgrade code does not yet support these experimental features.
390 385 # This is an artificial limitation.
391 386 'manifestv2',
392 387 'treemanifest',
393 388 # This was a precursor to generaldelta and was never enabled by default.
394 389 # It should (hopefully) not exist in the wild.
395 390 'parentdelta',
396 391 # Upgrade should operate on the actual store, not the shared link.
397 392 'shared',
398 393 ])
399 394
400 395 def upgradesupportremovedrequirements(repo):
401 396 """Obtain requirements that can be removed during an upgrade.
402 397
403 398 If an upgrade were to create a repository that dropped a requirement,
404 399 the dropped requirement must appear in the returned set for the upgrade
405 400 to be allowed.
406 401 """
407 402 return set()
408 403
409 404 def upgradesupporteddestrequirements(repo):
410 405 """Obtain requirements that upgrade supports in the destination.
411 406
412 407 If the result of the upgrade would create requirements not in this set,
413 408 the upgrade is disallowed.
414 409
415 410 Extensions should monkeypatch this to add their custom requirements.
416 411 """
417 412 return set([
418 413 'dotencode',
419 414 'fncache',
420 415 'generaldelta',
421 416 'revlogv1',
422 417 'store',
423 418 ])
424 419
425 420 def upgradeallowednewrequirements(repo):
426 421 """Obtain requirements that can be added to a repository during upgrade.
427 422
428 423 This is used to disallow proposed requirements from being added when
429 424 they weren't present before.
430 425
431 426 We use a list of allowed requirement additions instead of a list of known
432 427 bad additions because the whitelist approach is safer and will prevent
433 428 future, unknown requirements from accidentally being added.
434 429 """
435 430 return set([
436 431 'dotencode',
437 432 'fncache',
438 433 'generaldelta',
439 434 ])
440 435
441 436 deficiency = 'deficiency'
442 437 optimisation = 'optimization'
443 438
444 439 class upgradeimprovement(object):
445 440 """Represents an improvement that can be made as part of an upgrade.
446 441
447 442 The following attributes are defined on each instance:
448 443
449 444 name
450 445 Machine-readable string uniquely identifying this improvement. It
451 446 will be mapped to an action later in the upgrade process.
452 447
453 448 type
454 449 Either ``deficiency`` or ``optimisation``. A deficiency is an obvious
455 450 problem. An optimization is an action (sometimes optional) that
456 451 can be taken to further improve the state of the repository.
457 452
458 453 description
459 454 Message intended for humans explaining the improvement in more detail,
460 455 including the implications of it. For ``deficiency`` types, should be
461 456 worded in the present tense. For ``optimisation`` types, should be
462 457 worded in the future tense.
463 458
464 459 upgrademessage
465 460 Message intended for humans explaining what an upgrade addressing this
466 461 issue will do. Should be worded in the future tense.
467 462
468 463 fromdefault (``deficiency`` types only)
469 464 Boolean indicating whether the current (deficient) state deviates
470 465 from Mercurial's default configuration.
471 466
472 467 fromconfig (``deficiency`` types only)
473 468 Boolean indicating whether the current (deficient) state deviates
474 469 from the current Mercurial configuration.
475 470 """
476 471 def __init__(self, name, type, description, upgrademessage, **kwargs):
477 472 self.name = name
478 473 self.type = type
479 474 self.description = description
480 475 self.upgrademessage = upgrademessage
481 476
482 477 for k, v in kwargs.items():
483 478 setattr(self, k, v)
484 479
485 480 def upgradefindimprovements(repo):
486 481 """Determine improvements that can be made to the repo during upgrade.
487 482
488 483 Returns a list of ``upgradeimprovement`` describing repository deficiencies
489 484 and optimizations.
490 485 """
491 486 # Avoid cycle: cmdutil -> repair -> localrepo -> cmdutil
492 487 from . import localrepo
493 488
494 489 newreporeqs = localrepo.newreporequirements(repo)
495 490
496 491 improvements = []
497 492
498 493 # We could detect lack of revlogv1 and store here, but they were added
499 494 # in 0.9.2 and we don't support upgrading repos without these
500 495 # requirements, so let's not bother.
501 496
502 497 if 'fncache' not in repo.requirements:
503 498 improvements.append(upgradeimprovement(
504 499 name='fncache',
505 500 type=deficiency,
506 501 description=_('long and reserved filenames may not work correctly; '
507 502 'repository performance is sub-optimal'),
508 503 upgrademessage=_('repository will be more resilient to storing '
509 504 'certain paths and performance of certain '
510 505 'operations should be improved'),
511 506 fromdefault=True,
512 507 fromconfig='fncache' in newreporeqs))
513 508
514 509 if 'dotencode' not in repo.requirements:
515 510 improvements.append(upgradeimprovement(
516 511 name='dotencode',
517 512 type=deficiency,
518 513 description=_('storage of filenames beginning with a period or '
519 514 'space may not work correctly'),
520 515 upgrademessage=_('repository will be better able to store files '
521 516 'beginning with a space or period'),
522 517 fromdefault=True,
523 518 fromconfig='dotencode' in newreporeqs))
524 519
525 520 if 'generaldelta' not in repo.requirements:
526 521 improvements.append(upgradeimprovement(
527 522 name='generaldelta',
528 523 type=deficiency,
529 524 description=_('deltas within internal storage are unable to '
530 525 'choose optimal revisions; repository is larger and '
531 526 'slower than it could be; interaction with other '
532 527 'repositories may require extra network and CPU '
533 528 'resources, making "hg push" and "hg pull" slower'),
534 529 upgrademessage=_('repository storage will be able to create '
535 530 'optimal deltas; new repository data will be '
536 531 'smaller and read times should decrease; '
537 532 'interacting with other repositories using this '
538 533 'storage model should require less network and '
539 534 'CPU resources, making "hg push" and "hg pull" '
540 535 'faster'),
541 536 fromdefault=True,
542 537 fromconfig='generaldelta' in newreporeqs))
543 538
544 539 # Mercurial 4.0 changed changelogs to not use delta chains. Search for
545 540 # changelogs with deltas.
546 541 cl = repo.changelog
547 542 for rev in cl:
548 543 chainbase = cl.chainbase(rev)
549 544 if chainbase != rev:
550 545 improvements.append(upgradeimprovement(
551 546 name='removecldeltachain',
552 547 type=deficiency,
553 548 description=_('changelog storage is using deltas instead of '
554 549 'raw entries; changelog reading and any '
555 550 'operation relying on changelog data are slower '
556 551 'than they could be'),
557 552 upgrademessage=_('changelog storage will be reformated to '
558 553 'store raw entries; changelog reading will be '
559 554 'faster; changelog size may be reduced'),
560 555 fromdefault=True,
561 556 fromconfig=True))
562 557 break
563 558
564 559 # Now for the optimizations.
565 560
566 561 # These are unconditionally added. There is logic later that figures out
567 562 # which ones to apply.
568 563
569 564 improvements.append(upgradeimprovement(
570 565 name='redeltaparent',
571 566 type=optimisation,
572 567 description=_('deltas within internal storage will be recalculated to '
573 568 'choose an optimal base revision where this was not '
574 569 'already done; the size of the repository may shrink and '
575 570 'various operations may become faster; the first time '
576 571 'this optimization is performed could slow down upgrade '
577 572 'execution considerably; subsequent invocations should '
578 573 'not run noticeably slower'),
579 574 upgrademessage=_('deltas within internal storage will choose a new '
580 575 'base revision if needed')))
581 576
582 577 improvements.append(upgradeimprovement(
583 578 name='redeltamultibase',
584 579 type=optimisation,
585 580 description=_('deltas within internal storage will be recalculated '
586 581 'against multiple base revision and the smallest '
587 582 'difference will be used; the size of the repository may '
588 583 'shrink significantly when there are many merges; this '
589 584 'optimization will slow down execution in proportion to '
590 585 'the number of merges in the repository and the amount '
591 586 'of files in the repository; this slow down should not '
592 587 'be significant unless there are tens of thousands of '
593 588 'files and thousands of merges'),
594 589 upgrademessage=_('deltas within internal storage will choose an '
595 590 'optimal delta by computing deltas against multiple '
596 591 'parents; may slow down execution time '
597 592 'significantly')))
598 593
599 594 improvements.append(upgradeimprovement(
600 595 name='redeltaall',
601 596 type=optimisation,
602 597 description=_('deltas within internal storage will always be '
603 598 'recalculated without reusing prior deltas; this will '
604 599 'likely make execution run several times slower; this '
605 600 'optimization is typically not needed'),
606 601 upgrademessage=_('deltas within internal storage will be fully '
607 602 'recomputed; this will likely drastically slow down '
608 603 'execution time')))
609 604
610 605 return improvements
611 606
612 607 def upgradedetermineactions(repo, improvements, sourcereqs, destreqs,
613 608 optimize):
614 609 """Determine upgrade actions that will be performed.
615 610
616 611 Given a list of improvements as returned by ``upgradefindimprovements``,
617 612 determine the list of upgrade actions that will be performed.
618 613
619 614 The role of this function is to filter improvements if needed, apply
620 615 recommended optimizations from the improvements list that make sense,
621 616 etc.
622 617
623 618 Returns a list of action names.
624 619 """
625 620 newactions = []
626 621
627 622 knownreqs = upgradesupporteddestrequirements(repo)
628 623
629 624 for i in improvements:
630 625 name = i.name
631 626
632 627 # If the action is a requirement that doesn't show up in the
633 628 # destination requirements, prune the action.
634 629 if name in knownreqs and name not in destreqs:
635 630 continue
636 631
637 632 if i.type == deficiency:
638 633 newactions.append(name)
639 634
640 635 newactions.extend(o for o in sorted(optimize) if o not in newactions)
641 636
642 637 # FUTURE consider adding some optimizations here for certain transitions.
643 638 # e.g. adding generaldelta could schedule parent redeltas.
644 639
645 640 return newactions
646 641
647 642 def _revlogfrompath(repo, path):
648 643 """Obtain a revlog from a repo path.
649 644
650 645 An instance of the appropriate class is returned.
651 646 """
652 647 if path == '00changelog.i':
653 648 return changelog.changelog(repo.svfs)
654 649 elif path.endswith('00manifest.i'):
655 650 mandir = path[:-len('00manifest.i')]
656 651 return manifest.manifestrevlog(repo.svfs, dir=mandir)
657 652 else:
658 653 # Filelogs don't do anything special with settings. So we can use a
659 654 # vanilla revlog.
660 655 return revlog.revlog(repo.svfs, path)
661 656
662 657 def _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse, aggressivemergedeltas):
663 658 """Copy revlogs between 2 repos."""
664 659 revcount = 0
665 660 srcsize = 0
666 661 srcrawsize = 0
667 662 dstsize = 0
668 663 fcount = 0
669 664 frevcount = 0
670 665 fsrcsize = 0
671 666 frawsize = 0
672 667 fdstsize = 0
673 668 mcount = 0
674 669 mrevcount = 0
675 670 msrcsize = 0
676 671 mrawsize = 0
677 672 mdstsize = 0
678 673 crevcount = 0
679 674 csrcsize = 0
680 675 crawsize = 0
681 676 cdstsize = 0
682 677
683 678 # Perform a pass to collect metadata. This validates we can open all
684 679 # source files and allows a unified progress bar to be displayed.
685 680 for unencoded, encoded, size in srcrepo.store.walk():
686 681 if unencoded.endswith('.d'):
687 682 continue
688 683
689 684 rl = _revlogfrompath(srcrepo, unencoded)
690 685 revcount += len(rl)
691 686
692 687 datasize = 0
693 688 rawsize = 0
694 689 idx = rl.index
695 690 for rev in rl:
696 691 e = idx[rev]
697 692 datasize += e[1]
698 693 rawsize += e[2]
699 694
700 695 srcsize += datasize
701 696 srcrawsize += rawsize
702 697
703 698 # This is for the separate progress bars.
704 699 if isinstance(rl, changelog.changelog):
705 700 crevcount += len(rl)
706 701 csrcsize += datasize
707 702 crawsize += rawsize
708 703 elif isinstance(rl, manifest.manifestrevlog):
709 704 mcount += 1
710 705 mrevcount += len(rl)
711 706 msrcsize += datasize
712 707 mrawsize += rawsize
713 708 elif isinstance(rl, revlog.revlog):
714 709 fcount += 1
715 710 frevcount += len(rl)
716 711 fsrcsize += datasize
717 712 frawsize += rawsize
718 713
719 714 if not revcount:
720 715 return
721 716
722 717 ui.write(_('migrating %d total revisions (%d in filelogs, %d in manifests, '
723 718 '%d in changelog)\n') %
724 719 (revcount, frevcount, mrevcount, crevcount))
725 720 ui.write(_('migrating %s in store; %s tracked data\n') % (
726 721 (util.bytecount(srcsize), util.bytecount(srcrawsize))))
727 722
728 723 # Used to keep track of progress.
729 724 progress = []
730 725 def oncopiedrevision(rl, rev, node):
731 726 progress[1] += 1
732 727 srcrepo.ui.progress(progress[0], progress[1], total=progress[2])
733 728
734 729 # Do the actual copying.
735 730 # FUTURE this operation can be farmed off to worker processes.
736 731 seen = set()
737 732 for unencoded, encoded, size in srcrepo.store.walk():
738 733 if unencoded.endswith('.d'):
739 734 continue
740 735
741 736 oldrl = _revlogfrompath(srcrepo, unencoded)
742 737 newrl = _revlogfrompath(dstrepo, unencoded)
743 738
744 739 if isinstance(oldrl, changelog.changelog) and 'c' not in seen:
745 740 ui.write(_('finished migrating %d manifest revisions across %d '
746 741 'manifests; change in size: %s\n') %
747 742 (mrevcount, mcount, util.bytecount(mdstsize - msrcsize)))
748 743
749 744 ui.write(_('migrating changelog containing %d revisions '
750 745 '(%s in store; %s tracked data)\n') %
751 746 (crevcount, util.bytecount(csrcsize),
752 747 util.bytecount(crawsize)))
753 748 seen.add('c')
754 749 progress[:] = [_('changelog revisions'), 0, crevcount]
755 750 elif isinstance(oldrl, manifest.manifestrevlog) and 'm' not in seen:
756 751 ui.write(_('finished migrating %d filelog revisions across %d '
757 752 'filelogs; change in size: %s\n') %
758 753 (frevcount, fcount, util.bytecount(fdstsize - fsrcsize)))
759 754
760 755 ui.write(_('migrating %d manifests containing %d revisions '
761 756 '(%s in store; %s tracked data)\n') %
762 757 (mcount, mrevcount, util.bytecount(msrcsize),
763 758 util.bytecount(mrawsize)))
764 759 seen.add('m')
765 760 progress[:] = [_('manifest revisions'), 0, mrevcount]
766 761 elif 'f' not in seen:
767 762 ui.write(_('migrating %d filelogs containing %d revisions '
768 763 '(%s in store; %s tracked data)\n') %
769 764 (fcount, frevcount, util.bytecount(fsrcsize),
770 765 util.bytecount(frawsize)))
771 766 seen.add('f')
772 767 progress[:] = [_('file revisions'), 0, frevcount]
773 768
774 769 ui.progress(progress[0], progress[1], total=progress[2])
775 770
776 771 ui.note(_('cloning %d revisions from %s\n') % (len(oldrl), unencoded))
777 772 oldrl.clone(tr, newrl, addrevisioncb=oncopiedrevision,
778 773 deltareuse=deltareuse,
779 774 aggressivemergedeltas=aggressivemergedeltas)
780 775
781 776 datasize = 0
782 777 idx = newrl.index
783 778 for rev in newrl:
784 779 datasize += idx[rev][1]
785 780
786 781 dstsize += datasize
787 782
788 783 if isinstance(newrl, changelog.changelog):
789 784 cdstsize += datasize
790 785 elif isinstance(newrl, manifest.manifestrevlog):
791 786 mdstsize += datasize
792 787 else:
793 788 fdstsize += datasize
794 789
795 790 ui.progress(progress[0], None)
796 791
797 792 ui.write(_('finished migrating %d changelog revisions; change in size: '
798 793 '%s\n') % (crevcount, util.bytecount(cdstsize - csrcsize)))
799 794
800 795 ui.write(_('finished migrating %d total revisions; total change in store '
801 796 'size: %s\n') % (revcount, util.bytecount(dstsize - srcsize)))
802 797
803 798 def _upgradefilterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
804 799 """Determine whether to copy a store file during upgrade.
805 800
806 801 This function is called when migrating store files from ``srcrepo`` to
807 802 ``dstrepo`` as part of upgrading a repository.
808 803
809 804 Args:
810 805 srcrepo: repo we are copying from
811 806 dstrepo: repo we are copying to
812 807 requirements: set of requirements for ``dstrepo``
813 808 path: store file being examined
814 809 mode: the ``ST_MODE`` file type of ``path``
815 810 st: ``stat`` data structure for ``path``
816 811
817 812 Function should return ``True`` if the file is to be copied.
818 813 """
819 814 # Skip revlogs.
820 815 if path.endswith(('.i', '.d')):
821 816 return False
822 817 # Skip transaction related files.
823 818 if path.startswith('undo'):
824 819 return False
825 820 # Only copy regular files.
826 821 if mode != stat.S_IFREG:
827 822 return False
828 823 # Skip other skipped files.
829 824 if path in ('lock', 'fncache'):
830 825 return False
831 826
832 827 return True
833 828
834 829 def _upgradefinishdatamigration(ui, srcrepo, dstrepo, requirements):
835 830 """Hook point for extensions to perform additional actions during upgrade.
836 831
837 832 This function is called after revlogs and store files have been copied but
838 833 before the new store is swapped into the original location.
839 834 """
840 835
841 836 def _upgraderepo(ui, srcrepo, dstrepo, requirements, actions):
842 837 """Do the low-level work of upgrading a repository.
843 838
844 839 The upgrade is effectively performed as a copy between a source
845 840 repository and a temporary destination repository.
846 841
847 842 The source repository is unmodified for as long as possible so the
848 843 upgrade can abort at any time without causing loss of service for
849 844 readers and without corrupting the source repository.
850 845 """
851 846 assert srcrepo.currentwlock()
852 847 assert dstrepo.currentwlock()
853 848
854 849 ui.write(_('(it is safe to interrupt this process any time before '
855 850 'data migration completes)\n'))
856 851
857 852 if 'redeltaall' in actions:
858 853 deltareuse = revlog.revlog.DELTAREUSENEVER
859 854 elif 'redeltaparent' in actions:
860 855 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
861 856 elif 'redeltamultibase' in actions:
862 857 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
863 858 else:
864 859 deltareuse = revlog.revlog.DELTAREUSEALWAYS
865 860
866 861 with dstrepo.transaction('upgrade') as tr:
867 862 _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse,
868 863 'redeltamultibase' in actions)
869 864
870 865 # Now copy other files in the store directory.
871 866 for p, kind, st in srcrepo.store.vfs.readdir('', stat=True):
872 867 if not _upgradefilterstorefile(srcrepo, dstrepo, requirements,
873 868 p, kind, st):
874 869 continue
875 870
876 871 srcrepo.ui.write(_('copying %s\n') % p)
877 872 src = srcrepo.store.vfs.join(p)
878 873 dst = dstrepo.store.vfs.join(p)
879 874 util.copyfile(src, dst, copystat=True)
880 875
881 876 _upgradefinishdatamigration(ui, srcrepo, dstrepo, requirements)
882 877
883 878 ui.write(_('data fully migrated to temporary repository\n'))
884 879
885 880 backuppath = tempfile.mkdtemp(prefix='upgradebackup.', dir=srcrepo.path)
886 881 backupvfs = vfsmod.vfs(backuppath)
887 882
888 883 # Make a backup of requires file first, as it is the first to be modified.
889 884 util.copyfile(srcrepo.vfs.join('requires'), backupvfs.join('requires'))
890 885
891 886 # We install an arbitrary requirement that clients must not support
892 887 # as a mechanism to lock out new clients during the data swap. This is
893 888 # better than allowing a client to continue while the repository is in
894 889 # an inconsistent state.
895 890 ui.write(_('marking source repository as being upgraded; clients will be '
896 891 'unable to read from repository\n'))
897 892 scmutil.writerequires(srcrepo.vfs,
898 893 srcrepo.requirements | set(['upgradeinprogress']))
899 894
900 895 ui.write(_('starting in-place swap of repository data\n'))
901 896 ui.write(_('replaced files will be backed up at %s\n') %
902 897 backuppath)
903 898
904 899 # Now swap in the new store directory. Doing it as a rename should make
905 900 # the operation nearly instantaneous and atomic (at least in well-behaved
906 901 # environments).
907 902 ui.write(_('replacing store...\n'))
908 903 tstart = util.timer()
909 904 util.rename(srcrepo.spath, backupvfs.join('store'))
910 905 util.rename(dstrepo.spath, srcrepo.spath)
911 906 elapsed = util.timer() - tstart
912 907 ui.write(_('store replacement complete; repository was inconsistent for '
913 908 '%0.1fs\n') % elapsed)
914 909
915 910 # We first write the requirements file. Any new requirements will lock
916 911 # out legacy clients.
917 912 ui.write(_('finalizing requirements file and making repository readable '
918 913 'again\n'))
919 914 scmutil.writerequires(srcrepo.vfs, requirements)
920 915
921 916 # The lock file from the old store won't be removed because nothing has a
922 917 # reference to its new location. So clean it up manually. Alternatively, we
923 918 # could update srcrepo.svfs and other variables to point to the new
924 919 # location. This is simpler.
925 920 backupvfs.unlink('store/lock')
926 921
927 922 return backuppath
928 923
929 924 def upgraderepo(ui, repo, run=False, optimize=None):
930 925 """Upgrade a repository in place."""
931 926 # Avoid cycle: cmdutil -> repair -> localrepo -> cmdutil
932 927 from . import localrepo
933 928
934 929 optimize = set(optimize or [])
935 930 repo = repo.unfiltered()
936 931
937 932 # Ensure the repository can be upgraded.
938 933 missingreqs = upgraderequiredsourcerequirements(repo) - repo.requirements
939 934 if missingreqs:
940 935 raise error.Abort(_('cannot upgrade repository; requirement '
941 936 'missing: %s') % _(', ').join(sorted(missingreqs)))
942 937
943 938 blockedreqs = upgradeblocksourcerequirements(repo) & repo.requirements
944 939 if blockedreqs:
945 940 raise error.Abort(_('cannot upgrade repository; unsupported source '
946 941 'requirement: %s') %
947 942 _(', ').join(sorted(blockedreqs)))
948 943
949 944 # FUTURE there is potentially a need to control the wanted requirements via
950 945 # command arguments or via an extension hook point.
951 946 newreqs = localrepo.newreporequirements(repo)
952 947
953 948 noremovereqs = (repo.requirements - newreqs -
954 949 upgradesupportremovedrequirements(repo))
955 950 if noremovereqs:
956 951 raise error.Abort(_('cannot upgrade repository; requirement would be '
957 952 'removed: %s') % _(', ').join(sorted(noremovereqs)))
958 953
959 954 noaddreqs = (newreqs - repo.requirements -
960 955 upgradeallowednewrequirements(repo))
961 956 if noaddreqs:
962 957 raise error.Abort(_('cannot upgrade repository; do not support adding '
963 958 'requirement: %s') %
964 959 _(', ').join(sorted(noaddreqs)))
965 960
966 961 unsupportedreqs = newreqs - upgradesupporteddestrequirements(repo)
967 962 if unsupportedreqs:
968 963 raise error.Abort(_('cannot upgrade repository; do not support '
969 964 'destination requirement: %s') %
970 965 _(', ').join(sorted(unsupportedreqs)))
971 966
972 967 # Find and validate all improvements that can be made.
973 968 improvements = upgradefindimprovements(repo)
974 969 for i in improvements:
975 970 if i.type not in (deficiency, optimisation):
976 971 raise error.Abort(_('unexpected improvement type %s for %s') % (
977 972 i.type, i.name))
978 973
979 974 # Validate arguments.
980 975 unknownoptimize = optimize - set(i.name for i in improvements
981 976 if i.type == optimisation)
982 977 if unknownoptimize:
983 978 raise error.Abort(_('unknown optimization action requested: %s') %
984 979 ', '.join(sorted(unknownoptimize)),
985 980 hint=_('run without arguments to see valid '
986 981 'optimizations'))
987 982
988 983 actions = upgradedetermineactions(repo, improvements, repo.requirements,
989 984 newreqs, optimize)
990 985
991 986 def printrequirements():
992 987 ui.write(_('requirements\n'))
993 988 ui.write(_(' preserved: %s\n') %
994 989 _(', ').join(sorted(newreqs & repo.requirements)))
995 990
996 991 if repo.requirements - newreqs:
997 992 ui.write(_(' removed: %s\n') %
998 993 _(', ').join(sorted(repo.requirements - newreqs)))
999 994
1000 995 if newreqs - repo.requirements:
1001 996 ui.write(_(' added: %s\n') %
1002 997 _(', ').join(sorted(newreqs - repo.requirements)))
1003 998
1004 999 ui.write('\n')
1005 1000
1006 1001 def printupgradeactions():
1007 1002 for action in actions:
1008 1003 for i in improvements:
1009 1004 if i.name == action:
1010 1005 ui.write('%s\n %s\n\n' %
1011 1006 (i.name, i.upgrademessage))
1012 1007
1013 1008 if not run:
1014 1009 fromdefault = []
1015 1010 fromconfig = []
1016 1011 optimizations = []
1017 1012
1018 1013 for i in improvements:
1019 1014 assert i.type in (deficiency, optimisation)
1020 1015 if i.type == deficiency:
1021 1016 if i.fromdefault:
1022 1017 fromdefault.append(i)
1023 1018 if i.fromconfig:
1024 1019 fromconfig.append(i)
1025 1020 else:
1026 1021 optimizations.append(i)
1027 1022
1028 1023 if fromdefault or fromconfig:
1029 1024 fromconfignames = set(x.name for x in fromconfig)
1030 1025 onlydefault = [i for i in fromdefault
1031 1026 if i.name not in fromconfignames]
1032 1027
1033 1028 if fromconfig:
1034 1029 ui.write(_('repository lacks features recommended by '
1035 1030 'current config options:\n\n'))
1036 1031 for i in fromconfig:
1037 1032 ui.write('%s\n %s\n\n' % (i.name, i.description))
1038 1033
1039 1034 if onlydefault:
1040 1035 ui.write(_('repository lacks features used by the default '
1041 1036 'config options:\n\n'))
1042 1037 for i in onlydefault:
1043 1038 ui.write('%s\n %s\n\n' % (i.name, i.description))
1044 1039
1045 1040 ui.write('\n')
1046 1041 else:
1047 1042 ui.write(_('(no feature deficiencies found in existing '
1048 1043 'repository)\n'))
1049 1044
1050 1045 ui.write(_('performing an upgrade with "--run" will make the following '
1051 1046 'changes:\n\n'))
1052 1047
1053 1048 printrequirements()
1054 1049 printupgradeactions()
1055 1050
1056 1051 unusedoptimize = [i for i in improvements
1057 1052 if i.name not in actions and i.type == optimisation]
1058 1053 if unusedoptimize:
1059 1054 ui.write(_('additional optimizations are available by specifying '
1060 1055 '"--optimize <name>":\n\n'))
1061 1056 for i in unusedoptimize:
1062 1057 ui.write(_('%s\n %s\n\n') % (i.name, i.description))
1063 1058 return
1064 1059
1065 1060 # Else we're in the run=true case.
1066 1061 ui.write(_('upgrade will perform the following actions:\n\n'))
1067 1062 printrequirements()
1068 1063 printupgradeactions()
1069 1064
1070 1065 ui.write(_('beginning upgrade...\n'))
1071 1066 with repo.wlock():
1072 1067 with repo.lock():
1073 1068 ui.write(_('repository locked and read-only\n'))
1074 1069 # Our strategy for upgrading the repository is to create a new,
1075 1070 # temporary repository, write data to it, then do a swap of the
1076 1071 # data. There are less heavyweight ways to do this, but it is easier
1077 1072 # to create a new repo object than to instantiate all the components
1078 1073 # (like the store) separately.
1079 1074 tmppath = tempfile.mkdtemp(prefix='upgrade.', dir=repo.path)
1080 1075 backuppath = None
1081 1076 try:
1082 1077 ui.write(_('creating temporary repository to stage migrated '
1083 1078 'data: %s\n') % tmppath)
1084 1079 dstrepo = localrepo.localrepository(repo.baseui,
1085 1080 path=tmppath,
1086 1081 create=True)
1087 1082
1088 1083 with dstrepo.wlock():
1089 1084 with dstrepo.lock():
1090 1085 backuppath = _upgraderepo(ui, repo, dstrepo, newreqs,
1091 1086 actions)
1092 1087
1093 1088 finally:
1094 1089 ui.write(_('removing temporary repository %s\n') % tmppath)
1095 1090 repo.vfs.rmtree(tmppath, forcibly=True)
1096 1091
1097 1092 if backuppath:
1098 1093 ui.warn(_('copy of old repository backed up at %s\n') %
1099 1094 backuppath)
1100 1095 ui.warn(_('the old repository will not be deleted; remove '
1101 1096 'it to free up disk space once the upgraded '
1102 1097 'repository is verified\n'))
General Comments 0
You need to be logged in to leave comments. Login now