##// END OF EJS Templates
transaction: change list of journal entries into a dictionary...
Joerg Sonnenberger -
r46483:a985c4fb default
parent child Browse files
Show More
@@ -1,541 +1,544 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
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 short,
17 17 )
18 18 from . import (
19 19 bundle2,
20 20 changegroup,
21 21 discovery,
22 22 error,
23 23 exchange,
24 24 obsolete,
25 25 obsutil,
26 26 pathutil,
27 27 phases,
28 28 pycompat,
29 29 requirements,
30 30 scmutil,
31 31 util,
32 32 )
33 33 from .utils import (
34 34 hashutil,
35 35 stringutil,
36 36 )
37 37
38 38
39 39 def backupbundle(
40 40 repo, bases, heads, node, suffix, compress=True, obsolescence=True
41 41 ):
42 42 """create a bundle with the specified revisions as a backup"""
43 43
44 44 backupdir = b"strip-backup"
45 45 vfs = repo.vfs
46 46 if not vfs.isdir(backupdir):
47 47 vfs.mkdir(backupdir)
48 48
49 49 # Include a hash of all the nodes in the filename for uniqueness
50 50 allcommits = repo.set(b'%ln::%ln', bases, heads)
51 51 allhashes = sorted(c.hex() for c in allcommits)
52 52 totalhash = hashutil.sha1(b''.join(allhashes)).digest()
53 53 name = b"%s/%s-%s-%s.hg" % (
54 54 backupdir,
55 55 short(node),
56 56 hex(totalhash[:4]),
57 57 suffix,
58 58 )
59 59
60 60 cgversion = changegroup.localversion(repo)
61 61 comp = None
62 62 if cgversion != b'01':
63 63 bundletype = b"HG20"
64 64 if compress:
65 65 comp = b'BZ'
66 66 elif compress:
67 67 bundletype = b"HG10BZ"
68 68 else:
69 69 bundletype = b"HG10UN"
70 70
71 71 outgoing = discovery.outgoing(repo, missingroots=bases, ancestorsof=heads)
72 72 contentopts = {
73 73 b'cg.version': cgversion,
74 74 b'obsolescence': obsolescence,
75 75 b'phases': True,
76 76 }
77 77 return bundle2.writenewbundle(
78 78 repo.ui,
79 79 repo,
80 80 b'strip',
81 81 name,
82 82 bundletype,
83 83 outgoing,
84 84 contentopts,
85 85 vfs,
86 86 compression=comp,
87 87 )
88 88
89 89
90 90 def _collectfiles(repo, striprev):
91 91 """find out the filelogs affected by the strip"""
92 92 files = set()
93 93
94 94 for x in pycompat.xrange(striprev, len(repo)):
95 95 files.update(repo[x].files())
96 96
97 97 return sorted(files)
98 98
99 99
100 100 def _collectrevlog(revlog, striprev):
101 101 _, brokenset = revlog.getstrippoint(striprev)
102 102 return [revlog.linkrev(r) for r in brokenset]
103 103
104 104
105 105 def _collectbrokencsets(repo, files, striprev):
106 106 """return the changesets which will be broken by the truncation"""
107 107 s = set()
108 108
109 109 for revlog in manifestrevlogs(repo):
110 110 s.update(_collectrevlog(revlog, striprev))
111 111 for fname in files:
112 112 s.update(_collectrevlog(repo.file(fname), striprev))
113 113
114 114 return s
115 115
116 116
117 117 def strip(ui, repo, nodelist, backup=True, topic=b'backup'):
118 118 # This function requires the caller to lock the repo, but it operates
119 119 # within a transaction of its own, and thus requires there to be no current
120 120 # transaction when it is called.
121 121 if repo.currenttransaction() is not None:
122 122 raise error.ProgrammingError(b'cannot strip from inside a transaction')
123 123
124 124 # Simple way to maintain backwards compatibility for this
125 125 # argument.
126 126 if backup in [b'none', b'strip']:
127 127 backup = False
128 128
129 129 repo = repo.unfiltered()
130 130 repo.destroying()
131 131 vfs = repo.vfs
132 132 # load bookmark before changelog to avoid side effect from outdated
133 133 # changelog (see repo._refreshchangelog)
134 134 repo._bookmarks
135 135 cl = repo.changelog
136 136
137 137 # TODO handle undo of merge sets
138 138 if isinstance(nodelist, bytes):
139 139 nodelist = [nodelist]
140 140 striplist = [cl.rev(node) for node in nodelist]
141 141 striprev = min(striplist)
142 142
143 143 files = _collectfiles(repo, striprev)
144 144 saverevs = _collectbrokencsets(repo, files, striprev)
145 145
146 146 # Some revisions with rev > striprev may not be descendants of striprev.
147 147 # We have to find these revisions and put them in a bundle, so that
148 148 # we can restore them after the truncations.
149 149 # To create the bundle we use repo.changegroupsubset which requires
150 150 # the list of heads and bases of the set of interesting revisions.
151 151 # (head = revision in the set that has no descendant in the set;
152 152 # base = revision in the set that has no ancestor in the set)
153 153 tostrip = set(striplist)
154 154 saveheads = set(saverevs)
155 155 for r in cl.revs(start=striprev + 1):
156 156 if any(p in tostrip for p in cl.parentrevs(r)):
157 157 tostrip.add(r)
158 158
159 159 if r not in tostrip:
160 160 saverevs.add(r)
161 161 saveheads.difference_update(cl.parentrevs(r))
162 162 saveheads.add(r)
163 163 saveheads = [cl.node(r) for r in saveheads]
164 164
165 165 # compute base nodes
166 166 if saverevs:
167 167 descendants = set(cl.descendants(saverevs))
168 168 saverevs.difference_update(descendants)
169 169 savebases = [cl.node(r) for r in saverevs]
170 170 stripbases = [cl.node(r) for r in tostrip]
171 171
172 172 stripobsidx = obsmarkers = ()
173 173 if repo.ui.configbool(b'devel', b'strip-obsmarkers'):
174 174 obsmarkers = obsutil.exclusivemarkers(repo, stripbases)
175 175 if obsmarkers:
176 176 stripobsidx = [
177 177 i for i, m in enumerate(repo.obsstore) if m in obsmarkers
178 178 ]
179 179
180 180 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
181 181
182 182 backupfile = None
183 183 node = nodelist[-1]
184 184 if backup:
185 185 backupfile = _createstripbackup(repo, stripbases, node, topic)
186 186 # create a changegroup for all the branches we need to keep
187 187 tmpbundlefile = None
188 188 if saveheads:
189 189 # do not compress temporary bundle if we remove it from disk later
190 190 #
191 191 # We do not include obsolescence, it might re-introduce prune markers
192 192 # we are trying to strip. This is harmless since the stripped markers
193 193 # are already backed up and we did not touched the markers for the
194 194 # saved changesets.
195 195 tmpbundlefile = backupbundle(
196 196 repo,
197 197 savebases,
198 198 saveheads,
199 199 node,
200 200 b'temp',
201 201 compress=False,
202 202 obsolescence=False,
203 203 )
204 204
205 205 with ui.uninterruptible():
206 206 try:
207 207 with repo.transaction(b"strip") as tr:
208 208 # TODO this code violates the interface abstraction of the
209 209 # transaction and makes assumptions that file storage is
210 210 # using append-only files. We'll need some kind of storage
211 211 # API to handle stripping for us.
212 offset = len(tr._entries)
212 oldfiles = set(tr._offsetmap.keys())
213 213
214 214 tr.startgroup()
215 215 cl.strip(striprev, tr)
216 216 stripmanifest(repo, striprev, tr, files)
217 217
218 218 for fn in files:
219 219 repo.file(fn).strip(striprev, tr)
220 220 tr.endgroup()
221 221
222 for i in pycompat.xrange(offset, len(tr._entries)):
223 file, troffset = tr._entries[i]
222 entries = tr.readjournal()
223
224 for file, troffset in entries:
225 if file in oldfiles:
226 continue
224 227 with repo.svfs(file, b'a', checkambig=True) as fp:
225 228 fp.truncate(troffset)
226 229 if troffset == 0:
227 230 repo.store.markremoved(file)
228 231
229 232 deleteobsmarkers(repo.obsstore, stripobsidx)
230 233 del repo.obsstore
231 234 repo.invalidatevolatilesets()
232 235 repo._phasecache.filterunknown(repo)
233 236
234 237 if tmpbundlefile:
235 238 ui.note(_(b"adding branch\n"))
236 239 f = vfs.open(tmpbundlefile, b"rb")
237 240 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
238 241 if not repo.ui.verbose:
239 242 # silence internal shuffling chatter
240 243 repo.ui.pushbuffer()
241 244 tmpbundleurl = b'bundle:' + vfs.join(tmpbundlefile)
242 245 txnname = b'strip'
243 246 if not isinstance(gen, bundle2.unbundle20):
244 247 txnname = b"strip\n%s" % util.hidepassword(tmpbundleurl)
245 248 with repo.transaction(txnname) as tr:
246 249 bundle2.applybundle(
247 250 repo, gen, tr, source=b'strip', url=tmpbundleurl
248 251 )
249 252 if not repo.ui.verbose:
250 253 repo.ui.popbuffer()
251 254 f.close()
252 255
253 256 with repo.transaction(b'repair') as tr:
254 257 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
255 258 repo._bookmarks.applychanges(repo, tr, bmchanges)
256 259
257 260 # remove undo files
258 261 for undovfs, undofile in repo.undofiles():
259 262 try:
260 263 undovfs.unlink(undofile)
261 264 except OSError as e:
262 265 if e.errno != errno.ENOENT:
263 266 ui.warn(
264 267 _(b'error removing %s: %s\n')
265 268 % (
266 269 undovfs.join(undofile),
267 270 stringutil.forcebytestr(e),
268 271 )
269 272 )
270 273
271 274 except: # re-raises
272 275 if backupfile:
273 276 ui.warn(
274 277 _(b"strip failed, backup bundle stored in '%s'\n")
275 278 % vfs.join(backupfile)
276 279 )
277 280 if tmpbundlefile:
278 281 ui.warn(
279 282 _(b"strip failed, unrecovered changes stored in '%s'\n")
280 283 % vfs.join(tmpbundlefile)
281 284 )
282 285 ui.warn(
283 286 _(
284 287 b"(fix the problem, then recover the changesets with "
285 288 b"\"hg unbundle '%s'\")\n"
286 289 )
287 290 % vfs.join(tmpbundlefile)
288 291 )
289 292 raise
290 293 else:
291 294 if tmpbundlefile:
292 295 # Remove temporary bundle only if there were no exceptions
293 296 vfs.unlink(tmpbundlefile)
294 297
295 298 repo.destroyed()
296 299 # return the backup file path (or None if 'backup' was False) so
297 300 # extensions can use it
298 301 return backupfile
299 302
300 303
301 304 def softstrip(ui, repo, nodelist, backup=True, topic=b'backup'):
302 305 """perform a "soft" strip using the archived phase"""
303 306 tostrip = [c.node() for c in repo.set(b'sort(%ln::)', nodelist)]
304 307 if not tostrip:
305 308 return None
306 309
307 310 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
308 311 if backup:
309 312 node = tostrip[0]
310 313 backupfile = _createstripbackup(repo, tostrip, node, topic)
311 314
312 315 with repo.transaction(b'strip') as tr:
313 316 phases.retractboundary(repo, tr, phases.archived, tostrip)
314 317 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
315 318 repo._bookmarks.applychanges(repo, tr, bmchanges)
316 319 return backupfile
317 320
318 321
319 322 def _bookmarkmovements(repo, tostrip):
320 323 # compute necessary bookmark movement
321 324 bm = repo._bookmarks
322 325 updatebm = []
323 326 for m in bm:
324 327 rev = repo[bm[m]].rev()
325 328 if rev in tostrip:
326 329 updatebm.append(m)
327 330 newbmtarget = None
328 331 # If we need to move bookmarks, compute bookmark
329 332 # targets. Otherwise we can skip doing this logic.
330 333 if updatebm:
331 334 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)),
332 335 # but is much faster
333 336 newbmtarget = repo.revs(b'max(parents(%ld) - (%ld))', tostrip, tostrip)
334 337 if newbmtarget:
335 338 newbmtarget = repo[newbmtarget.first()].node()
336 339 else:
337 340 newbmtarget = b'.'
338 341 return newbmtarget, updatebm
339 342
340 343
341 344 def _createstripbackup(repo, stripbases, node, topic):
342 345 # backup the changeset we are about to strip
343 346 vfs = repo.vfs
344 347 cl = repo.changelog
345 348 backupfile = backupbundle(repo, stripbases, cl.heads(), node, topic)
346 349 repo.ui.status(_(b"saved backup bundle to %s\n") % vfs.join(backupfile))
347 350 repo.ui.log(
348 351 b"backupbundle", b"saved backup bundle to %s\n", vfs.join(backupfile)
349 352 )
350 353 return backupfile
351 354
352 355
353 356 def safestriproots(ui, repo, nodes):
354 357 """return list of roots of nodes where descendants are covered by nodes"""
355 358 torev = repo.unfiltered().changelog.rev
356 359 revs = {torev(n) for n in nodes}
357 360 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
358 361 # orphaned = affected - wanted
359 362 # affected = descendants(roots(wanted))
360 363 # wanted = revs
361 364 revset = b'%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )'
362 365 tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs))
363 366 notstrip = revs - tostrip
364 367 if notstrip:
365 368 nodestr = b', '.join(sorted(short(repo[n].node()) for n in notstrip))
366 369 ui.warn(
367 370 _(b'warning: orphaned descendants detected, not stripping %s\n')
368 371 % nodestr
369 372 )
370 373 return [c.node() for c in repo.set(b'roots(%ld)', tostrip)]
371 374
372 375
373 376 class stripcallback(object):
374 377 """used as a transaction postclose callback"""
375 378
376 379 def __init__(self, ui, repo, backup, topic):
377 380 self.ui = ui
378 381 self.repo = repo
379 382 self.backup = backup
380 383 self.topic = topic or b'backup'
381 384 self.nodelist = []
382 385
383 386 def addnodes(self, nodes):
384 387 self.nodelist.extend(nodes)
385 388
386 389 def __call__(self, tr):
387 390 roots = safestriproots(self.ui, self.repo, self.nodelist)
388 391 if roots:
389 392 strip(self.ui, self.repo, roots, self.backup, self.topic)
390 393
391 394
392 395 def delayedstrip(ui, repo, nodelist, topic=None, backup=True):
393 396 """like strip, but works inside transaction and won't strip irreverent revs
394 397
395 398 nodelist must explicitly contain all descendants. Otherwise a warning will
396 399 be printed that some nodes are not stripped.
397 400
398 401 Will do a backup if `backup` is True. The last non-None "topic" will be
399 402 used as the backup topic name. The default backup topic name is "backup".
400 403 """
401 404 tr = repo.currenttransaction()
402 405 if not tr:
403 406 nodes = safestriproots(ui, repo, nodelist)
404 407 return strip(ui, repo, nodes, backup=backup, topic=topic)
405 408 # transaction postclose callbacks are called in alphabet order.
406 409 # use '\xff' as prefix so we are likely to be called last.
407 410 callback = tr.getpostclose(b'\xffstrip')
408 411 if callback is None:
409 412 callback = stripcallback(ui, repo, backup=backup, topic=topic)
410 413 tr.addpostclose(b'\xffstrip', callback)
411 414 if topic:
412 415 callback.topic = topic
413 416 callback.addnodes(nodelist)
414 417
415 418
416 419 def stripmanifest(repo, striprev, tr, files):
417 420 for revlog in manifestrevlogs(repo):
418 421 revlog.strip(striprev, tr)
419 422
420 423
421 424 def manifestrevlogs(repo):
422 425 yield repo.manifestlog.getstorage(b'')
423 426 if scmutil.istreemanifest(repo):
424 427 # This logic is safe if treemanifest isn't enabled, but also
425 428 # pointless, so we skip it if treemanifest isn't enabled.
426 429 for unencoded, encoded, size in repo.store.datafiles():
427 430 if unencoded.startswith(b'meta/') and unencoded.endswith(
428 431 b'00manifest.i'
429 432 ):
430 433 dir = unencoded[5:-12]
431 434 yield repo.manifestlog.getstorage(dir)
432 435
433 436
434 437 def rebuildfncache(ui, repo):
435 438 """Rebuilds the fncache file from repo history.
436 439
437 440 Missing entries will be added. Extra entries will be removed.
438 441 """
439 442 repo = repo.unfiltered()
440 443
441 444 if b'fncache' not in repo.requirements:
442 445 ui.warn(
443 446 _(
444 447 b'(not rebuilding fncache because repository does not '
445 448 b'support fncache)\n'
446 449 )
447 450 )
448 451 return
449 452
450 453 with repo.lock():
451 454 fnc = repo.store.fncache
452 455 fnc.ensureloaded(warn=ui.warn)
453 456
454 457 oldentries = set(fnc.entries)
455 458 newentries = set()
456 459 seenfiles = set()
457 460
458 461 progress = ui.makeprogress(
459 462 _(b'rebuilding'), unit=_(b'changesets'), total=len(repo)
460 463 )
461 464 for rev in repo:
462 465 progress.update(rev)
463 466
464 467 ctx = repo[rev]
465 468 for f in ctx.files():
466 469 # This is to minimize I/O.
467 470 if f in seenfiles:
468 471 continue
469 472 seenfiles.add(f)
470 473
471 474 i = b'data/%s.i' % f
472 475 d = b'data/%s.d' % f
473 476
474 477 if repo.store._exists(i):
475 478 newentries.add(i)
476 479 if repo.store._exists(d):
477 480 newentries.add(d)
478 481
479 482 progress.complete()
480 483
481 484 if requirements.TREEMANIFEST_REQUIREMENT in repo.requirements:
482 485 # This logic is safe if treemanifest isn't enabled, but also
483 486 # pointless, so we skip it if treemanifest isn't enabled.
484 487 for dir in pathutil.dirs(seenfiles):
485 488 i = b'meta/%s/00manifest.i' % dir
486 489 d = b'meta/%s/00manifest.d' % dir
487 490
488 491 if repo.store._exists(i):
489 492 newentries.add(i)
490 493 if repo.store._exists(d):
491 494 newentries.add(d)
492 495
493 496 addcount = len(newentries - oldentries)
494 497 removecount = len(oldentries - newentries)
495 498 for p in sorted(oldentries - newentries):
496 499 ui.write(_(b'removing %s\n') % p)
497 500 for p in sorted(newentries - oldentries):
498 501 ui.write(_(b'adding %s\n') % p)
499 502
500 503 if addcount or removecount:
501 504 ui.write(
502 505 _(b'%d items added, %d removed from fncache\n')
503 506 % (addcount, removecount)
504 507 )
505 508 fnc.entries = newentries
506 509 fnc._dirty = True
507 510
508 511 with repo.transaction(b'fncache') as tr:
509 512 fnc.write(tr)
510 513 else:
511 514 ui.write(_(b'fncache already up to date\n'))
512 515
513 516
514 517 def deleteobsmarkers(obsstore, indices):
515 518 """Delete some obsmarkers from obsstore and return how many were deleted
516 519
517 520 'indices' is a list of ints which are the indices
518 521 of the markers to be deleted.
519 522
520 523 Every invocation of this function completely rewrites the obsstore file,
521 524 skipping the markers we want to be removed. The new temporary file is
522 525 created, remaining markers are written there and on .close() this file
523 526 gets atomically renamed to obsstore, thus guaranteeing consistency."""
524 527 if not indices:
525 528 # we don't want to rewrite the obsstore with the same content
526 529 return
527 530
528 531 left = []
529 532 current = obsstore._all
530 533 n = 0
531 534 for i, m in enumerate(current):
532 535 if i in indices:
533 536 n += 1
534 537 continue
535 538 left.append(m)
536 539
537 540 newobsstorefile = obsstore.svfs(b'obsstore', b'w', atomictemp=True)
538 541 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
539 542 newobsstorefile.write(bytes)
540 543 newobsstorefile.close()
541 544 return n
@@ -1,732 +1,737 b''
1 1 # transaction.py - simple journaling scheme for mercurial
2 2 #
3 3 # This transaction scheme is intended to gracefully handle program
4 4 # errors and interruptions. More serious failures like system crashes
5 5 # can be recovered with an fsck-like tool. As the whole repository is
6 6 # effectively log-structured, this should amount to simply truncating
7 7 # anything that isn't referenced in the changelog.
8 8 #
9 9 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
10 10 #
11 11 # This software may be used and distributed according to the terms of the
12 12 # GNU General Public License version 2 or any later version.
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import errno
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 error,
21 21 pycompat,
22 22 util,
23 23 )
24 24 from .utils import stringutil
25 25
26 26 version = 2
27 27
28 28 # These are the file generators that should only be executed after the
29 29 # finalizers are done, since they rely on the output of the finalizers (like
30 30 # the changelog having been written).
31 31 postfinalizegenerators = {b'bookmarks', b'dirstate'}
32 32
33 33 GEN_GROUP_ALL = b'all'
34 34 GEN_GROUP_PRE_FINALIZE = b'prefinalize'
35 35 GEN_GROUP_POST_FINALIZE = b'postfinalize'
36 36
37 37
38 38 def active(func):
39 39 def _active(self, *args, **kwds):
40 40 if self._count == 0:
41 41 raise error.ProgrammingError(
42 42 b'cannot use transaction when it is already committed/aborted'
43 43 )
44 44 return func(self, *args, **kwds)
45 45
46 46 return _active
47 47
48 48
49 49 def _playback(
50 50 journal,
51 51 report,
52 52 opener,
53 53 vfsmap,
54 54 entries,
55 55 backupentries,
56 56 unlink=True,
57 57 checkambigfiles=None,
58 58 ):
59 59 for f, o in entries:
60 60 if o or not unlink:
61 61 checkambig = checkambigfiles and (f, b'') in checkambigfiles
62 62 try:
63 63 fp = opener(f, b'a', checkambig=checkambig)
64 64 if fp.tell() < o:
65 65 raise error.Abort(
66 66 _(
67 67 b"attempted to truncate %s to %d bytes, but it was "
68 68 b"already %d bytes\n"
69 69 )
70 70 % (f, o, fp.tell())
71 71 )
72 72 fp.truncate(o)
73 73 fp.close()
74 74 except IOError:
75 75 report(_(b"failed to truncate %s\n") % f)
76 76 raise
77 77 else:
78 78 try:
79 79 opener.unlink(f)
80 80 except (IOError, OSError) as inst:
81 81 if inst.errno != errno.ENOENT:
82 82 raise
83 83
84 84 backupfiles = []
85 85 for l, f, b, c in backupentries:
86 86 if l not in vfsmap and c:
87 87 report(b"couldn't handle %s: unknown cache location %s\n" % (b, l))
88 88 vfs = vfsmap[l]
89 89 try:
90 90 if f and b:
91 91 filepath = vfs.join(f)
92 92 backuppath = vfs.join(b)
93 93 checkambig = checkambigfiles and (f, l) in checkambigfiles
94 94 try:
95 95 util.copyfile(backuppath, filepath, checkambig=checkambig)
96 96 backupfiles.append(b)
97 97 except IOError:
98 98 report(_(b"failed to recover %s\n") % f)
99 99 else:
100 100 target = f or b
101 101 try:
102 102 vfs.unlink(target)
103 103 except (IOError, OSError) as inst:
104 104 if inst.errno != errno.ENOENT:
105 105 raise
106 106 except (IOError, OSError, error.Abort):
107 107 if not c:
108 108 raise
109 109
110 110 backuppath = b"%s.backupfiles" % journal
111 111 if opener.exists(backuppath):
112 112 opener.unlink(backuppath)
113 113 opener.unlink(journal)
114 114 try:
115 115 for f in backupfiles:
116 116 if opener.exists(f):
117 117 opener.unlink(f)
118 118 except (IOError, OSError, error.Abort):
119 119 # only pure backup file remains, it is sage to ignore any error
120 120 pass
121 121
122 122
123 123 class transaction(util.transactional):
124 124 def __init__(
125 125 self,
126 126 report,
127 127 opener,
128 128 vfsmap,
129 129 journalname,
130 130 undoname=None,
131 131 after=None,
132 132 createmode=None,
133 133 validator=None,
134 134 releasefn=None,
135 135 checkambigfiles=None,
136 136 name='<unnamed>',
137 137 ):
138 138 """Begin a new transaction
139 139
140 140 Begins a new transaction that allows rolling back writes in the event of
141 141 an exception.
142 142
143 143 * `after`: called after the transaction has been committed
144 144 * `createmode`: the mode of the journal file that will be created
145 145 * `releasefn`: called after releasing (with transaction and result)
146 146
147 147 `checkambigfiles` is a set of (path, vfs-location) tuples,
148 148 which determine whether file stat ambiguity should be avoided
149 149 for corresponded files.
150 150 """
151 151 self._count = 1
152 152 self._usages = 1
153 153 self._report = report
154 154 # a vfs to the store content
155 155 self._opener = opener
156 156 # a map to access file in various {location -> vfs}
157 157 vfsmap = vfsmap.copy()
158 158 vfsmap[b''] = opener # set default value
159 159 self._vfsmap = vfsmap
160 160 self._after = after
161 self._entries = []
162 self._map = {}
161 self._offsetmap = {}
163 162 self._journal = journalname
164 163 self._undoname = undoname
165 164 self._queue = []
166 165 # A callback to do something just after releasing transaction.
167 166 if releasefn is None:
168 167 releasefn = lambda tr, success: None
169 168 self._releasefn = releasefn
170 169
171 170 self._checkambigfiles = set()
172 171 if checkambigfiles:
173 172 self._checkambigfiles.update(checkambigfiles)
174 173
175 174 self._names = [name]
176 175
177 176 # A dict dedicated to precisely tracking the changes introduced in the
178 177 # transaction.
179 178 self.changes = {}
180 179
181 180 # a dict of arguments to be passed to hooks
182 181 self.hookargs = {}
183 self._file = opener.open(self._journal, b"w")
182 self._file = opener.open(self._journal, b"w+")
184 183
185 184 # a list of ('location', 'path', 'backuppath', cache) entries.
186 185 # - if 'backuppath' is empty, no file existed at backup time
187 186 # - if 'path' is empty, this is a temporary transaction file
188 187 # - if 'location' is not empty, the path is outside main opener reach.
189 188 # use 'location' value as a key in a vfsmap to find the right 'vfs'
190 189 # (cache is currently unused)
191 190 self._backupentries = []
192 191 self._backupmap = {}
193 192 self._backupjournal = b"%s.backupfiles" % self._journal
194 193 self._backupsfile = opener.open(self._backupjournal, b'w')
195 194 self._backupsfile.write(b'%d\n' % version)
196 195
197 196 if createmode is not None:
198 197 opener.chmod(self._journal, createmode & 0o666)
199 198 opener.chmod(self._backupjournal, createmode & 0o666)
200 199
201 200 # hold file generations to be performed on commit
202 201 self._filegenerators = {}
203 202 # hold callback to write pending data for hooks
204 203 self._pendingcallback = {}
205 204 # True is any pending data have been written ever
206 205 self._anypending = False
207 206 # holds callback to call when writing the transaction
208 207 self._finalizecallback = {}
209 208 # holds callback to call when validating the transaction
210 209 # should raise exception if anything is wrong
211 210 self._validatecallback = {}
212 211 if validator is not None:
213 212 self._validatecallback[b'001-userhooks'] = validator
214 213 # hold callback for post transaction close
215 214 self._postclosecallback = {}
216 215 # holds callbacks to call during abort
217 216 self._abortcallback = {}
218 217
219 218 def __repr__(self):
220 219 name = '/'.join(self._names)
221 220 return '<transaction name=%s, count=%d, usages=%d>' % (
222 221 name,
223 222 self._count,
224 223 self._usages,
225 224 )
226 225
227 226 def __del__(self):
228 227 if self._journal:
229 228 self._abort()
230 229
231 230 @active
232 231 def startgroup(self):
233 232 """delay registration of file entry
234 233
235 234 This is used by strip to delay vision of strip offset. The transaction
236 235 sees either none or all of the strip actions to be done."""
237 236 self._queue.append([])
238 237
239 238 @active
240 239 def endgroup(self):
241 240 """apply delayed registration of file entry.
242 241
243 242 This is used by strip to delay vision of strip offset. The transaction
244 243 sees either none or all of the strip actions to be done."""
245 244 q = self._queue.pop()
246 245 for f, o in q:
247 246 self._addentry(f, o)
248 247
249 248 @active
250 249 def add(self, file, offset):
251 250 """record the state of an append-only file before update"""
252 if file in self._map or file in self._backupmap:
251 if file in self._offsetmap or file in self._backupmap:
253 252 return
254 253 if self._queue:
255 254 self._queue[-1].append((file, offset))
256 255 return
257 256
258 257 self._addentry(file, offset)
259 258
260 259 def _addentry(self, file, offset):
261 260 """add a append-only entry to memory and on-disk state"""
262 if file in self._map or file in self._backupmap:
261 if file in self._offsetmap or file in self._backupmap:
263 262 return
264 self._entries.append((file, offset))
265 self._map[file] = len(self._entries) - 1
263 self._offsetmap[file] = offset
266 264 # add enough data to the journal to do the truncate
267 265 self._file.write(b"%s\0%d\n" % (file, offset))
268 266 self._file.flush()
269 267
270 268 @active
271 269 def addbackup(self, file, hardlink=True, location=b''):
272 270 """Adds a backup of the file to the transaction
273 271
274 272 Calling addbackup() creates a hardlink backup of the specified file
275 273 that is used to recover the file in the event of the transaction
276 274 aborting.
277 275
278 276 * `file`: the file path, relative to .hg/store
279 277 * `hardlink`: use a hardlink to quickly create the backup
280 278 """
281 279 if self._queue:
282 280 msg = b'cannot use transaction.addbackup inside "group"'
283 281 raise error.ProgrammingError(msg)
284 282
285 if file in self._map or file in self._backupmap:
283 if file in self._offsetmap or file in self._backupmap:
286 284 return
287 285 vfs = self._vfsmap[location]
288 286 dirname, filename = vfs.split(file)
289 287 backupfilename = b"%s.backup.%s" % (self._journal, filename)
290 288 backupfile = vfs.reljoin(dirname, backupfilename)
291 289 if vfs.exists(file):
292 290 filepath = vfs.join(file)
293 291 backuppath = vfs.join(backupfile)
294 292 util.copyfile(filepath, backuppath, hardlink=hardlink)
295 293 else:
296 294 backupfile = b''
297 295
298 296 self._addbackupentry((location, file, backupfile, False))
299 297
300 298 def _addbackupentry(self, entry):
301 299 """register a new backup entry and write it to disk"""
302 300 self._backupentries.append(entry)
303 301 self._backupmap[entry[1]] = len(self._backupentries) - 1
304 302 self._backupsfile.write(b"%s\0%s\0%s\0%d\n" % entry)
305 303 self._backupsfile.flush()
306 304
307 305 @active
308 306 def registertmp(self, tmpfile, location=b''):
309 307 """register a temporary transaction file
310 308
311 309 Such files will be deleted when the transaction exits (on both
312 310 failure and success).
313 311 """
314 312 self._addbackupentry((location, b'', tmpfile, False))
315 313
316 314 @active
317 315 def addfilegenerator(
318 316 self, genid, filenames, genfunc, order=0, location=b''
319 317 ):
320 318 """add a function to generates some files at transaction commit
321 319
322 320 The `genfunc` argument is a function capable of generating proper
323 321 content of each entry in the `filename` tuple.
324 322
325 323 At transaction close time, `genfunc` will be called with one file
326 324 object argument per entries in `filenames`.
327 325
328 326 The transaction itself is responsible for the backup, creation and
329 327 final write of such file.
330 328
331 329 The `genid` argument is used to ensure the same set of file is only
332 330 generated once. Call to `addfilegenerator` for a `genid` already
333 331 present will overwrite the old entry.
334 332
335 333 The `order` argument may be used to control the order in which multiple
336 334 generator will be executed.
337 335
338 336 The `location` arguments may be used to indicate the files are located
339 337 outside of the the standard directory for transaction. It should match
340 338 one of the key of the `transaction.vfsmap` dictionary.
341 339 """
342 340 # For now, we are unable to do proper backup and restore of custom vfs
343 341 # but for bookmarks that are handled outside this mechanism.
344 342 self._filegenerators[genid] = (order, filenames, genfunc, location)
345 343
346 344 @active
347 345 def removefilegenerator(self, genid):
348 346 """reverse of addfilegenerator, remove a file generator function"""
349 347 if genid in self._filegenerators:
350 348 del self._filegenerators[genid]
351 349
352 350 def _generatefiles(self, suffix=b'', group=GEN_GROUP_ALL):
353 351 # write files registered for generation
354 352 any = False
355 353
356 354 if group == GEN_GROUP_ALL:
357 355 skip_post = skip_pre = False
358 356 else:
359 357 skip_pre = group == GEN_GROUP_POST_FINALIZE
360 358 skip_post = group == GEN_GROUP_PRE_FINALIZE
361 359
362 360 for id, entry in sorted(pycompat.iteritems(self._filegenerators)):
363 361 any = True
364 362 order, filenames, genfunc, location = entry
365 363
366 364 # for generation at closing, check if it's before or after finalize
367 365 is_post = id in postfinalizegenerators
368 366 if skip_post and is_post:
369 367 continue
370 368 elif skip_pre and not is_post:
371 369 continue
372 370
373 371 vfs = self._vfsmap[location]
374 372 files = []
375 373 try:
376 374 for name in filenames:
377 375 name += suffix
378 376 if suffix:
379 377 self.registertmp(name, location=location)
380 378 checkambig = False
381 379 else:
382 380 self.addbackup(name, location=location)
383 381 checkambig = (name, location) in self._checkambigfiles
384 382 files.append(
385 383 vfs(name, b'w', atomictemp=True, checkambig=checkambig)
386 384 )
387 385 genfunc(*files)
388 386 for f in files:
389 387 f.close()
390 388 # skip discard() loop since we're sure no open file remains
391 389 del files[:]
392 390 finally:
393 391 for f in files:
394 392 f.discard()
395 393 return any
396 394
397 395 @active
398 396 def findoffset(self, file):
399 if file in self._map:
400 return self._entries[self._map[file]][1]
401 return None
397 return self._offsetmap.get(file)
398
399 @active
400 def readjournal(self):
401 self._file.seek(0)
402 entries = []
403 for l in self._file:
404 file, troffset = l.split(b'\0')
405 entries.append((file, int(troffset)))
406 return entries
402 407
403 408 @active
404 409 def replace(self, file, offset):
405 410 '''
406 411 replace can only replace already committed entries
407 412 that are not pending in the queue
408 413 '''
409 414
410 if file not in self._map:
415 if file not in self._offsetmap:
411 416 raise KeyError(file)
412 index = self._map[file]
413 self._entries[index] = (file, offset)
417 self._offsetmap[file] = offset
414 418 self._file.write(b"%s\0%d\n" % (file, offset))
415 419 self._file.flush()
416 420
417 421 @active
418 422 def nest(self, name='<unnamed>'):
419 423 self._count += 1
420 424 self._usages += 1
421 425 self._names.append(name)
422 426 return self
423 427
424 428 def release(self):
425 429 if self._count > 0:
426 430 self._usages -= 1
427 431 if self._names:
428 432 self._names.pop()
429 433 # if the transaction scopes are left without being closed, fail
430 434 if self._count > 0 and self._usages == 0:
431 435 self._abort()
432 436
433 437 def running(self):
434 438 return self._count > 0
435 439
436 440 def addpending(self, category, callback):
437 441 """add a callback to be called when the transaction is pending
438 442
439 443 The transaction will be given as callback's first argument.
440 444
441 445 Category is a unique identifier to allow overwriting an old callback
442 446 with a newer callback.
443 447 """
444 448 self._pendingcallback[category] = callback
445 449
446 450 @active
447 451 def writepending(self):
448 452 '''write pending file to temporary version
449 453
450 454 This is used to allow hooks to view a transaction before commit'''
451 455 categories = sorted(self._pendingcallback)
452 456 for cat in categories:
453 457 # remove callback since the data will have been flushed
454 458 any = self._pendingcallback.pop(cat)(self)
455 459 self._anypending = self._anypending or any
456 460 self._anypending |= self._generatefiles(suffix=b'.pending')
457 461 return self._anypending
458 462
459 463 @active
460 464 def hasfinalize(self, category):
461 465 """check is a callback already exist for a category
462 466 """
463 467 return category in self._finalizecallback
464 468
465 469 @active
466 470 def addfinalize(self, category, callback):
467 471 """add a callback to be called when the transaction is closed
468 472
469 473 The transaction will be given as callback's first argument.
470 474
471 475 Category is a unique identifier to allow overwriting old callbacks with
472 476 newer callbacks.
473 477 """
474 478 self._finalizecallback[category] = callback
475 479
476 480 @active
477 481 def addpostclose(self, category, callback):
478 482 """add or replace a callback to be called after the transaction closed
479 483
480 484 The transaction will be given as callback's first argument.
481 485
482 486 Category is a unique identifier to allow overwriting an old callback
483 487 with a newer callback.
484 488 """
485 489 self._postclosecallback[category] = callback
486 490
487 491 @active
488 492 def getpostclose(self, category):
489 493 """return a postclose callback added before, or None"""
490 494 return self._postclosecallback.get(category, None)
491 495
492 496 @active
493 497 def addabort(self, category, callback):
494 498 """add a callback to be called when the transaction is aborted.
495 499
496 500 The transaction will be given as the first argument to the callback.
497 501
498 502 Category is a unique identifier to allow overwriting an old callback
499 503 with a newer callback.
500 504 """
501 505 self._abortcallback[category] = callback
502 506
503 507 @active
504 508 def addvalidator(self, category, callback):
505 509 """ adds a callback to be called when validating the transaction.
506 510
507 511 The transaction will be given as the first argument to the callback.
508 512
509 513 callback should raise exception if to abort transaction """
510 514 self._validatecallback[category] = callback
511 515
512 516 @active
513 517 def close(self):
514 518 '''commit the transaction'''
515 519 if self._count == 1:
516 520 for category in sorted(self._validatecallback):
517 521 self._validatecallback[category](self)
518 522 self._validatecallback = None # Help prevent cycles.
519 523 self._generatefiles(group=GEN_GROUP_PRE_FINALIZE)
520 524 while self._finalizecallback:
521 525 callbacks = self._finalizecallback
522 526 self._finalizecallback = {}
523 527 categories = sorted(callbacks)
524 528 for cat in categories:
525 529 callbacks[cat](self)
526 530 # Prevent double usage and help clear cycles.
527 531 self._finalizecallback = None
528 532 self._generatefiles(group=GEN_GROUP_POST_FINALIZE)
529 533
530 534 self._count -= 1
531 535 if self._count != 0:
532 536 return
533 537 self._file.close()
534 538 self._backupsfile.close()
535 539 # cleanup temporary files
536 540 for l, f, b, c in self._backupentries:
537 541 if l not in self._vfsmap and c:
538 542 self._report(
539 543 b"couldn't remove %s: unknown cache location %s\n" % (b, l)
540 544 )
541 545 continue
542 546 vfs = self._vfsmap[l]
543 547 if not f and b and vfs.exists(b):
544 548 try:
545 549 vfs.unlink(b)
546 550 except (IOError, OSError, error.Abort) as inst:
547 551 if not c:
548 552 raise
549 553 # Abort may be raise by read only opener
550 554 self._report(
551 555 b"couldn't remove %s: %s\n" % (vfs.join(b), inst)
552 556 )
553 self._entries = []
557 self._offsetmap = {}
554 558 self._writeundo()
555 559 if self._after:
556 560 self._after()
557 561 self._after = None # Help prevent cycles.
558 562 if self._opener.isfile(self._backupjournal):
559 563 self._opener.unlink(self._backupjournal)
560 564 if self._opener.isfile(self._journal):
561 565 self._opener.unlink(self._journal)
562 566 for l, _f, b, c in self._backupentries:
563 567 if l not in self._vfsmap and c:
564 568 self._report(
565 569 b"couldn't remove %s: unknown cache location"
566 570 b"%s\n" % (b, l)
567 571 )
568 572 continue
569 573 vfs = self._vfsmap[l]
570 574 if b and vfs.exists(b):
571 575 try:
572 576 vfs.unlink(b)
573 577 except (IOError, OSError, error.Abort) as inst:
574 578 if not c:
575 579 raise
576 580 # Abort may be raise by read only opener
577 581 self._report(
578 582 b"couldn't remove %s: %s\n" % (vfs.join(b), inst)
579 583 )
580 584 self._backupentries = []
581 585 self._journal = None
582 586
583 587 self._releasefn(self, True) # notify success of closing transaction
584 588 self._releasefn = None # Help prevent cycles.
585 589
586 590 # run post close action
587 591 categories = sorted(self._postclosecallback)
588 592 for cat in categories:
589 593 self._postclosecallback[cat](self)
590 594 # Prevent double usage and help clear cycles.
591 595 self._postclosecallback = None
592 596
593 597 @active
594 598 def abort(self):
595 599 '''abort the transaction (generally called on error, or when the
596 600 transaction is not explicitly committed before going out of
597 601 scope)'''
598 602 self._abort()
599 603
600 604 def _writeundo(self):
601 605 """write transaction data for possible future undo call"""
602 606 if self._undoname is None:
603 607 return
604 608 undobackupfile = self._opener.open(
605 609 b"%s.backupfiles" % self._undoname, b'w'
606 610 )
607 611 undobackupfile.write(b'%d\n' % version)
608 612 for l, f, b, c in self._backupentries:
609 613 if not f: # temporary file
610 614 continue
611 615 if not b:
612 616 u = b''
613 617 else:
614 618 if l not in self._vfsmap and c:
615 619 self._report(
616 620 b"couldn't remove %s: unknown cache location"
617 621 b"%s\n" % (b, l)
618 622 )
619 623 continue
620 624 vfs = self._vfsmap[l]
621 625 base, name = vfs.split(b)
622 626 assert name.startswith(self._journal), name
623 627 uname = name.replace(self._journal, self._undoname, 1)
624 628 u = vfs.reljoin(base, uname)
625 629 util.copyfile(vfs.join(b), vfs.join(u), hardlink=True)
626 630 undobackupfile.write(b"%s\0%s\0%s\0%d\n" % (l, f, u, c))
627 631 undobackupfile.close()
628 632
629 633 def _abort(self):
634 entries = self.readjournal()
630 635 self._count = 0
631 636 self._usages = 0
632 637 self._file.close()
633 638 self._backupsfile.close()
634 639
635 640 try:
636 if not self._entries and not self._backupentries:
641 if not self._offsetmap and not self._backupentries:
637 642 if self._backupjournal:
638 643 self._opener.unlink(self._backupjournal)
639 644 if self._journal:
640 645 self._opener.unlink(self._journal)
641 646 return
642 647
643 648 self._report(_(b"transaction abort!\n"))
644 649
645 650 try:
646 651 for cat in sorted(self._abortcallback):
647 652 self._abortcallback[cat](self)
648 653 # Prevent double usage and help clear cycles.
649 654 self._abortcallback = None
650 655 _playback(
651 656 self._journal,
652 657 self._report,
653 658 self._opener,
654 659 self._vfsmap,
655 self._entries,
660 entries,
656 661 self._backupentries,
657 662 False,
658 663 checkambigfiles=self._checkambigfiles,
659 664 )
660 665 self._report(_(b"rollback completed\n"))
661 666 except BaseException as exc:
662 667 self._report(_(b"rollback failed - please run hg recover\n"))
663 668 self._report(
664 669 _(b"(failure reason: %s)\n") % stringutil.forcebytestr(exc)
665 670 )
666 671 finally:
667 672 self._journal = None
668 673 self._releasefn(self, False) # notify failure of transaction
669 674 self._releasefn = None # Help prevent cycles.
670 675
671 676
672 677 def rollback(opener, vfsmap, file, report, checkambigfiles=None):
673 678 """Rolls back the transaction contained in the given file
674 679
675 680 Reads the entries in the specified file, and the corresponding
676 681 '*.backupfiles' file, to recover from an incomplete transaction.
677 682
678 683 * `file`: a file containing a list of entries, specifying where
679 684 to truncate each file. The file should contain a list of
680 685 file\0offset pairs, delimited by newlines. The corresponding
681 686 '*.backupfiles' file should contain a list of file\0backupfile
682 687 pairs, delimited by \0.
683 688
684 689 `checkambigfiles` is a set of (path, vfs-location) tuples,
685 690 which determine whether file stat ambiguity should be avoided at
686 691 restoring corresponded files.
687 692 """
688 693 entries = []
689 694 backupentries = []
690 695
691 696 fp = opener.open(file)
692 697 lines = fp.readlines()
693 698 fp.close()
694 699 for l in lines:
695 700 try:
696 701 f, o = l.split(b'\0')
697 702 entries.append((f, int(o)))
698 703 except ValueError:
699 704 report(
700 705 _(b"couldn't read journal entry %r!\n") % pycompat.bytestr(l)
701 706 )
702 707
703 708 backupjournal = b"%s.backupfiles" % file
704 709 if opener.exists(backupjournal):
705 710 fp = opener.open(backupjournal)
706 711 lines = fp.readlines()
707 712 if lines:
708 713 ver = lines[0][:-1]
709 714 if ver == (b'%d' % version):
710 715 for line in lines[1:]:
711 716 if line:
712 717 # Shave off the trailing newline
713 718 line = line[:-1]
714 719 l, f, b, c = line.split(b'\0')
715 720 backupentries.append((l, f, b, bool(c)))
716 721 else:
717 722 report(
718 723 _(
719 724 b"journal was created by a different version of "
720 725 b"Mercurial\n"
721 726 )
722 727 )
723 728
724 729 _playback(
725 730 file,
726 731 report,
727 732 opener,
728 733 vfsmap,
729 734 entries,
730 735 backupentries,
731 736 checkambigfiles=checkambigfiles,
732 737 )
General Comments 0
You need to be logged in to leave comments. Login now