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