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