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