##// END OF EJS Templates
repair: reword comment about bookmarks logic...
Augie Fackler -
r42432:e10b8058 default
parent child Browse files
Show More
@@ -1,474 +1,476 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 (
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 if updatebm: # don't compute anything is there is no bookmark to move anyway
282 # If we need to move bookmarks, compute bookmark
283 # targets. Otherwise we can skip doing this logic.
284 if updatebm:
283 285 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)),
284 286 # but is much faster
285 287 newbmtarget = repo.revs('max(parents(%ld) - (%ld))', tostrip, tostrip)
286 288 if newbmtarget:
287 289 newbmtarget = repo[newbmtarget.first()].node()
288 290 else:
289 291 newbmtarget = '.'
290 292 return newbmtarget, updatebm
291 293
292 294 def _createstripbackup(repo, stripbases, node, topic):
293 295 # backup the changeset we are about to strip
294 296 vfs = repo.vfs
295 297 cl = repo.changelog
296 298 backupfile = backupbundle(repo, stripbases, cl.heads(), node, topic)
297 299 repo.ui.status(_("saved backup bundle to %s\n") %
298 300 vfs.join(backupfile))
299 301 repo.ui.log("backupbundle", "saved backup bundle to %s\n",
300 302 vfs.join(backupfile))
301 303 return backupfile
302 304
303 305 def safestriproots(ui, repo, nodes):
304 306 """return list of roots of nodes where descendants are covered by nodes"""
305 307 torev = repo.unfiltered().changelog.rev
306 308 revs = set(torev(n) for n in nodes)
307 309 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
308 310 # orphaned = affected - wanted
309 311 # affected = descendants(roots(wanted))
310 312 # wanted = revs
311 313 revset = '%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )'
312 314 tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs))
313 315 notstrip = revs - tostrip
314 316 if notstrip:
315 317 nodestr = ', '.join(sorted(short(repo[n].node()) for n in notstrip))
316 318 ui.warn(_('warning: orphaned descendants detected, '
317 319 'not stripping %s\n') % nodestr)
318 320 return [c.node() for c in repo.set('roots(%ld)', tostrip)]
319 321
320 322 class stripcallback(object):
321 323 """used as a transaction postclose callback"""
322 324
323 325 def __init__(self, ui, repo, backup, topic):
324 326 self.ui = ui
325 327 self.repo = repo
326 328 self.backup = backup
327 329 self.topic = topic or 'backup'
328 330 self.nodelist = []
329 331
330 332 def addnodes(self, nodes):
331 333 self.nodelist.extend(nodes)
332 334
333 335 def __call__(self, tr):
334 336 roots = safestriproots(self.ui, self.repo, self.nodelist)
335 337 if roots:
336 338 strip(self.ui, self.repo, roots, self.backup, self.topic)
337 339
338 340 def delayedstrip(ui, repo, nodelist, topic=None, backup=True):
339 341 """like strip, but works inside transaction and won't strip irreverent revs
340 342
341 343 nodelist must explicitly contain all descendants. Otherwise a warning will
342 344 be printed that some nodes are not stripped.
343 345
344 346 Will do a backup if `backup` is True. The last non-None "topic" will be
345 347 used as the backup topic name. The default backup topic name is "backup".
346 348 """
347 349 tr = repo.currenttransaction()
348 350 if not tr:
349 351 nodes = safestriproots(ui, repo, nodelist)
350 352 return strip(ui, repo, nodes, backup=backup, topic=topic)
351 353 # transaction postclose callbacks are called in alphabet order.
352 354 # use '\xff' as prefix so we are likely to be called last.
353 355 callback = tr.getpostclose('\xffstrip')
354 356 if callback is None:
355 357 callback = stripcallback(ui, repo, backup=backup, topic=topic)
356 358 tr.addpostclose('\xffstrip', callback)
357 359 if topic:
358 360 callback.topic = topic
359 361 callback.addnodes(nodelist)
360 362
361 363 def stripmanifest(repo, striprev, tr, files):
362 364 revlog = repo.manifestlog.getstorage(b'')
363 365 revlog.strip(striprev, tr)
364 366 striptrees(repo, tr, striprev, files)
365 367
366 368 def striptrees(repo, tr, striprev, files):
367 369 if 'treemanifest' in repo.requirements: # safe but unnecessary
368 370 # otherwise
369 371 for unencoded, encoded, size in repo.store.datafiles():
370 372 if (unencoded.startswith('meta/') and
371 373 unencoded.endswith('00manifest.i')):
372 374 dir = unencoded[5:-12]
373 375 repo.manifestlog.getstorage(dir).strip(striprev, tr)
374 376
375 377 def rebuildfncache(ui, repo):
376 378 """Rebuilds the fncache file from repo history.
377 379
378 380 Missing entries will be added. Extra entries will be removed.
379 381 """
380 382 repo = repo.unfiltered()
381 383
382 384 if 'fncache' not in repo.requirements:
383 385 ui.warn(_('(not rebuilding fncache because repository does not '
384 386 'support fncache)\n'))
385 387 return
386 388
387 389 with repo.lock():
388 390 fnc = repo.store.fncache
389 391 # Trigger load of fncache.
390 392 if 'irrelevant' in fnc:
391 393 pass
392 394
393 395 oldentries = set(fnc.entries)
394 396 newentries = set()
395 397 seenfiles = set()
396 398
397 399 progress = ui.makeprogress(_('rebuilding'), unit=_('changesets'),
398 400 total=len(repo))
399 401 for rev in repo:
400 402 progress.update(rev)
401 403
402 404 ctx = repo[rev]
403 405 for f in ctx.files():
404 406 # This is to minimize I/O.
405 407 if f in seenfiles:
406 408 continue
407 409 seenfiles.add(f)
408 410
409 411 i = 'data/%s.i' % f
410 412 d = 'data/%s.d' % f
411 413
412 414 if repo.store._exists(i):
413 415 newentries.add(i)
414 416 if repo.store._exists(d):
415 417 newentries.add(d)
416 418
417 419 progress.complete()
418 420
419 421 if 'treemanifest' in repo.requirements: # safe but unnecessary otherwise
420 422 for dir in util.dirs(seenfiles):
421 423 i = 'meta/%s/00manifest.i' % dir
422 424 d = 'meta/%s/00manifest.d' % dir
423 425
424 426 if repo.store._exists(i):
425 427 newentries.add(i)
426 428 if repo.store._exists(d):
427 429 newentries.add(d)
428 430
429 431 addcount = len(newentries - oldentries)
430 432 removecount = len(oldentries - newentries)
431 433 for p in sorted(oldentries - newentries):
432 434 ui.write(_('removing %s\n') % p)
433 435 for p in sorted(newentries - oldentries):
434 436 ui.write(_('adding %s\n') % p)
435 437
436 438 if addcount or removecount:
437 439 ui.write(_('%d items added, %d removed from fncache\n') %
438 440 (addcount, removecount))
439 441 fnc.entries = newentries
440 442 fnc._dirty = True
441 443
442 444 with repo.transaction('fncache') as tr:
443 445 fnc.write(tr)
444 446 else:
445 447 ui.write(_('fncache already up to date\n'))
446 448
447 449 def deleteobsmarkers(obsstore, indices):
448 450 """Delete some obsmarkers from obsstore and return how many were deleted
449 451
450 452 'indices' is a list of ints which are the indices
451 453 of the markers to be deleted.
452 454
453 455 Every invocation of this function completely rewrites the obsstore file,
454 456 skipping the markers we want to be removed. The new temporary file is
455 457 created, remaining markers are written there and on .close() this file
456 458 gets atomically renamed to obsstore, thus guaranteeing consistency."""
457 459 if not indices:
458 460 # we don't want to rewrite the obsstore with the same content
459 461 return
460 462
461 463 left = []
462 464 current = obsstore._all
463 465 n = 0
464 466 for i, m in enumerate(current):
465 467 if i in indices:
466 468 n += 1
467 469 continue
468 470 left.append(m)
469 471
470 472 newobsstorefile = obsstore.svfs('obsstore', 'w', atomictemp=True)
471 473 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
472 474 newobsstorefile.write(bytes)
473 475 newobsstorefile.close()
474 476 return n
General Comments 0
You need to be logged in to leave comments. Login now