repair.py
546 lines
| 17.7 KiB
| text/x-python
|
PythonLexer
/ mercurial / repair.py
|
r4702 | # repair.py - functions for repository repair for mercurial | ||
# | ||||
# Copyright 2005, 2006 Chris Mason <mason@suse.com> | ||||
|
r47575 | # Copyright 2007 Olivia Mackall | ||
|
r4702 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r4702 | |||
|
r25970 | from __future__ import absolute_import | ||
|
r16440 | import errno | ||
|
r4702 | |||
|
r25970 | from .i18n import _ | ||
|
r34219 | from .node import ( | ||
hex, | ||||
short, | ||||
) | ||||
|
r25970 | from . import ( | ||
bundle2, | ||||
changegroup, | ||||
r32468 | discovery, | |||
|
r26587 | error, | ||
|
r25970 | exchange, | ||
|
r28868 | obsolete, | ||
r33144 | obsutil, | |||
r43923 | pathutil, | |||
|
r39781 | phases, | ||
|
r38806 | pycompat, | ||
|
r45932 | requirements, | ||
|
r46129 | scmutil, | ||
|
r25970 | ) | ||
|
r44517 | from .utils import ( | ||
hashutil, | ||||
stringutil, | ||||
r47669 | urlutil, | |||
|
r44517 | ) | ||
|
r43346 | |||
|
r25970 | |||
|
r43346 | def backupbundle( | ||
repo, bases, heads, node, suffix, compress=True, obsolescence=True | ||||
): | ||||
|
r5905 | """create a bundle with the specified revisions as a backup""" | ||
|
r23898 | |||
|
r43347 | backupdir = b"strip-backup" | ||
|
r20977 | vfs = repo.vfs | ||
if not vfs.isdir(backupdir): | ||||
vfs.mkdir(backupdir) | ||||
|
r23835 | |||
# Include a hash of all the nodes in the filename for uniqueness | ||||
|
r43347 | allcommits = repo.set(b'%ln::%ln', bases, heads) | ||
|
r23835 | allhashes = sorted(c.hex() for c in allcommits) | ||
|
r44517 | totalhash = hashutil.sha1(b''.join(allhashes)).digest() | ||
|
r43347 | name = b"%s/%s-%s-%s.hg" % ( | ||
|
r43346 | backupdir, | ||
short(node), | ||||
hex(totalhash[:4]), | ||||
suffix, | ||||
) | ||||
|
r23835 | |||
|
r34179 | cgversion = changegroup.localversion(repo) | ||
|
r26425 | comp = None | ||
|
r43347 | if cgversion != b'01': | ||
bundletype = b"HG20" | ||||
|
r26425 | if compress: | ||
|
r43347 | comp = b'BZ' | ||
|
r23898 | elif compress: | ||
|
r43347 | bundletype = b"HG10BZ" | ||
|
r11791 | else: | ||
|
r43347 | bundletype = b"HG10UN" | ||
r32468 | ||||
|
r45704 | outgoing = discovery.outgoing(repo, missingroots=bases, ancestorsof=heads) | ||
|
r33032 | contentopts = { | ||
|
r43347 | b'cg.version': cgversion, | ||
b'obsolescence': obsolescence, | ||||
b'phases': True, | ||||
|
r33032 | } | ||
|
r43346 | return bundle2.writenewbundle( | ||
repo.ui, | ||||
repo, | ||||
|
r43347 | b'strip', | ||
|
r43346 | name, | ||
bundletype, | ||||
outgoing, | ||||
contentopts, | ||||
vfs, | ||||
compression=comp, | ||||
) | ||||
|
r4702 | |||
|
r5910 | def _collectfiles(repo, striprev): | ||
"""find out the filelogs affected by the strip""" | ||||
|
r8462 | files = set() | ||
|
r4702 | |||
|
r38806 | for x in pycompat.xrange(striprev, len(repo)): | ||
|
r8479 | files.update(repo[x].files()) | ||
|
r5902 | |||
|
r8462 | return sorted(files) | ||
|
r5902 | |||
|
r43346 | |||
|
r33690 | def _collectrevlog(revlog, striprev): | ||
_, brokenset = revlog.getstrippoint(striprev) | ||||
return [revlog.linkrev(r) for r in brokenset] | ||||
|
r43346 | |||
|
r13702 | def _collectbrokencsets(repo, files, striprev): | ||
"""return the changesets which will be broken by the truncation""" | ||||
|
r13705 | s = set() | ||
|
r5909 | |||
|
r43183 | for revlog in manifestrevlogs(repo): | ||
s.update(_collectrevlog(revlog, striprev)) | ||||
|
r13705 | for fname in files: | ||
|
r33690 | s.update(_collectrevlog(repo.file(fname), striprev)) | ||
|
r5909 | |||
|
r13705 | return s | ||
|
r5909 | |||
|
r43346 | |||
|
r43347 | def strip(ui, repo, nodelist, backup=True, topic=b'backup'): | ||
|
r32922 | # This function requires the caller to lock the repo, but it operates | ||
# within a transaction of its own, and thus requires there to be no current | ||||
# transaction when it is called. | ||||
|
r32924 | if repo.currenttransaction() is not None: | ||
|
r43347 | raise error.ProgrammingError(b'cannot strip from inside a transaction') | ||
|
r32924 | |||
|
r22057 | # Simple way to maintain backwards compatibility for this | ||
# argument. | ||||
|
r43347 | if backup in [b'none', b'strip']: | ||
|
r22057 | backup = False | ||
|
r18004 | repo = repo.unfiltered() | ||
|
r18310 | repo.destroying() | ||
|
r41131 | vfs = repo.vfs | ||
r42902 | # load bookmark before changelog to avoid side effect from outdated | |||
# changelog (see repo._refreshchangelog) | ||||
repo._bookmarks | ||||
|
r41131 | cl = repo.changelog | ||
|
r17013 | |||
|
r16237 | # TODO handle undo of merge sets | ||
|
r44179 | if isinstance(nodelist, bytes): | ||
|
r16252 | nodelist = [nodelist] | ||
striplist = [cl.rev(node) for node in nodelist] | ||||
striprev = min(striplist) | ||||
|
r4702 | |||
|
r30707 | files = _collectfiles(repo, striprev) | ||
saverevs = _collectbrokencsets(repo, files, striprev) | ||||
|
r6147 | # Some revisions with rev > striprev may not be descendants of striprev. | ||
# We have to find these revisions and put them in a bundle, so that | ||||
# we can restore them after the truncations. | ||||
# To create the bundle we use repo.changegroupsubset which requires | ||||
# the list of heads and bases of the set of interesting revisions. | ||||
# (head = revision in the set that has no descendant in the set; | ||||
# base = revision in the set that has no ancestor in the set) | ||||
|
r16252 | tostrip = set(striplist) | ||
|
r30707 | saveheads = set(saverevs) | ||
|
r30706 | for r in cl.revs(start=striprev + 1): | ||
if any(p in tostrip for p in cl.parentrevs(r)): | ||||
tostrip.add(r) | ||||
|
r13702 | |||
if r not in tostrip: | ||||
saverevs.add(r) | ||||
saveheads.difference_update(cl.parentrevs(r)) | ||||
saveheads.add(r) | ||||
saveheads = [cl.node(r) for r in saveheads] | ||||
|
r4702 | |||
|
r15386 | # compute base nodes | ||
if saverevs: | ||||
|
r16867 | descendants = set(cl.descendants(saverevs)) | ||
|
r15386 | saverevs.difference_update(descendants) | ||
savebases = [cl.node(r) for r in saverevs] | ||||
|
r16252 | stripbases = [cl.node(r) for r in tostrip] | ||
|
r18040 | |||
r32629 | stripobsidx = obsmarkers = () | |||
|
r43347 | if repo.ui.configbool(b'devel', b'strip-obsmarkers'): | ||
r33144 | obsmarkers = obsutil.exclusivemarkers(repo, stripbases) | |||
r32629 | if obsmarkers: | |||
|
r43346 | stripobsidx = [ | ||
i for i, m in enumerate(repo.obsstore) if m in obsmarkers | ||||
] | ||||
r32629 | ||||
|
r41135 | newbmtarget, updatebm = _bookmarkmovements(repo, tostrip) | ||
|
r13362 | |||
|
r11197 | backupfile = None | ||
|
r24252 | node = nodelist[-1] | ||
|
r22057 | if backup: | ||
|
r41133 | backupfile = _createstripbackup(repo, stripbases, node, topic) | ||
|
r41132 | # create a changegroup for all the branches we need to keep | ||
|
r29954 | tmpbundlefile = None | ||
|
r29951 | if saveheads: | ||
|
r29954 | # do not compress temporary bundle if we remove it from disk later | ||
r32628 | # | |||
# We do not include obsolescence, it might re-introduce prune markers | ||||
# we are trying to strip. This is harmless since the stripped markers | ||||
# are already backed up and we did not touched the markers for the | ||||
# saved changesets. | ||||
|
r43346 | tmpbundlefile = backupbundle( | ||
repo, | ||||
savebases, | ||||
saveheads, | ||||
node, | ||||
|
r43347 | b'temp', | ||
|
r43346 | compress=False, | ||
obsolescence=False, | ||||
) | ||||
|
r4702 | |||
|
r41106 | with ui.uninterruptible(): | ||
|
r38546 | try: | ||
|
r43347 | with repo.transaction(b"strip") as tr: | ||
|
r39722 | # TODO this code violates the interface abstraction of the | ||
# transaction and makes assumptions that file storage is | ||||
# using append-only files. We'll need some kind of storage | ||||
# API to handle stripping for us. | ||||
|
r46483 | oldfiles = set(tr._offsetmap.keys()) | ||
|
r46484 | oldfiles.update(tr._newfiles) | ||
|
r8073 | |||
|
r38546 | tr.startgroup() | ||
cl.strip(striprev, tr) | ||||
stripmanifest(repo, striprev, tr, files) | ||||
|
r8073 | |||
|
r38546 | for fn in files: | ||
repo.file(fn).strip(striprev, tr) | ||||
tr.endgroup() | ||||
|
r11197 | |||
|
r46483 | entries = tr.readjournal() | ||
for file, troffset in entries: | ||||
if file in oldfiles: | ||||
continue | ||||
|
r43347 | with repo.svfs(file, b'a', checkambig=True) as fp: | ||
|
r38546 | fp.truncate(troffset) | ||
if troffset == 0: | ||||
repo.store.markremoved(file) | ||||
deleteobsmarkers(repo.obsstore, stripobsidx) | ||||
del repo.obsstore | ||||
repo.invalidatevolatilesets() | ||||
repo._phasecache.filterunknown(repo) | ||||
r32629 | ||||
|
r38546 | if tmpbundlefile: | ||
|
r43347 | ui.note(_(b"adding branch\n")) | ||
f = vfs.open(tmpbundlefile, b"rb") | ||||
|
r38546 | gen = exchange.readbundle(ui, f, tmpbundlefile, vfs) | ||
if not repo.ui.verbose: | ||||
# silence internal shuffling chatter | ||||
repo.ui.pushbuffer() | ||||
|
r43347 | tmpbundleurl = b'bundle:' + vfs.join(tmpbundlefile) | ||
txnname = b'strip' | ||||
|
r38546 | if not isinstance(gen, bundle2.unbundle20): | ||
r47669 | txnname = b"strip\n%s" % urlutil.hidepassword(tmpbundleurl) | |||
|
r38546 | with repo.transaction(txnname) as tr: | ||
|
r43346 | bundle2.applybundle( | ||
|
r43347 | repo, gen, tr, source=b'strip', url=tmpbundleurl | ||
|
r43346 | ) | ||
|
r38546 | if not repo.ui.verbose: | ||
repo.ui.popbuffer() | ||||
f.close() | ||||
|
r31626 | |||
|
r43347 | with repo.transaction(b'repair') as tr: | ||
|
r38546 | bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm] | ||
|
r41135 | repo._bookmarks.applychanges(repo, tr, bmchanges) | ||
|
r27157 | |||
|
r38546 | # remove undo files | ||
for undovfs, undofile in repo.undofiles(): | ||||
try: | ||||
undovfs.unlink(undofile) | ||||
except OSError as e: | ||||
if e.errno != errno.ENOENT: | ||||
|
r43346 | ui.warn( | ||
|
r43347 | _(b'error removing %s: %s\n') | ||
|
r43346 | % ( | ||
undovfs.join(undofile), | ||||
stringutil.forcebytestr(e), | ||||
) | ||||
) | ||||
|
r16237 | |||
|
r43346 | except: # re-raises | ||
|
r38546 | if backupfile: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b"strip failed, backup bundle stored in '%s'\n") | ||
|
r43346 | % vfs.join(backupfile) | ||
) | ||||
|
r38546 | if tmpbundlefile: | ||
|
r43346 | ui.warn( | ||
|
r43347 | _(b"strip failed, unrecovered changes stored in '%s'\n") | ||
|
r43346 | % vfs.join(tmpbundlefile) | ||
) | ||||
ui.warn( | ||||
_( | ||||
|
r43347 | b"(fix the problem, then recover the changesets with " | ||
b"\"hg unbundle '%s'\")\n" | ||||
|
r43346 | ) | ||
% vfs.join(tmpbundlefile) | ||||
) | ||||
|
r38546 | raise | ||
else: | ||||
if tmpbundlefile: | ||||
# Remove temporary bundle only if there were no exceptions | ||||
vfs.unlink(tmpbundlefile) | ||||
|
r4702 | |||
|
r18395 | repo.destroyed() | ||
|
r30274 | # return the backup file path (or None if 'backup' was False) so | ||
# extensions can use it | ||||
return backupfile | ||||
|
r25652 | |||
|
r43346 | |||
|
r43347 | def softstrip(ui, repo, nodelist, backup=True, topic=b'backup'): | ||
|
r41960 | """perform a "soft" strip using the archived phase""" | ||
|
r43347 | tostrip = [c.node() for c in repo.set(b'sort(%ln::)', nodelist)] | ||
|
r41960 | if not tostrip: | ||
return None | ||||
|
r47265 | backupfile = None | ||
|
r41960 | if backup: | ||
node = tostrip[0] | ||||
backupfile = _createstripbackup(repo, tostrip, node, topic) | ||||
|
r47264 | newbmtarget, updatebm = _bookmarkmovements(repo, tostrip) | ||
|
r43347 | with repo.transaction(b'strip') as tr: | ||
|
r41960 | phases.retractboundary(repo, tr, phases.archived, tostrip) | ||
bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm] | ||||
repo._bookmarks.applychanges(repo, tr, bmchanges) | ||||
return backupfile | ||||
|
r41135 | def _bookmarkmovements(repo, tostrip): | ||
# compute necessary bookmark movement | ||||
bm = repo._bookmarks | ||||
updatebm = [] | ||||
for m in bm: | ||||
rev = repo[bm[m]].rev() | ||||
if rev in tostrip: | ||||
updatebm.append(m) | ||||
newbmtarget = None | ||||
|
r42432 | # If we need to move bookmarks, compute bookmark | ||
# targets. Otherwise we can skip doing this logic. | ||||
if updatebm: | ||||
|
r41135 | # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)), | ||
# but is much faster | ||||
|
r43347 | newbmtarget = repo.revs(b'max(parents(%ld) - (%ld))', tostrip, tostrip) | ||
|
r41135 | if newbmtarget: | ||
newbmtarget = repo[newbmtarget.first()].node() | ||||
else: | ||||
|
r43347 | newbmtarget = b'.' | ||
|
r41135 | return newbmtarget, updatebm | ||
|
r43346 | |||
|
r41133 | def _createstripbackup(repo, stripbases, node, topic): | ||
# backup the changeset we are about to strip | ||||
vfs = repo.vfs | ||||
cl = repo.changelog | ||||
backupfile = backupbundle(repo, stripbases, cl.heads(), node, topic) | ||||
|
r43347 | repo.ui.status(_(b"saved backup bundle to %s\n") % vfs.join(backupfile)) | ||
|
r43346 | repo.ui.log( | ||
|
r43347 | b"backupbundle", b"saved backup bundle to %s\n", vfs.join(backupfile) | ||
|
r43346 | ) | ||
|
r41133 | return backupfile | ||
|
r43346 | |||
|
r33087 | def safestriproots(ui, repo, nodes): | ||
"""return list of roots of nodes where descendants are covered by nodes""" | ||||
torev = repo.unfiltered().changelog.rev | ||||
|
r44937 | revs = {torev(n) for n in nodes} | ||
|
r33087 | # tostrip = wanted - unsafe = wanted - ancestors(orphaned) | ||
# orphaned = affected - wanted | ||||
# affected = descendants(roots(wanted)) | ||||
# wanted = revs | ||||
|
r43347 | revset = b'%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )' | ||
|
r39781 | tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs)) | ||
|
r33087 | notstrip = revs - tostrip | ||
if notstrip: | ||||
|
r43347 | nodestr = b', '.join(sorted(short(repo[n].node()) for n in notstrip)) | ||
|
r43346 | ui.warn( | ||
|
r43387 | _(b'warning: orphaned descendants detected, not stripping %s\n') | ||
|
r43346 | % nodestr | ||
) | ||||
|
r43347 | return [c.node() for c in repo.set(b'roots(%ld)', tostrip)] | ||
|
r33087 | |||
|
r43346 | |||
|
r33087 | class stripcallback(object): | ||
"""used as a transaction postclose callback""" | ||||
def __init__(self, ui, repo, backup, topic): | ||||
self.ui = ui | ||||
self.repo = repo | ||||
self.backup = backup | ||||
|
r43347 | self.topic = topic or b'backup' | ||
|
r33087 | self.nodelist = [] | ||
def addnodes(self, nodes): | ||||
self.nodelist.extend(nodes) | ||||
def __call__(self, tr): | ||||
roots = safestriproots(self.ui, self.repo, self.nodelist) | ||||
if roots: | ||||
|
r33108 | strip(self.ui, self.repo, roots, self.backup, self.topic) | ||
|
r33087 | |||
|
r43346 | |||
|
r38835 | def delayedstrip(ui, repo, nodelist, topic=None, backup=True): | ||
|
r33087 | """like strip, but works inside transaction and won't strip irreverent revs | ||
nodelist must explicitly contain all descendants. Otherwise a warning will | ||||
be printed that some nodes are not stripped. | ||||
|
r38835 | Will do a backup if `backup` is True. The last non-None "topic" will be | ||
used as the backup topic name. The default backup topic name is "backup". | ||||
|
r33087 | """ | ||
tr = repo.currenttransaction() | ||||
if not tr: | ||||
nodes = safestriproots(ui, repo, nodelist) | ||||
|
r38835 | return strip(ui, repo, nodes, backup=backup, topic=topic) | ||
|
r33087 | # transaction postclose callbacks are called in alphabet order. | ||
# use '\xff' as prefix so we are likely to be called last. | ||||
|
r43347 | callback = tr.getpostclose(b'\xffstrip') | ||
|
r33087 | if callback is None: | ||
|
r38835 | callback = stripcallback(ui, repo, backup=backup, topic=topic) | ||
|
r43347 | tr.addpostclose(b'\xffstrip', callback) | ||
|
r33087 | if topic: | ||
callback.topic = topic | ||||
callback.addnodes(nodelist) | ||||
|
r43346 | |||
|
r33691 | def stripmanifest(repo, striprev, tr, files): | ||
|
r43182 | for revlog in manifestrevlogs(repo): | ||
revlog.strip(striprev, tr) | ||||
|
r33691 | |||
|
r43346 | |||
|
r43182 | def manifestrevlogs(repo): | ||
yield repo.manifestlog.getstorage(b'') | ||||
|
r46129 | if scmutil.istreemanifest(repo): | ||
|
r42439 | # This logic is safe if treemanifest isn't enabled, but also | ||
# pointless, so we skip it if treemanifest isn't enabled. | ||||
r47657 | for t, unencoded, encoded, size in repo.store.datafiles(): | |||
|
r43347 | if unencoded.startswith(b'meta/') and unencoded.endswith( | ||
b'00manifest.i' | ||||
|
r43346 | ): | ||
|
r32296 | dir = unencoded[5:-12] | ||
|
r43182 | yield repo.manifestlog.getstorage(dir) | ||
|
r32196 | |||
|
r43346 | |||
|
r25652 | def rebuildfncache(ui, repo): | ||
"""Rebuilds the fncache file from repo history. | ||||
Missing entries will be added. Extra entries will be removed. | ||||
""" | ||||
repo = repo.unfiltered() | ||||
|
r47383 | if requirements.FNCACHE_REQUIREMENT not in repo.requirements: | ||
|
r43346 | ui.warn( | ||
_( | ||||
|
r43347 | b'(not rebuilding fncache because repository does not ' | ||
b'support fncache)\n' | ||||
|
r43346 | ) | ||
) | ||||
|
r25652 | return | ||
|
r27860 | with repo.lock(): | ||
|
r25652 | fnc = repo.store.fncache | ||
|
r42960 | fnc.ensureloaded(warn=ui.warn) | ||
|
r25652 | |||
oldentries = set(fnc.entries) | ||||
newentries = set() | ||||
seenfiles = set() | ||||
|
r43346 | progress = ui.makeprogress( | ||
|
r43347 | _(b'rebuilding'), unit=_(b'changesets'), total=len(repo) | ||
|
r43346 | ) | ||
|
r25652 | for rev in repo: | ||
|
r38413 | progress.update(rev) | ||
|
r25652 | |||
ctx = repo[rev] | ||||
for f in ctx.files(): | ||||
# This is to minimize I/O. | ||||
if f in seenfiles: | ||||
continue | ||||
seenfiles.add(f) | ||||
|
r43347 | i = b'data/%s.i' % f | ||
d = b'data/%s.d' % f | ||||
|
r25652 | |||
if repo.store._exists(i): | ||||
newentries.add(i) | ||||
if repo.store._exists(d): | ||||
newentries.add(d) | ||||
|
r38413 | progress.complete() | ||
|
r25652 | |||
|
r45932 | if requirements.TREEMANIFEST_REQUIREMENT in repo.requirements: | ||
|
r42439 | # This logic is safe if treemanifest isn't enabled, but also | ||
# pointless, so we skip it if treemanifest isn't enabled. | ||||
r43923 | for dir in pathutil.dirs(seenfiles): | |||
|
r43347 | i = b'meta/%s/00manifest.i' % dir | ||
d = b'meta/%s/00manifest.d' % dir | ||||
|
r28007 | |||
if repo.store._exists(i): | ||||
newentries.add(i) | ||||
if repo.store._exists(d): | ||||
newentries.add(d) | ||||
|
r25652 | addcount = len(newentries - oldentries) | ||
removecount = len(oldentries - newentries) | ||||
for p in sorted(oldentries - newentries): | ||||
|
r43347 | ui.write(_(b'removing %s\n') % p) | ||
|
r25652 | for p in sorted(newentries - oldentries): | ||
|
r43347 | ui.write(_(b'adding %s\n') % p) | ||
|
r25652 | |||
if addcount or removecount: | ||||
|
r43346 | ui.write( | ||
|
r43347 | _(b'%d items added, %d removed from fncache\n') | ||
|
r43346 | % (addcount, removecount) | ||
) | ||||
|
r25652 | fnc.entries = newentries | ||
fnc._dirty = True | ||||
|
r43347 | with repo.transaction(b'fncache') as tr: | ||
|
r25652 | fnc.write(tr) | ||
else: | ||||
|
r43347 | ui.write(_(b'fncache already up to date\n')) | ||
|
r25652 | |||
|
r43346 | |||
|
r28868 | def deleteobsmarkers(obsstore, indices): | ||
"""Delete some obsmarkers from obsstore and return how many were deleted | ||||
'indices' is a list of ints which are the indices | ||||
of the markers to be deleted. | ||||
Every invocation of this function completely rewrites the obsstore file, | ||||
skipping the markers we want to be removed. The new temporary file is | ||||
created, remaining markers are written there and on .close() this file | ||||
gets atomically renamed to obsstore, thus guaranteeing consistency.""" | ||||
if not indices: | ||||
# we don't want to rewrite the obsstore with the same content | ||||
return | ||||
left = [] | ||||
current = obsstore._all | ||||
n = 0 | ||||
for i, m in enumerate(current): | ||||
if i in indices: | ||||
n += 1 | ||||
continue | ||||
left.append(m) | ||||
|
r43347 | newobsstorefile = obsstore.svfs(b'obsstore', b'w', atomictemp=True) | ||
|
r28868 | for bytes in obsolete.encodemarkers(left, True, obsstore._version): | ||
newobsstorefile.write(bytes) | ||||
newobsstorefile.close() | ||||
return n | ||||