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