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