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