##// END OF EJS Templates
softstrip: fix a reference to an undefined variable...
Martin von Zweigbergk -
r47265:cd915434 default
parent child Browse files
Show More
@@ -1,545 +1,546 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
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 short,
17 17 )
18 18 from . import (
19 19 bundle2,
20 20 changegroup,
21 21 discovery,
22 22 error,
23 23 exchange,
24 24 obsolete,
25 25 obsutil,
26 26 pathutil,
27 27 phases,
28 28 pycompat,
29 29 requirements,
30 30 scmutil,
31 31 util,
32 32 )
33 33 from .utils import (
34 34 hashutil,
35 35 stringutil,
36 36 )
37 37
38 38
39 39 def backupbundle(
40 40 repo, bases, heads, node, suffix, compress=True, obsolescence=True
41 41 ):
42 42 """create a bundle with the specified revisions as a backup"""
43 43
44 44 backupdir = b"strip-backup"
45 45 vfs = repo.vfs
46 46 if not vfs.isdir(backupdir):
47 47 vfs.mkdir(backupdir)
48 48
49 49 # Include a hash of all the nodes in the filename for uniqueness
50 50 allcommits = repo.set(b'%ln::%ln', bases, heads)
51 51 allhashes = sorted(c.hex() for c in allcommits)
52 52 totalhash = hashutil.sha1(b''.join(allhashes)).digest()
53 53 name = b"%s/%s-%s-%s.hg" % (
54 54 backupdir,
55 55 short(node),
56 56 hex(totalhash[:4]),
57 57 suffix,
58 58 )
59 59
60 60 cgversion = changegroup.localversion(repo)
61 61 comp = None
62 62 if cgversion != b'01':
63 63 bundletype = b"HG20"
64 64 if compress:
65 65 comp = b'BZ'
66 66 elif compress:
67 67 bundletype = b"HG10BZ"
68 68 else:
69 69 bundletype = b"HG10UN"
70 70
71 71 outgoing = discovery.outgoing(repo, missingroots=bases, ancestorsof=heads)
72 72 contentopts = {
73 73 b'cg.version': cgversion,
74 74 b'obsolescence': obsolescence,
75 75 b'phases': True,
76 76 }
77 77 return bundle2.writenewbundle(
78 78 repo.ui,
79 79 repo,
80 80 b'strip',
81 81 name,
82 82 bundletype,
83 83 outgoing,
84 84 contentopts,
85 85 vfs,
86 86 compression=comp,
87 87 )
88 88
89 89
90 90 def _collectfiles(repo, striprev):
91 91 """find out the filelogs affected by the strip"""
92 92 files = set()
93 93
94 94 for x in pycompat.xrange(striprev, len(repo)):
95 95 files.update(repo[x].files())
96 96
97 97 return sorted(files)
98 98
99 99
100 100 def _collectrevlog(revlog, striprev):
101 101 _, brokenset = revlog.getstrippoint(striprev)
102 102 return [revlog.linkrev(r) for r in brokenset]
103 103
104 104
105 105 def _collectbrokencsets(repo, files, striprev):
106 106 """return the changesets which will be broken by the truncation"""
107 107 s = set()
108 108
109 109 for revlog in manifestrevlogs(repo):
110 110 s.update(_collectrevlog(revlog, striprev))
111 111 for fname in files:
112 112 s.update(_collectrevlog(repo.file(fname), striprev))
113 113
114 114 return s
115 115
116 116
117 117 def strip(ui, repo, nodelist, backup=True, topic=b'backup'):
118 118 # This function requires the caller to lock the repo, but it operates
119 119 # within a transaction of its own, and thus requires there to be no current
120 120 # transaction when it is called.
121 121 if repo.currenttransaction() is not None:
122 122 raise error.ProgrammingError(b'cannot strip from inside a transaction')
123 123
124 124 # Simple way to maintain backwards compatibility for this
125 125 # argument.
126 126 if backup in [b'none', b'strip']:
127 127 backup = False
128 128
129 129 repo = repo.unfiltered()
130 130 repo.destroying()
131 131 vfs = repo.vfs
132 132 # load bookmark before changelog to avoid side effect from outdated
133 133 # changelog (see repo._refreshchangelog)
134 134 repo._bookmarks
135 135 cl = repo.changelog
136 136
137 137 # TODO handle undo of merge sets
138 138 if isinstance(nodelist, bytes):
139 139 nodelist = [nodelist]
140 140 striplist = [cl.rev(node) for node in nodelist]
141 141 striprev = min(striplist)
142 142
143 143 files = _collectfiles(repo, striprev)
144 144 saverevs = _collectbrokencsets(repo, files, striprev)
145 145
146 146 # Some revisions with rev > striprev may not be descendants of striprev.
147 147 # We have to find these revisions and put them in a bundle, so that
148 148 # we can restore them after the truncations.
149 149 # To create the bundle we use repo.changegroupsubset which requires
150 150 # the list of heads and bases of the set of interesting revisions.
151 151 # (head = revision in the set that has no descendant in the set;
152 152 # base = revision in the set that has no ancestor in the set)
153 153 tostrip = set(striplist)
154 154 saveheads = set(saverevs)
155 155 for r in cl.revs(start=striprev + 1):
156 156 if any(p in tostrip for p in cl.parentrevs(r)):
157 157 tostrip.add(r)
158 158
159 159 if r not in tostrip:
160 160 saverevs.add(r)
161 161 saveheads.difference_update(cl.parentrevs(r))
162 162 saveheads.add(r)
163 163 saveheads = [cl.node(r) for r in saveheads]
164 164
165 165 # compute base nodes
166 166 if saverevs:
167 167 descendants = set(cl.descendants(saverevs))
168 168 saverevs.difference_update(descendants)
169 169 savebases = [cl.node(r) for r in saverevs]
170 170 stripbases = [cl.node(r) for r in tostrip]
171 171
172 172 stripobsidx = obsmarkers = ()
173 173 if repo.ui.configbool(b'devel', b'strip-obsmarkers'):
174 174 obsmarkers = obsutil.exclusivemarkers(repo, stripbases)
175 175 if obsmarkers:
176 176 stripobsidx = [
177 177 i for i, m in enumerate(repo.obsstore) if m in obsmarkers
178 178 ]
179 179
180 180 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
181 181
182 182 backupfile = None
183 183 node = nodelist[-1]
184 184 if backup:
185 185 backupfile = _createstripbackup(repo, stripbases, node, topic)
186 186 # create a changegroup for all the branches we need to keep
187 187 tmpbundlefile = None
188 188 if saveheads:
189 189 # do not compress temporary bundle if we remove it from disk later
190 190 #
191 191 # We do not include obsolescence, it might re-introduce prune markers
192 192 # we are trying to strip. This is harmless since the stripped markers
193 193 # are already backed up and we did not touched the markers for the
194 194 # saved changesets.
195 195 tmpbundlefile = backupbundle(
196 196 repo,
197 197 savebases,
198 198 saveheads,
199 199 node,
200 200 b'temp',
201 201 compress=False,
202 202 obsolescence=False,
203 203 )
204 204
205 205 with ui.uninterruptible():
206 206 try:
207 207 with repo.transaction(b"strip") as tr:
208 208 # TODO this code violates the interface abstraction of the
209 209 # transaction and makes assumptions that file storage is
210 210 # using append-only files. We'll need some kind of storage
211 211 # API to handle stripping for us.
212 212 oldfiles = set(tr._offsetmap.keys())
213 213 oldfiles.update(tr._newfiles)
214 214
215 215 tr.startgroup()
216 216 cl.strip(striprev, tr)
217 217 stripmanifest(repo, striprev, tr, files)
218 218
219 219 for fn in files:
220 220 repo.file(fn).strip(striprev, tr)
221 221 tr.endgroup()
222 222
223 223 entries = tr.readjournal()
224 224
225 225 for file, troffset in entries:
226 226 if file in oldfiles:
227 227 continue
228 228 with repo.svfs(file, b'a', checkambig=True) as fp:
229 229 fp.truncate(troffset)
230 230 if troffset == 0:
231 231 repo.store.markremoved(file)
232 232
233 233 deleteobsmarkers(repo.obsstore, stripobsidx)
234 234 del repo.obsstore
235 235 repo.invalidatevolatilesets()
236 236 repo._phasecache.filterunknown(repo)
237 237
238 238 if tmpbundlefile:
239 239 ui.note(_(b"adding branch\n"))
240 240 f = vfs.open(tmpbundlefile, b"rb")
241 241 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
242 242 if not repo.ui.verbose:
243 243 # silence internal shuffling chatter
244 244 repo.ui.pushbuffer()
245 245 tmpbundleurl = b'bundle:' + vfs.join(tmpbundlefile)
246 246 txnname = b'strip'
247 247 if not isinstance(gen, bundle2.unbundle20):
248 248 txnname = b"strip\n%s" % util.hidepassword(tmpbundleurl)
249 249 with repo.transaction(txnname) as tr:
250 250 bundle2.applybundle(
251 251 repo, gen, tr, source=b'strip', url=tmpbundleurl
252 252 )
253 253 if not repo.ui.verbose:
254 254 repo.ui.popbuffer()
255 255 f.close()
256 256
257 257 with repo.transaction(b'repair') as tr:
258 258 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
259 259 repo._bookmarks.applychanges(repo, tr, bmchanges)
260 260
261 261 # remove undo files
262 262 for undovfs, undofile in repo.undofiles():
263 263 try:
264 264 undovfs.unlink(undofile)
265 265 except OSError as e:
266 266 if e.errno != errno.ENOENT:
267 267 ui.warn(
268 268 _(b'error removing %s: %s\n')
269 269 % (
270 270 undovfs.join(undofile),
271 271 stringutil.forcebytestr(e),
272 272 )
273 273 )
274 274
275 275 except: # re-raises
276 276 if backupfile:
277 277 ui.warn(
278 278 _(b"strip failed, backup bundle stored in '%s'\n")
279 279 % vfs.join(backupfile)
280 280 )
281 281 if tmpbundlefile:
282 282 ui.warn(
283 283 _(b"strip failed, unrecovered changes stored in '%s'\n")
284 284 % vfs.join(tmpbundlefile)
285 285 )
286 286 ui.warn(
287 287 _(
288 288 b"(fix the problem, then recover the changesets with "
289 289 b"\"hg unbundle '%s'\")\n"
290 290 )
291 291 % vfs.join(tmpbundlefile)
292 292 )
293 293 raise
294 294 else:
295 295 if tmpbundlefile:
296 296 # Remove temporary bundle only if there were no exceptions
297 297 vfs.unlink(tmpbundlefile)
298 298
299 299 repo.destroyed()
300 300 # return the backup file path (or None if 'backup' was False) so
301 301 # extensions can use it
302 302 return backupfile
303 303
304 304
305 305 def softstrip(ui, repo, nodelist, backup=True, topic=b'backup'):
306 306 """perform a "soft" strip using the archived phase"""
307 307 tostrip = [c.node() for c in repo.set(b'sort(%ln::)', nodelist)]
308 308 if not tostrip:
309 309 return None
310 310
311 backupfile = None
311 312 if backup:
312 313 node = tostrip[0]
313 314 backupfile = _createstripbackup(repo, tostrip, node, topic)
314 315
315 316 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
316 317 with repo.transaction(b'strip') as tr:
317 318 phases.retractboundary(repo, tr, phases.archived, tostrip)
318 319 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
319 320 repo._bookmarks.applychanges(repo, tr, bmchanges)
320 321 return backupfile
321 322
322 323
323 324 def _bookmarkmovements(repo, tostrip):
324 325 # compute necessary bookmark movement
325 326 bm = repo._bookmarks
326 327 updatebm = []
327 328 for m in bm:
328 329 rev = repo[bm[m]].rev()
329 330 if rev in tostrip:
330 331 updatebm.append(m)
331 332 newbmtarget = None
332 333 # If we need to move bookmarks, compute bookmark
333 334 # targets. Otherwise we can skip doing this logic.
334 335 if updatebm:
335 336 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)),
336 337 # but is much faster
337 338 newbmtarget = repo.revs(b'max(parents(%ld) - (%ld))', tostrip, tostrip)
338 339 if newbmtarget:
339 340 newbmtarget = repo[newbmtarget.first()].node()
340 341 else:
341 342 newbmtarget = b'.'
342 343 return newbmtarget, updatebm
343 344
344 345
345 346 def _createstripbackup(repo, stripbases, node, topic):
346 347 # backup the changeset we are about to strip
347 348 vfs = repo.vfs
348 349 cl = repo.changelog
349 350 backupfile = backupbundle(repo, stripbases, cl.heads(), node, topic)
350 351 repo.ui.status(_(b"saved backup bundle to %s\n") % vfs.join(backupfile))
351 352 repo.ui.log(
352 353 b"backupbundle", b"saved backup bundle to %s\n", vfs.join(backupfile)
353 354 )
354 355 return backupfile
355 356
356 357
357 358 def safestriproots(ui, repo, nodes):
358 359 """return list of roots of nodes where descendants are covered by nodes"""
359 360 torev = repo.unfiltered().changelog.rev
360 361 revs = {torev(n) for n in nodes}
361 362 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
362 363 # orphaned = affected - wanted
363 364 # affected = descendants(roots(wanted))
364 365 # wanted = revs
365 366 revset = b'%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )'
366 367 tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs))
367 368 notstrip = revs - tostrip
368 369 if notstrip:
369 370 nodestr = b', '.join(sorted(short(repo[n].node()) for n in notstrip))
370 371 ui.warn(
371 372 _(b'warning: orphaned descendants detected, not stripping %s\n')
372 373 % nodestr
373 374 )
374 375 return [c.node() for c in repo.set(b'roots(%ld)', tostrip)]
375 376
376 377
377 378 class stripcallback(object):
378 379 """used as a transaction postclose callback"""
379 380
380 381 def __init__(self, ui, repo, backup, topic):
381 382 self.ui = ui
382 383 self.repo = repo
383 384 self.backup = backup
384 385 self.topic = topic or b'backup'
385 386 self.nodelist = []
386 387
387 388 def addnodes(self, nodes):
388 389 self.nodelist.extend(nodes)
389 390
390 391 def __call__(self, tr):
391 392 roots = safestriproots(self.ui, self.repo, self.nodelist)
392 393 if roots:
393 394 strip(self.ui, self.repo, roots, self.backup, self.topic)
394 395
395 396
396 397 def delayedstrip(ui, repo, nodelist, topic=None, backup=True):
397 398 """like strip, but works inside transaction and won't strip irreverent revs
398 399
399 400 nodelist must explicitly contain all descendants. Otherwise a warning will
400 401 be printed that some nodes are not stripped.
401 402
402 403 Will do a backup if `backup` is True. The last non-None "topic" will be
403 404 used as the backup topic name. The default backup topic name is "backup".
404 405 """
405 406 tr = repo.currenttransaction()
406 407 if not tr:
407 408 nodes = safestriproots(ui, repo, nodelist)
408 409 return strip(ui, repo, nodes, backup=backup, topic=topic)
409 410 # transaction postclose callbacks are called in alphabet order.
410 411 # use '\xff' as prefix so we are likely to be called last.
411 412 callback = tr.getpostclose(b'\xffstrip')
412 413 if callback is None:
413 414 callback = stripcallback(ui, repo, backup=backup, topic=topic)
414 415 tr.addpostclose(b'\xffstrip', callback)
415 416 if topic:
416 417 callback.topic = topic
417 418 callback.addnodes(nodelist)
418 419
419 420
420 421 def stripmanifest(repo, striprev, tr, files):
421 422 for revlog in manifestrevlogs(repo):
422 423 revlog.strip(striprev, tr)
423 424
424 425
425 426 def manifestrevlogs(repo):
426 427 yield repo.manifestlog.getstorage(b'')
427 428 if scmutil.istreemanifest(repo):
428 429 # This logic is safe if treemanifest isn't enabled, but also
429 430 # pointless, so we skip it if treemanifest isn't enabled.
430 431 for unencoded, encoded, size in repo.store.datafiles():
431 432 if unencoded.startswith(b'meta/') and unencoded.endswith(
432 433 b'00manifest.i'
433 434 ):
434 435 dir = unencoded[5:-12]
435 436 yield repo.manifestlog.getstorage(dir)
436 437
437 438
438 439 def rebuildfncache(ui, repo):
439 440 """Rebuilds the fncache file from repo history.
440 441
441 442 Missing entries will be added. Extra entries will be removed.
442 443 """
443 444 repo = repo.unfiltered()
444 445
445 446 if b'fncache' not in repo.requirements:
446 447 ui.warn(
447 448 _(
448 449 b'(not rebuilding fncache because repository does not '
449 450 b'support fncache)\n'
450 451 )
451 452 )
452 453 return
453 454
454 455 with repo.lock():
455 456 fnc = repo.store.fncache
456 457 fnc.ensureloaded(warn=ui.warn)
457 458
458 459 oldentries = set(fnc.entries)
459 460 newentries = set()
460 461 seenfiles = set()
461 462
462 463 progress = ui.makeprogress(
463 464 _(b'rebuilding'), unit=_(b'changesets'), total=len(repo)
464 465 )
465 466 for rev in repo:
466 467 progress.update(rev)
467 468
468 469 ctx = repo[rev]
469 470 for f in ctx.files():
470 471 # This is to minimize I/O.
471 472 if f in seenfiles:
472 473 continue
473 474 seenfiles.add(f)
474 475
475 476 i = b'data/%s.i' % f
476 477 d = b'data/%s.d' % f
477 478
478 479 if repo.store._exists(i):
479 480 newentries.add(i)
480 481 if repo.store._exists(d):
481 482 newentries.add(d)
482 483
483 484 progress.complete()
484 485
485 486 if requirements.TREEMANIFEST_REQUIREMENT in repo.requirements:
486 487 # This logic is safe if treemanifest isn't enabled, but also
487 488 # pointless, so we skip it if treemanifest isn't enabled.
488 489 for dir in pathutil.dirs(seenfiles):
489 490 i = b'meta/%s/00manifest.i' % dir
490 491 d = b'meta/%s/00manifest.d' % dir
491 492
492 493 if repo.store._exists(i):
493 494 newentries.add(i)
494 495 if repo.store._exists(d):
495 496 newentries.add(d)
496 497
497 498 addcount = len(newentries - oldentries)
498 499 removecount = len(oldentries - newentries)
499 500 for p in sorted(oldentries - newentries):
500 501 ui.write(_(b'removing %s\n') % p)
501 502 for p in sorted(newentries - oldentries):
502 503 ui.write(_(b'adding %s\n') % p)
503 504
504 505 if addcount or removecount:
505 506 ui.write(
506 507 _(b'%d items added, %d removed from fncache\n')
507 508 % (addcount, removecount)
508 509 )
509 510 fnc.entries = newentries
510 511 fnc._dirty = True
511 512
512 513 with repo.transaction(b'fncache') as tr:
513 514 fnc.write(tr)
514 515 else:
515 516 ui.write(_(b'fncache already up to date\n'))
516 517
517 518
518 519 def deleteobsmarkers(obsstore, indices):
519 520 """Delete some obsmarkers from obsstore and return how many were deleted
520 521
521 522 'indices' is a list of ints which are the indices
522 523 of the markers to be deleted.
523 524
524 525 Every invocation of this function completely rewrites the obsstore file,
525 526 skipping the markers we want to be removed. The new temporary file is
526 527 created, remaining markers are written there and on .close() this file
527 528 gets atomically renamed to obsstore, thus guaranteeing consistency."""
528 529 if not indices:
529 530 # we don't want to rewrite the obsstore with the same content
530 531 return
531 532
532 533 left = []
533 534 current = obsstore._all
534 535 n = 0
535 536 for i, m in enumerate(current):
536 537 if i in indices:
537 538 n += 1
538 539 continue
539 540 left.append(m)
540 541
541 542 newobsstorefile = obsstore.svfs(b'obsstore', b'w', atomictemp=True)
542 543 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
543 544 newobsstorefile.write(bytes)
544 545 newobsstorefile.close()
545 546 return n
General Comments 0
You need to be logged in to leave comments. Login now