##// END OF EJS Templates
softstrip: move _bookmarkmovements() call to where it's needed...
Martin von Zweigbergk -
r47264:beaa233e default
parent child Browse files
Show More
@@ -1,545 +1,545 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 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
312 311 if backup:
313 312 node = tostrip[0]
314 313 backupfile = _createstripbackup(repo, tostrip, node, topic)
315 314
315 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
316 316 with repo.transaction(b'strip') as tr:
317 317 phases.retractboundary(repo, tr, phases.archived, tostrip)
318 318 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
319 319 repo._bookmarks.applychanges(repo, tr, bmchanges)
320 320 return backupfile
321 321
322 322
323 323 def _bookmarkmovements(repo, tostrip):
324 324 # compute necessary bookmark movement
325 325 bm = repo._bookmarks
326 326 updatebm = []
327 327 for m in bm:
328 328 rev = repo[bm[m]].rev()
329 329 if rev in tostrip:
330 330 updatebm.append(m)
331 331 newbmtarget = None
332 332 # If we need to move bookmarks, compute bookmark
333 333 # targets. Otherwise we can skip doing this logic.
334 334 if updatebm:
335 335 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)),
336 336 # but is much faster
337 337 newbmtarget = repo.revs(b'max(parents(%ld) - (%ld))', tostrip, tostrip)
338 338 if newbmtarget:
339 339 newbmtarget = repo[newbmtarget.first()].node()
340 340 else:
341 341 newbmtarget = b'.'
342 342 return newbmtarget, updatebm
343 343
344 344
345 345 def _createstripbackup(repo, stripbases, node, topic):
346 346 # backup the changeset we are about to strip
347 347 vfs = repo.vfs
348 348 cl = repo.changelog
349 349 backupfile = backupbundle(repo, stripbases, cl.heads(), node, topic)
350 350 repo.ui.status(_(b"saved backup bundle to %s\n") % vfs.join(backupfile))
351 351 repo.ui.log(
352 352 b"backupbundle", b"saved backup bundle to %s\n", vfs.join(backupfile)
353 353 )
354 354 return backupfile
355 355
356 356
357 357 def safestriproots(ui, repo, nodes):
358 358 """return list of roots of nodes where descendants are covered by nodes"""
359 359 torev = repo.unfiltered().changelog.rev
360 360 revs = {torev(n) for n in nodes}
361 361 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
362 362 # orphaned = affected - wanted
363 363 # affected = descendants(roots(wanted))
364 364 # wanted = revs
365 365 revset = b'%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )'
366 366 tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs))
367 367 notstrip = revs - tostrip
368 368 if notstrip:
369 369 nodestr = b', '.join(sorted(short(repo[n].node()) for n in notstrip))
370 370 ui.warn(
371 371 _(b'warning: orphaned descendants detected, not stripping %s\n')
372 372 % nodestr
373 373 )
374 374 return [c.node() for c in repo.set(b'roots(%ld)', tostrip)]
375 375
376 376
377 377 class stripcallback(object):
378 378 """used as a transaction postclose callback"""
379 379
380 380 def __init__(self, ui, repo, backup, topic):
381 381 self.ui = ui
382 382 self.repo = repo
383 383 self.backup = backup
384 384 self.topic = topic or b'backup'
385 385 self.nodelist = []
386 386
387 387 def addnodes(self, nodes):
388 388 self.nodelist.extend(nodes)
389 389
390 390 def __call__(self, tr):
391 391 roots = safestriproots(self.ui, self.repo, self.nodelist)
392 392 if roots:
393 393 strip(self.ui, self.repo, roots, self.backup, self.topic)
394 394
395 395
396 396 def delayedstrip(ui, repo, nodelist, topic=None, backup=True):
397 397 """like strip, but works inside transaction and won't strip irreverent revs
398 398
399 399 nodelist must explicitly contain all descendants. Otherwise a warning will
400 400 be printed that some nodes are not stripped.
401 401
402 402 Will do a backup if `backup` is True. The last non-None "topic" will be
403 403 used as the backup topic name. The default backup topic name is "backup".
404 404 """
405 405 tr = repo.currenttransaction()
406 406 if not tr:
407 407 nodes = safestriproots(ui, repo, nodelist)
408 408 return strip(ui, repo, nodes, backup=backup, topic=topic)
409 409 # transaction postclose callbacks are called in alphabet order.
410 410 # use '\xff' as prefix so we are likely to be called last.
411 411 callback = tr.getpostclose(b'\xffstrip')
412 412 if callback is None:
413 413 callback = stripcallback(ui, repo, backup=backup, topic=topic)
414 414 tr.addpostclose(b'\xffstrip', callback)
415 415 if topic:
416 416 callback.topic = topic
417 417 callback.addnodes(nodelist)
418 418
419 419
420 420 def stripmanifest(repo, striprev, tr, files):
421 421 for revlog in manifestrevlogs(repo):
422 422 revlog.strip(striprev, tr)
423 423
424 424
425 425 def manifestrevlogs(repo):
426 426 yield repo.manifestlog.getstorage(b'')
427 427 if scmutil.istreemanifest(repo):
428 428 # This logic is safe if treemanifest isn't enabled, but also
429 429 # pointless, so we skip it if treemanifest isn't enabled.
430 430 for unencoded, encoded, size in repo.store.datafiles():
431 431 if unencoded.startswith(b'meta/') and unencoded.endswith(
432 432 b'00manifest.i'
433 433 ):
434 434 dir = unencoded[5:-12]
435 435 yield repo.manifestlog.getstorage(dir)
436 436
437 437
438 438 def rebuildfncache(ui, repo):
439 439 """Rebuilds the fncache file from repo history.
440 440
441 441 Missing entries will be added. Extra entries will be removed.
442 442 """
443 443 repo = repo.unfiltered()
444 444
445 445 if b'fncache' not in repo.requirements:
446 446 ui.warn(
447 447 _(
448 448 b'(not rebuilding fncache because repository does not '
449 449 b'support fncache)\n'
450 450 )
451 451 )
452 452 return
453 453
454 454 with repo.lock():
455 455 fnc = repo.store.fncache
456 456 fnc.ensureloaded(warn=ui.warn)
457 457
458 458 oldentries = set(fnc.entries)
459 459 newentries = set()
460 460 seenfiles = set()
461 461
462 462 progress = ui.makeprogress(
463 463 _(b'rebuilding'), unit=_(b'changesets'), total=len(repo)
464 464 )
465 465 for rev in repo:
466 466 progress.update(rev)
467 467
468 468 ctx = repo[rev]
469 469 for f in ctx.files():
470 470 # This is to minimize I/O.
471 471 if f in seenfiles:
472 472 continue
473 473 seenfiles.add(f)
474 474
475 475 i = b'data/%s.i' % f
476 476 d = b'data/%s.d' % f
477 477
478 478 if repo.store._exists(i):
479 479 newentries.add(i)
480 480 if repo.store._exists(d):
481 481 newentries.add(d)
482 482
483 483 progress.complete()
484 484
485 485 if requirements.TREEMANIFEST_REQUIREMENT in repo.requirements:
486 486 # This logic is safe if treemanifest isn't enabled, but also
487 487 # pointless, so we skip it if treemanifest isn't enabled.
488 488 for dir in pathutil.dirs(seenfiles):
489 489 i = b'meta/%s/00manifest.i' % dir
490 490 d = b'meta/%s/00manifest.d' % dir
491 491
492 492 if repo.store._exists(i):
493 493 newentries.add(i)
494 494 if repo.store._exists(d):
495 495 newentries.add(d)
496 496
497 497 addcount = len(newentries - oldentries)
498 498 removecount = len(oldentries - newentries)
499 499 for p in sorted(oldentries - newentries):
500 500 ui.write(_(b'removing %s\n') % p)
501 501 for p in sorted(newentries - oldentries):
502 502 ui.write(_(b'adding %s\n') % p)
503 503
504 504 if addcount or removecount:
505 505 ui.write(
506 506 _(b'%d items added, %d removed from fncache\n')
507 507 % (addcount, removecount)
508 508 )
509 509 fnc.entries = newentries
510 510 fnc._dirty = True
511 511
512 512 with repo.transaction(b'fncache') as tr:
513 513 fnc.write(tr)
514 514 else:
515 515 ui.write(_(b'fncache already up to date\n'))
516 516
517 517
518 518 def deleteobsmarkers(obsstore, indices):
519 519 """Delete some obsmarkers from obsstore and return how many were deleted
520 520
521 521 'indices' is a list of ints which are the indices
522 522 of the markers to be deleted.
523 523
524 524 Every invocation of this function completely rewrites the obsstore file,
525 525 skipping the markers we want to be removed. The new temporary file is
526 526 created, remaining markers are written there and on .close() this file
527 527 gets atomically renamed to obsstore, thus guaranteeing consistency."""
528 528 if not indices:
529 529 # we don't want to rewrite the obsstore with the same content
530 530 return
531 531
532 532 left = []
533 533 current = obsstore._all
534 534 n = 0
535 535 for i, m in enumerate(current):
536 536 if i in indices:
537 537 n += 1
538 538 continue
539 539 left.append(m)
540 540
541 541 newobsstorefile = obsstore.svfs(b'obsstore', b'w', atomictemp=True)
542 542 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
543 543 newobsstorefile.write(bytes)
544 544 newobsstorefile.close()
545 545 return n
General Comments 0
You need to be logged in to leave comments. Login now