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