##// END OF EJS Templates
repair: use progress helper...
Martin von Zweigbergk -
r38413:f0b0c853 default
parent child Browse files
Show More
@@ -1,435 +1,435
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 189 try:
190 190 with repo.transaction("strip") as tr:
191 191 offset = len(tr.entries)
192 192
193 193 tr.startgroup()
194 194 cl.strip(striprev, tr)
195 195 stripmanifest(repo, striprev, tr, files)
196 196
197 197 for fn in files:
198 198 repo.file(fn).strip(striprev, tr)
199 199 tr.endgroup()
200 200
201 201 for i in xrange(offset, len(tr.entries)):
202 202 file, troffset, ignore = tr.entries[i]
203 203 with repo.svfs(file, 'a', checkambig=True) as fp:
204 204 fp.truncate(troffset)
205 205 if troffset == 0:
206 206 repo.store.markremoved(file)
207 207
208 208 deleteobsmarkers(repo.obsstore, stripobsidx)
209 209 del repo.obsstore
210 210 repo.invalidatevolatilesets()
211 211 repo._phasecache.filterunknown(repo)
212 212
213 213 if tmpbundlefile:
214 214 ui.note(_("adding branch\n"))
215 215 f = vfs.open(tmpbundlefile, "rb")
216 216 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
217 217 if not repo.ui.verbose:
218 218 # silence internal shuffling chatter
219 219 repo.ui.pushbuffer()
220 220 tmpbundleurl = 'bundle:' + vfs.join(tmpbundlefile)
221 221 txnname = 'strip'
222 222 if not isinstance(gen, bundle2.unbundle20):
223 223 txnname = "strip\n%s" % util.hidepassword(tmpbundleurl)
224 224 with repo.transaction(txnname) as tr:
225 225 bundle2.applybundle(repo, gen, tr, source='strip',
226 226 url=tmpbundleurl)
227 227 if not repo.ui.verbose:
228 228 repo.ui.popbuffer()
229 229 f.close()
230 230
231 231 with repo.transaction('repair') as tr:
232 232 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
233 233 bm.applychanges(repo, tr, bmchanges)
234 234
235 235 # remove undo files
236 236 for undovfs, undofile in repo.undofiles():
237 237 try:
238 238 undovfs.unlink(undofile)
239 239 except OSError as e:
240 240 if e.errno != errno.ENOENT:
241 241 ui.warn(_('error removing %s: %s\n') %
242 242 (undovfs.join(undofile),
243 243 stringutil.forcebytestr(e)))
244 244
245 245 except: # re-raises
246 246 if backupfile:
247 247 ui.warn(_("strip failed, backup bundle stored in '%s'\n")
248 248 % vfs.join(backupfile))
249 249 if tmpbundlefile:
250 250 ui.warn(_("strip failed, unrecovered changes stored in '%s'\n")
251 251 % vfs.join(tmpbundlefile))
252 252 ui.warn(_("(fix the problem, then recover the changesets with "
253 253 "\"hg unbundle '%s'\")\n") % vfs.join(tmpbundlefile))
254 254 raise
255 255 else:
256 256 if tmpbundlefile:
257 257 # Remove temporary bundle only if there were no exceptions
258 258 vfs.unlink(tmpbundlefile)
259 259
260 260 repo.destroyed()
261 261 # return the backup file path (or None if 'backup' was False) so
262 262 # extensions can use it
263 263 return backupfile
264 264
265 265 def safestriproots(ui, repo, nodes):
266 266 """return list of roots of nodes where descendants are covered by nodes"""
267 267 torev = repo.unfiltered().changelog.rev
268 268 revs = set(torev(n) for n in nodes)
269 269 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
270 270 # orphaned = affected - wanted
271 271 # affected = descendants(roots(wanted))
272 272 # wanted = revs
273 273 tostrip = set(repo.revs('%ld-(::((roots(%ld)::)-%ld))', revs, revs, revs))
274 274 notstrip = revs - tostrip
275 275 if notstrip:
276 276 nodestr = ', '.join(sorted(short(repo[n].node()) for n in notstrip))
277 277 ui.warn(_('warning: orphaned descendants detected, '
278 278 'not stripping %s\n') % nodestr)
279 279 return [c.node() for c in repo.set('roots(%ld)', tostrip)]
280 280
281 281 class stripcallback(object):
282 282 """used as a transaction postclose callback"""
283 283
284 284 def __init__(self, ui, repo, backup, topic):
285 285 self.ui = ui
286 286 self.repo = repo
287 287 self.backup = backup
288 288 self.topic = topic or 'backup'
289 289 self.nodelist = []
290 290
291 291 def addnodes(self, nodes):
292 292 self.nodelist.extend(nodes)
293 293
294 294 def __call__(self, tr):
295 295 roots = safestriproots(self.ui, self.repo, self.nodelist)
296 296 if roots:
297 297 strip(self.ui, self.repo, roots, self.backup, self.topic)
298 298
299 299 def delayedstrip(ui, repo, nodelist, topic=None):
300 300 """like strip, but works inside transaction and won't strip irreverent revs
301 301
302 302 nodelist must explicitly contain all descendants. Otherwise a warning will
303 303 be printed that some nodes are not stripped.
304 304
305 305 Always do a backup. The last non-None "topic" will be used as the backup
306 306 topic name. The default backup topic name is "backup".
307 307 """
308 308 tr = repo.currenttransaction()
309 309 if not tr:
310 310 nodes = safestriproots(ui, repo, nodelist)
311 311 return strip(ui, repo, nodes, True, topic)
312 312 # transaction postclose callbacks are called in alphabet order.
313 313 # use '\xff' as prefix so we are likely to be called last.
314 314 callback = tr.getpostclose('\xffstrip')
315 315 if callback is None:
316 316 callback = stripcallback(ui, repo, True, topic)
317 317 tr.addpostclose('\xffstrip', callback)
318 318 if topic:
319 319 callback.topic = topic
320 320 callback.addnodes(nodelist)
321 321
322 322 def stripmanifest(repo, striprev, tr, files):
323 323 revlog = repo.manifestlog._revlog
324 324 revlog.strip(striprev, tr)
325 325 striptrees(repo, tr, striprev, files)
326 326
327 327 def striptrees(repo, tr, striprev, files):
328 328 if 'treemanifest' in repo.requirements: # safe but unnecessary
329 329 # otherwise
330 330 for unencoded, encoded, size in repo.store.datafiles():
331 331 if (unencoded.startswith('meta/') and
332 332 unencoded.endswith('00manifest.i')):
333 333 dir = unencoded[5:-12]
334 334 repo.manifestlog._revlog.dirlog(dir).strip(striprev, tr)
335 335
336 336 def rebuildfncache(ui, repo):
337 337 """Rebuilds the fncache file from repo history.
338 338
339 339 Missing entries will be added. Extra entries will be removed.
340 340 """
341 341 repo = repo.unfiltered()
342 342
343 343 if 'fncache' not in repo.requirements:
344 344 ui.warn(_('(not rebuilding fncache because repository does not '
345 345 'support fncache)\n'))
346 346 return
347 347
348 348 with repo.lock():
349 349 fnc = repo.store.fncache
350 350 # Trigger load of fncache.
351 351 if 'irrelevant' in fnc:
352 352 pass
353 353
354 354 oldentries = set(fnc.entries)
355 355 newentries = set()
356 356 seenfiles = set()
357 357
358 repolen = len(repo)
358 progress = ui.makeprogress(_('rebuilding'), unit=_('changesets'),
359 total=len(repo))
359 360 for rev in repo:
360 ui.progress(_('rebuilding'), rev, total=repolen,
361 unit=_('changesets'))
361 progress.update(rev)
362 362
363 363 ctx = repo[rev]
364 364 for f in ctx.files():
365 365 # This is to minimize I/O.
366 366 if f in seenfiles:
367 367 continue
368 368 seenfiles.add(f)
369 369
370 370 i = 'data/%s.i' % f
371 371 d = 'data/%s.d' % f
372 372
373 373 if repo.store._exists(i):
374 374 newentries.add(i)
375 375 if repo.store._exists(d):
376 376 newentries.add(d)
377 377
378 ui.progress(_('rebuilding'), None)
378 progress.complete()
379 379
380 380 if 'treemanifest' in repo.requirements: # safe but unnecessary otherwise
381 381 for dir in util.dirs(seenfiles):
382 382 i = 'meta/%s/00manifest.i' % dir
383 383 d = 'meta/%s/00manifest.d' % dir
384 384
385 385 if repo.store._exists(i):
386 386 newentries.add(i)
387 387 if repo.store._exists(d):
388 388 newentries.add(d)
389 389
390 390 addcount = len(newentries - oldentries)
391 391 removecount = len(oldentries - newentries)
392 392 for p in sorted(oldentries - newentries):
393 393 ui.write(_('removing %s\n') % p)
394 394 for p in sorted(newentries - oldentries):
395 395 ui.write(_('adding %s\n') % p)
396 396
397 397 if addcount or removecount:
398 398 ui.write(_('%d items added, %d removed from fncache\n') %
399 399 (addcount, removecount))
400 400 fnc.entries = newentries
401 401 fnc._dirty = True
402 402
403 403 with repo.transaction('fncache') as tr:
404 404 fnc.write(tr)
405 405 else:
406 406 ui.write(_('fncache already up to date\n'))
407 407
408 408 def deleteobsmarkers(obsstore, indices):
409 409 """Delete some obsmarkers from obsstore and return how many were deleted
410 410
411 411 'indices' is a list of ints which are the indices
412 412 of the markers to be deleted.
413 413
414 414 Every invocation of this function completely rewrites the obsstore file,
415 415 skipping the markers we want to be removed. The new temporary file is
416 416 created, remaining markers are written there and on .close() this file
417 417 gets atomically renamed to obsstore, thus guaranteeing consistency."""
418 418 if not indices:
419 419 # we don't want to rewrite the obsstore with the same content
420 420 return
421 421
422 422 left = []
423 423 current = obsstore._all
424 424 n = 0
425 425 for i, m in enumerate(current):
426 426 if i in indices:
427 427 n += 1
428 428 continue
429 429 left.append(m)
430 430
431 431 newobsstorefile = obsstore.svfs('obsstore', 'w', atomictemp=True)
432 432 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
433 433 newobsstorefile.write(bytes)
434 434 newobsstorefile.close()
435 435 return n
General Comments 0
You need to be logged in to leave comments. Login now