changegroup.py
1111 lines
| 41.9 KiB
| text/x-python
|
PythonLexer
/ mercurial / changegroup.py
|
r8226 | # changegroup.py - Mercurial changegroup manipulation functions | ||
# | ||||
# Copyright 2006 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r3877 | |||
|
r25921 | from __future__ import absolute_import | ||
import os | ||||
import struct | ||||
import tempfile | ||||
|
r20933 | import weakref | ||
|
r25921 | |||
from .i18n import _ | ||||
from .node import ( | ||||
hex, | ||||
nullid, | ||||
nullrev, | ||||
short, | ||||
) | ||||
from . import ( | ||||
branchmap, | ||||
dagutil, | ||||
discovery, | ||||
error, | ||||
mdiff, | ||||
phases, | ||||
util, | ||||
) | ||||
|
r1981 | |||
|
r22390 | _CHANGEGROUPV1_DELTA_HEADER = "20s20s20s20s" | ||
|
r23181 | _CHANGEGROUPV2_DELTA_HEADER = "20s20s20s20s20s" | ||
|
r27433 | _CHANGEGROUPV3_DELTA_HEADER = ">20s20s20s20s20sH" | ||
|
r14141 | |||
|
r13457 | def readexactly(stream, n): | ||
'''read n bytes from stream.read and abort if less was available''' | ||||
s = stream.read(n) | ||||
if len(s) < n: | ||||
|
r26587 | raise error.Abort(_("stream ended unexpectedly" | ||
|
r13457 | " (got %d bytes, expected %d)") | ||
% (len(s), n)) | ||||
return s | ||||
def getchunk(stream): | ||||
"""return the next chunk from stream as a string""" | ||||
d = readexactly(stream, 4) | ||||
|
r1981 | l = struct.unpack(">l", d)[0] | ||
if l <= 4: | ||||
|
r13458 | if l: | ||
|
r26587 | raise error.Abort(_("invalid chunk length %d") % l) | ||
|
r1981 | return "" | ||
|
r13457 | return readexactly(stream, l - 4) | ||
|
r1981 | |||
|
r5368 | def chunkheader(length): | ||
|
r9437 | """return a changegroup chunk header (string)""" | ||
|
r5368 | return struct.pack(">l", length + 4) | ||
|
r1981 | |||
def closechunk(): | ||||
|
r9437 | """return a changegroup chunk header (string) for a zero-length chunk""" | ||
|
r1981 | return struct.pack(">l", 0) | ||
|
r23890 | def combineresults(results): | ||
"""logic to combine 0 or more addchangegroup results into one""" | ||||
changedheads = 0 | ||||
result = 1 | ||||
for ret in results: | ||||
# If any changegroup result is 0, return 0 | ||||
if ret == 0: | ||||
result = 0 | ||||
break | ||||
if ret < -1: | ||||
changedheads += ret + 1 | ||||
elif ret > 1: | ||||
changedheads += ret - 1 | ||||
if changedheads > 0: | ||||
result = 1 + changedheads | ||||
elif changedheads < 0: | ||||
result = -1 + changedheads | ||||
return result | ||||
|
r3662 | bundletypes = { | ||
|
r26272 | "": ("", None), # only when using unbundle on ssh and old http servers | ||
|
r14060 | # since the unification ssh accepts a header but there | ||
# is no capability signaling it. | ||||
|
r24686 | "HG20": (), # special-cased below | ||
|
r26271 | "HG10UN": ("HG10UN", None), | ||
|
r26266 | "HG10BZ": ("HG10", 'BZ'), | ||
"HG10GZ": ("HG10GZ", 'GZ'), | ||||
|
r3662 | } | ||
|
r9087 | # hgweb uses this list to communicate its preferred type | ||
|
r6152 | bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN'] | ||
|
r26540 | def writechunks(ui, chunks, filename, vfs=None): | ||
"""Write chunks to a file and return its filename. | ||||
|
r3659 | |||
|
r26540 | The stream is assumed to be a bundle file. | ||
|
r3659 | Existing files will not be overwritten. | ||
If no filename is specified, a temporary file is created. | ||||
""" | ||||
fh = None | ||||
cleanup = None | ||||
try: | ||||
if filename: | ||||
|
r20976 | if vfs: | ||
fh = vfs.open(filename, "wb") | ||||
else: | ||||
fh = open(filename, "wb") | ||||
|
r3659 | else: | ||
fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg") | ||||
fh = os.fdopen(fd, "wb") | ||||
cleanup = filename | ||||
|
r26540 | for c in chunks: | ||
fh.write(c) | ||||
|
r3659 | cleanup = None | ||
return filename | ||||
finally: | ||||
if fh is not None: | ||||
fh.close() | ||||
if cleanup is not None: | ||||
|
r20976 | if filename and vfs: | ||
vfs.unlink(cleanup) | ||||
else: | ||||
os.unlink(cleanup) | ||||
|
r3660 | |||
|
r26540 | def writebundle(ui, cg, filename, bundletype, vfs=None, compression=None): | ||
"""Write a bundle file and return its filename. | ||||
Existing files will not be overwritten. | ||||
If no filename is specified, a temporary file is created. | ||||
bz2 compression can be turned off. | ||||
The bundle file will be deleted in case of errors. | ||||
""" | ||||
if bundletype == "HG20": | ||||
from . import bundle2 | ||||
bundle = bundle2.bundle20(ui) | ||||
bundle.setcompression(compression) | ||||
part = bundle.newpart('changegroup', data=cg.getchunks()) | ||||
part.addparam('version', cg.version) | ||||
chunkiter = bundle.getchunks() | ||||
else: | ||||
# compression argument is only for the bundle2 case | ||||
assert compression is None | ||||
if cg.version != '01': | ||||
|
r26587 | raise error.Abort(_('old bundle types only supports v1 ' | ||
'changegroups')) | ||||
|
r26540 | header, comp = bundletypes[bundletype] | ||
if comp not in util.compressors: | ||||
|
r26587 | raise error.Abort(_('unknown stream compression type: %s') | ||
% comp) | ||||
|
r26540 | z = util.compressors[comp]() | ||
subchunkiter = cg.getchunks() | ||||
def chunkiter(): | ||||
yield header | ||||
for chunk in subchunkiter: | ||||
yield z.compress(chunk) | ||||
yield z.flush() | ||||
chunkiter = chunkiter() | ||||
# parse the changegroup data, otherwise we will block | ||||
# in case of sshrepo because we don't know the end of the stream | ||||
# an empty chunkgroup is the end of the changegroup | ||||
# a changegroup has at least 2 chunkgroups (changelog and manifest). | ||||
# after that, an empty chunkgroup is the end of the changegroup | ||||
return writechunks(ui, chunkiter, filename, vfs=vfs) | ||||
|
r22390 | class cg1unpacker(object): | ||
|
r26708 | """Unpacker for cg1 changegroup streams. | ||
A changegroup unpacker handles the framing of the revision data in | ||||
the wire format. Most consumers will want to use the apply() | ||||
method to add the changes from the changegroup to a repository. | ||||
If you're forwarding a changegroup unmodified to another consumer, | ||||
use getchunks(), which returns an iterator of changegroup | ||||
chunks. This is mostly useful for cases where you need to know the | ||||
data stream has ended by observing the end of the changegroup. | ||||
deltachunk() is useful only if you're applying delta data. Most | ||||
consumers should prefer apply() instead. | ||||
A few other public methods exist. Those are used only for | ||||
bundlerepo and some debug commands - their use is discouraged. | ||||
""" | ||||
|
r22390 | deltaheader = _CHANGEGROUPV1_DELTA_HEADER | ||
|
r14141 | deltaheadersize = struct.calcsize(deltaheader) | ||
|
r23896 | version = '01' | ||
|
r12043 | def __init__(self, fh, alg): | ||
|
r26267 | if alg == 'UN': | ||
alg = None # get more modern without breaking too much | ||||
|
r26266 | if not alg in util.decompressors: | ||
|
r26587 | raise error.Abort(_('unknown stream compression type: %s') | ||
|
r26266 | % alg) | ||
|
r26392 | if alg == 'BZ': | ||
alg = '_truncatedBZ' | ||||
|
r26266 | self._stream = util.decompressors[alg](fh) | ||
|
r12044 | self._type = alg | ||
|
r12334 | self.callback = None | ||
|
r26706 | |||
# These methods (compressed, read, seek, tell) all appear to only | ||||
# be used by bundlerepo, but it's a little hard to tell. | ||||
|
r12044 | def compressed(self): | ||
|
r26267 | return self._type is not None | ||
|
r12043 | def read(self, l): | ||
return self._stream.read(l) | ||||
|
r12330 | def seek(self, pos): | ||
return self._stream.seek(pos) | ||||
def tell(self): | ||||
|
r12332 | return self._stream.tell() | ||
|
r12347 | def close(self): | ||
return self._stream.close() | ||||
|
r12334 | |||
|
r26707 | def _chunklength(self): | ||
|
r13459 | d = readexactly(self._stream, 4) | ||
|
r13458 | l = struct.unpack(">l", d)[0] | ||
if l <= 4: | ||||
if l: | ||||
|
r26587 | raise error.Abort(_("invalid chunk length %d") % l) | ||
|
r13458 | return 0 | ||
if self.callback: | ||||
|
r12334 | self.callback() | ||
|
r13458 | return l - 4 | ||
|
r12334 | |||
|
r14144 | def changelogheader(self): | ||
"""v10 does not have a changelog header chunk""" | ||||
return {} | ||||
def manifestheader(self): | ||||
"""v10 does not have a manifest header chunk""" | ||||
return {} | ||||
def filelogheader(self): | ||||
"""return the header of the filelogs chunk, v10 only has the filename""" | ||||
|
r26707 | l = self._chunklength() | ||
|
r14144 | if not l: | ||
return {} | ||||
fname = readexactly(self._stream, l) | ||||
|
r20675 | return {'filename': fname} | ||
|
r12334 | |||
|
r14141 | def _deltaheader(self, headertuple, prevnode): | ||
node, p1, p2, cs = headertuple | ||||
if prevnode is None: | ||||
deltabase = p1 | ||||
else: | ||||
deltabase = prevnode | ||||
|
r27433 | flags = 0 | ||
return node, p1, p2, deltabase, cs, flags | ||||
|
r14141 | |||
|
r14144 | def deltachunk(self, prevnode): | ||
|
r26707 | l = self._chunklength() | ||
|
r12336 | if not l: | ||
return {} | ||||
|
r14141 | headerdata = readexactly(self._stream, self.deltaheadersize) | ||
header = struct.unpack(self.deltaheader, headerdata) | ||||
delta = readexactly(self._stream, l - self.deltaheadersize) | ||||
|
r27433 | node, p1, p2, deltabase, cs, flags = self._deltaheader(header, prevnode) | ||
|
r20675 | return {'node': node, 'p1': p1, 'p2': p2, 'cs': cs, | ||
|
r27433 | 'deltabase': deltabase, 'delta': delta, 'flags': flags} | ||
|
r12336 | |||
|
r20999 | def getchunks(self): | ||
"""returns all the chunks contains in the bundle | ||||
Used when you need to forward the binary stream to a file or another | ||||
network API. To do so, it parse the changegroup data, otherwise it will | ||||
block in case of sshrepo because it don't know the end of the stream. | ||||
""" | ||||
# an empty chunkgroup is the end of the changegroup | ||||
# a changegroup has at least 2 chunkgroups (changelog and manifest). | ||||
# after that, an empty chunkgroup is the end of the changegroup | ||||
empty = False | ||||
count = 0 | ||||
while not empty or count <= 2: | ||||
empty = True | ||||
count += 1 | ||||
while True: | ||||
chunk = getchunk(self) | ||||
if not chunk: | ||||
break | ||||
empty = False | ||||
yield chunkheader(len(chunk)) | ||||
pos = 0 | ||||
while pos < len(chunk): | ||||
next = pos + 2**20 | ||||
yield chunk[pos:next] | ||||
pos = next | ||||
yield closechunk() | ||||
|
r26712 | def _unpackmanifests(self, repo, revmap, trp, prog, numchanges): | ||
# We know that we'll never have more manifests than we had | ||||
# changesets. | ||||
self.callback = prog(_('manifests'), numchanges) | ||||
# no need to check for empty manifest group here: | ||||
# if the result of the merge of 1 and 2 is the same in 3 and 4, | ||||
# no new manifest will be created and the manifest group will | ||||
# be empty during the pull | ||||
self.manifestheader() | ||||
repo.manifest.addgroup(self, revmap, trp) | ||||
repo.ui.progress(_('manifests'), None) | ||||
|
r26695 | def apply(self, repo, srctype, url, emptyok=False, | ||
targetphase=phases.draft, expectedtotal=None): | ||||
"""Add the changegroup returned by source.read() to this repo. | ||||
srctype is a string like 'push', 'pull', or 'unbundle'. url is | ||||
the URL of the repo where this changegroup is coming from. | ||||
Return an integer summarizing the change to this repo: | ||||
- nothing changed or no source: 0 | ||||
- more heads than before: 1+added heads (2..n) | ||||
- fewer heads than before: -1-removed heads (-2..-n) | ||||
- number of heads stays the same: 1 | ||||
""" | ||||
repo = repo.unfiltered() | ||||
def csmap(x): | ||||
repo.ui.debug("add changeset %s\n" % short(x)) | ||||
return len(cl) | ||||
def revmap(x): | ||||
return cl.rev(x) | ||||
changesets = files = revisions = 0 | ||||
tr = repo.transaction("\n".join([srctype, util.hidepassword(url)])) | ||||
|
r26880 | try: | ||
# The transaction could have been created before and already | ||||
# carries source information. In this case we use the top | ||||
# level data. We overwrite the argument because we need to use | ||||
# the top level value (if they exist) in this function. | ||||
srctype = tr.hookargs.setdefault('source', srctype) | ||||
url = tr.hookargs.setdefault('url', url) | ||||
|
r26881 | repo.hook('prechangegroup', throw=True, **tr.hookargs) | ||
|
r26695 | |||
|
r26880 | # write changelog data to temp files so concurrent readers | ||
# will not see an inconsistent view | ||||
cl = repo.changelog | ||||
cl.delayupdate(tr) | ||||
oldheads = cl.heads() | ||||
|
r26695 | |||
trp = weakref.proxy(tr) | ||||
# pull off the changeset group | ||||
repo.ui.status(_("adding changesets\n")) | ||||
clstart = len(cl) | ||||
class prog(object): | ||||
def __init__(self, step, total): | ||||
self._step = step | ||||
self._total = total | ||||
self._count = 1 | ||||
def __call__(self): | ||||
repo.ui.progress(self._step, self._count, unit=_('chunks'), | ||||
total=self._total) | ||||
self._count += 1 | ||||
self.callback = prog(_('changesets'), expectedtotal) | ||||
efiles = set() | ||||
def onchangelog(cl, node): | ||||
efiles.update(cl.read(node)[3]) | ||||
self.changelogheader() | ||||
srccontent = cl.addgroup(self, csmap, trp, | ||||
addrevisioncb=onchangelog) | ||||
efiles = len(efiles) | ||||
if not (srccontent or emptyok): | ||||
raise error.Abort(_("received changelog group is empty")) | ||||
clend = len(cl) | ||||
changesets = clend - clstart | ||||
repo.ui.progress(_('changesets'), None) | ||||
# pull off the manifest group | ||||
repo.ui.status(_("adding manifests\n")) | ||||
|
r26712 | self._unpackmanifests(repo, revmap, trp, prog, changesets) | ||
|
r26695 | |||
needfiles = {} | ||||
if repo.ui.configbool('server', 'validate', default=False): | ||||
# validate incoming csets have their manifests | ||||
for cset in xrange(clstart, clend): | ||||
mfnode = repo.changelog.read(repo.changelog.node(cset))[0] | ||||
mfest = repo.manifest.readdelta(mfnode) | ||||
# store file nodes we must see | ||||
for f, n in mfest.iteritems(): | ||||
needfiles.setdefault(f, set()).add(n) | ||||
# process the files | ||||
repo.ui.status(_("adding file changes\n")) | ||||
self.callback = None | ||||
pr = prog(_('files'), efiles) | ||||
|
r26704 | newrevs, newfiles = _addchangegroupfiles( | ||
|
r27735 | repo, self, revmap, trp, pr, needfiles) | ||
|
r26695 | revisions += newrevs | ||
files += newfiles | ||||
dh = 0 | ||||
if oldheads: | ||||
heads = cl.heads() | ||||
dh = len(heads) - len(oldheads) | ||||
for h in heads: | ||||
if h not in oldheads and repo[h].closesbranch(): | ||||
dh -= 1 | ||||
htext = "" | ||||
if dh: | ||||
htext = _(" (%+d heads)") % dh | ||||
repo.ui.status(_("added %d changesets" | ||||
" with %d changes to %d files%s\n") | ||||
% (changesets, revisions, files, htext)) | ||||
repo.invalidatevolatilesets() | ||||
if changesets > 0: | ||||
if 'node' not in tr.hookargs: | ||||
tr.hookargs['node'] = hex(cl.node(clstart)) | ||||
|
r27739 | tr.hookargs['node_last'] = hex(cl.node(clend - 1)) | ||
|
r26695 | hookargs = dict(tr.hookargs) | ||
else: | ||||
hookargs = dict(tr.hookargs) | ||||
hookargs['node'] = hex(cl.node(clstart)) | ||||
|
r27739 | hookargs['node_last'] = hex(cl.node(clend - 1)) | ||
|
r26751 | repo.hook('pretxnchangegroup', throw=True, **hookargs) | ||
|
r26695 | |||
added = [cl.node(r) for r in xrange(clstart, clend)] | ||||
publishing = repo.publishing() | ||||
if srctype in ('push', 'serve'): | ||||
# Old servers can not push the boundary themselves. | ||||
# New servers won't push the boundary if changeset already | ||||
# exists locally as secret | ||||
# | ||||
# We should not use added here but the list of all change in | ||||
# the bundle | ||||
if publishing: | ||||
phases.advanceboundary(repo, tr, phases.public, srccontent) | ||||
else: | ||||
# Those changesets have been pushed from the outside, their | ||||
# phases are going to be pushed alongside. Therefor | ||||
# `targetphase` is ignored. | ||||
phases.advanceboundary(repo, tr, phases.draft, srccontent) | ||||
phases.retractboundary(repo, tr, phases.draft, added) | ||||
elif srctype != 'strip': | ||||
# publishing only alter behavior during push | ||||
# | ||||
# strip should not touch boundary at all | ||||
phases.retractboundary(repo, tr, targetphase, added) | ||||
if changesets > 0: | ||||
if srctype != 'strip': | ||||
# During strip, branchcache is invalid but coming call to | ||||
# `destroyed` will repair it. | ||||
# In other case we can safely update cache on disk. | ||||
branchmap.updatecache(repo.filtered('served')) | ||||
def runhooks(): | ||||
# These hooks run when the lock releases, not when the | ||||
# transaction closes. So it's possible for the changelog | ||||
# to have changed since we last saw it. | ||||
if clstart >= len(repo): | ||||
return | ||||
# forcefully update the on-disk branch cache | ||||
repo.ui.debug("updating the branch cache\n") | ||||
repo.hook("changegroup", **hookargs) | ||||
for n in added: | ||||
args = hookargs.copy() | ||||
args['node'] = hex(n) | ||||
|
r27739 | del args['node_last'] | ||
|
r26695 | repo.hook("incoming", **args) | ||
newheads = [h for h in repo.heads() if h not in oldheads] | ||||
repo.ui.log("incoming", | ||||
"%s incoming changes - new heads: %s\n", | ||||
len(added), | ||||
', '.join([hex(c[:6]) for c in newheads])) | ||||
tr.addpostclose('changegroup-runhooks-%020i' % clstart, | ||||
lambda tr: repo._afterlock(runhooks)) | ||||
tr.close() | ||||
finally: | ||||
tr.release() | ||||
repo.ui.flush() | ||||
# never return 0 here: | ||||
if dh < 0: | ||||
return dh - 1 | ||||
else: | ||||
return dh + 1 | ||||
|
r23181 | class cg2unpacker(cg1unpacker): | ||
|
r26708 | """Unpacker for cg2 streams. | ||
cg2 streams add support for generaldelta, so the delta header | ||||
format is slightly different. All other features about the data | ||||
remain the same. | ||||
""" | ||||
|
r23181 | deltaheader = _CHANGEGROUPV2_DELTA_HEADER | ||
deltaheadersize = struct.calcsize(deltaheader) | ||||
|
r23896 | version = '02' | ||
|
r23181 | |||
def _deltaheader(self, headertuple, prevnode): | ||||
node, p1, p2, deltabase, cs = headertuple | ||||
|
r27433 | flags = 0 | ||
return node, p1, p2, deltabase, cs, flags | ||||
|
r23181 | |||
|
r27432 | class cg3unpacker(cg2unpacker): | ||
"""Unpacker for cg3 streams. | ||||
|
r27433 | cg3 streams add support for exchanging treemanifests and revlog | ||
|
r27753 | flags. It adds the revlog flags to the delta header and an empty chunk | ||
separating manifests and files. | ||||
|
r27432 | """ | ||
|
r27433 | deltaheader = _CHANGEGROUPV3_DELTA_HEADER | ||
deltaheadersize = struct.calcsize(deltaheader) | ||||
|
r27432 | version = '03' | ||
|
r27433 | def _deltaheader(self, headertuple, prevnode): | ||
node, p1, p2, deltabase, cs, flags = headertuple | ||||
return node, p1, p2, deltabase, cs, flags | ||||
|
r27754 | def _unpackmanifests(self, repo, revmap, trp, prog, numchanges): | ||
super(cg3unpacker, self)._unpackmanifests(repo, revmap, trp, prog, | ||||
numchanges) | ||||
while True: | ||||
chunkdata = self.filelogheader() | ||||
if not chunkdata: | ||||
break | ||||
# If we get here, there are directory manifests in the changegroup | ||||
d = chunkdata["filename"] | ||||
repo.ui.debug("adding %s revisions\n" % d) | ||||
dirlog = repo.manifest.dirlog(d) | ||||
if not dirlog.addgroup(self, revmap, trp): | ||||
raise error.Abort(_("received dir revlog group is empty")) | ||||
|
r12329 | class headerlessfixup(object): | ||
def __init__(self, fh, h): | ||||
self._h = h | ||||
self._fh = fh | ||||
def read(self, n): | ||||
if self._h: | ||||
d, self._h = self._h[:n], self._h[n:] | ||||
if len(d) < n: | ||||
|
r13457 | d += readexactly(self._fh, n - len(d)) | ||
|
r12329 | return d | ||
|
r13457 | return readexactly(self._fh, n) | ||
|
r12329 | |||
|
r27432 | def _moddirs(files): | ||
"""Given a set of modified files, find the list of modified directories. | ||||
This returns a list of (path to changed dir, changed dir) tuples, | ||||
as that's what the one client needs anyway. | ||||
>>> _moddirs(['a/b/c.py', 'a/b/c.txt', 'a/d/e/f/g.txt', 'i.txt', ]) | ||||
[('/', 'a/'), ('a/', 'b/'), ('a/', 'd/'), ('a/d/', 'e/'), ('a/d/e/', 'f/')] | ||||
""" | ||||
alldirs = set() | ||||
for f in files: | ||||
path = f.split('/')[:-1] | ||||
for i in xrange(len(path) - 1, -1, -1): | ||||
dn = '/'.join(path[:i]) | ||||
current = dn + '/', path[i] + '/' | ||||
if current in alldirs: | ||||
break | ||||
alldirs.add(current) | ||||
return sorted(alldirs) | ||||
|
r22390 | class cg1packer(object): | ||
deltaheader = _CHANGEGROUPV1_DELTA_HEADER | ||||
|
r23896 | version = '01' | ||
|
r19202 | def __init__(self, repo, bundlecaps=None): | ||
"""Given a source repo, construct a bundler. | ||||
bundlecaps is optional and can be used to specify the set of | ||||
capabilities which can be used to build the bundle. | ||||
""" | ||||
|
r19201 | # Set of capabilities we can use to build the bundle. | ||
if bundlecaps is None: | ||||
bundlecaps = set() | ||||
self._bundlecaps = bundlecaps | ||||
|
r25831 | # experimental config: bundle.reorder | ||
|
r19202 | reorder = repo.ui.config('bundle', 'reorder', 'auto') | ||
if reorder == 'auto': | ||||
reorder = None | ||||
else: | ||||
reorder = util.parsebool(reorder) | ||||
self._repo = repo | ||||
self._reorder = reorder | ||||
|
r19208 | self._progress = repo.ui.progress | ||
|
r23748 | if self._repo.ui.verbose and not self._repo.ui.debugflag: | ||
self._verbosenote = self._repo.ui.note | ||||
else: | ||||
self._verbosenote = lambda s: None | ||||
|
r13831 | def close(self): | ||
return closechunk() | ||||
|
r19200 | |||
|
r13831 | def fileheader(self, fname): | ||
return chunkheader(len(fname)) + fname | ||||
|
r19200 | |||
|
r24912 | def group(self, nodelist, revlog, lookup, units=None): | ||
|
r19200 | """Calculate a delta group, yielding a sequence of changegroup chunks | ||
(strings). | ||||
Given a list of changeset revs, return a set of deltas and | ||||
metadata corresponding to nodes. The first delta is | ||||
first parent(nodelist[0]) -> nodelist[0], the receiver is | ||||
guaranteed to have this parent as it has all history before | ||||
these changesets. In the case firstparent is nullrev the | ||||
changegroup starts with a full revision. | ||||
|
r19208 | |||
If units is not None, progress detail will be generated, units specifies | ||||
the type of revlog that is touched (changelog, manifest, etc.). | ||||
|
r19200 | """ | ||
# if we don't have any revisions touched by these changesets, bail | ||||
if len(nodelist) == 0: | ||||
yield self.close() | ||||
return | ||||
# for generaldelta revlogs, we linearize the revs; this will both be | ||||
# much quicker and generate a much smaller bundle | ||||
|
r24912 | if (revlog._generaldelta and self._reorder is None) or self._reorder: | ||
|
r19200 | dag = dagutil.revlogdag(revlog) | ||
revs = set(revlog.rev(n) for n in nodelist) | ||||
revs = dag.linearize(revs) | ||||
else: | ||||
revs = sorted([revlog.rev(n) for n in nodelist]) | ||||
# add the parent of the first rev | ||||
p = revlog.parentrevs(revs[0])[0] | ||||
revs.insert(0, p) | ||||
# build deltas | ||||
|
r19208 | total = len(revs) - 1 | ||
msgbundling = _('bundling') | ||||
|
r19200 | for r in xrange(len(revs) - 1): | ||
|
r19208 | if units is not None: | ||
self._progress(msgbundling, r + 1, unit=units, total=total) | ||||
|
r19200 | prev, curr = revs[r], revs[r + 1] | ||
|
r19207 | linknode = lookup(revlog.node(curr)) | ||
for c in self.revchunk(revlog, curr, prev, linknode): | ||||
|
r19200 | yield c | ||
|
r24901 | if units is not None: | ||
self._progress(msgbundling, None) | ||||
|
r19200 | yield self.close() | ||
|
r19289 | # filter any nodes that claim to be part of the known set | ||
|
r24896 | def prune(self, revlog, missing, commonrevs): | ||
|
r19289 | rr, rl = revlog.rev, revlog.linkrev | ||
return [n for n in missing if rl(rr(n)) not in commonrevs] | ||||
|
r27432 | def _packmanifests(self, mfnodes, tmfnodes, lookuplinknode): | ||
|
r26711 | """Pack flat manifests into a changegroup stream.""" | ||
ml = self._repo.manifest | ||||
size = 0 | ||||
for chunk in self.group( | ||||
mfnodes, ml, lookuplinknode, units=_('manifests')): | ||||
size += len(chunk) | ||||
yield chunk | ||||
self._verbosenote(_('%8.i (manifests)\n') % size) | ||||
|
r27432 | # It looks odd to assert this here, but tmfnodes doesn't get | ||
# filled in until after we've called lookuplinknode for | ||||
# sending root manifests, so the only way to tell the streams | ||||
# got crossed is to check after we've done all the work. | ||||
assert not tmfnodes | ||||
|
r26711 | |||
|
r19204 | def generate(self, commonrevs, clnodes, fastpathlinkrev, source): | ||
|
r19202 | '''yield a sequence of changegroup chunks (strings)''' | ||
repo = self._repo | ||||
|
r24978 | cl = repo.changelog | ||
ml = repo.manifest | ||||
|
r19204 | |||
|
r23381 | clrevorder = {} | ||
|
r19204 | mfs = {} # needed manifests | ||
|
r27432 | tmfnodes = {} | ||
|
r19204 | fnodes = {} # needed file nodes | ||
|
r27237 | # maps manifest node id -> set(changed files) | ||
mfchangedfiles = {} | ||||
|
r19204 | |||
|
r19207 | # Callback for the changelog, used to collect changed files and manifest | ||
# nodes. | ||||
# Returns the linkrev node (identity in the changelog case). | ||||
def lookupcl(x): | ||||
c = cl.read(x) | ||||
|
r23381 | clrevorder[x] = len(clrevorder) | ||
|
r27237 | n = c[0] | ||
|
r19207 | # record the first changeset introducing this manifest version | ||
|
r27237 | mfs.setdefault(n, x) | ||
# Record a complete list of potentially-changed files in | ||||
# this manifest. | ||||
mfchangedfiles.setdefault(n, set()).update(c[3]) | ||||
|
r19207 | return x | ||
|
r19204 | |||
|
r23748 | self._verbosenote(_('uncompressed size of bundle content:\n')) | ||
size = 0 | ||||
|
r24912 | for chunk in self.group(clnodes, cl, lookupcl, units=_('changesets')): | ||
|
r23748 | size += len(chunk) | ||
|
r23224 | yield chunk | ||
|
r23748 | self._verbosenote(_('%8.i (changelog)\n') % size) | ||
|
r23224 | |||
|
r24977 | # We need to make sure that the linkrev in the changegroup refers to | ||
# the first changeset that introduced the manifest or file revision. | ||||
# The fastpath is usually safer than the slowpath, because the filelogs | ||||
# are walked in revlog order. | ||||
# | ||||
# When taking the slowpath with reorder=None and the manifest revlog | ||||
# uses generaldelta, the manifest may be walked in the "wrong" order. | ||||
# Without 'clrevorder', we would get an incorrect linkrev (see fix in | ||||
# cc0ff93d0c0c). | ||||
# | ||||
# When taking the fastpath, we are only vulnerable to reordering | ||||
# of the changelog itself. The changelog never uses generaldelta, so | ||||
# it is only reordered when reorder=True. To handle this case, we | ||||
# simply take the slowpath, which already has the 'clrevorder' logic. | ||||
# This was also fixed in cc0ff93d0c0c. | ||||
|
r24976 | fastpathlinkrev = fastpathlinkrev and not self._reorder | ||
|
r27432 | # Treemanifests don't work correctly with fastpathlinkrev | ||
# either, because we don't discover which directory nodes to | ||||
# send along with files. This could probably be fixed. | ||||
fastpathlinkrev = fastpathlinkrev and ( | ||||
'treemanifest' not in repo.requirements) | ||||
|
r19207 | # Callback for the manifest, used to collect linkrevs for filelog | ||
# revisions. | ||||
# Returns the linkrev node (collected in lookupcl). | ||||
|
r27239 | if fastpathlinkrev: | ||
lookupmflinknode = mfs.__getitem__ | ||||
else: | ||||
def lookupmflinknode(x): | ||||
"""Callback for looking up the linknode for manifests. | ||||
|
r27219 | |||
|
r27239 | Returns the linkrev node for the specified manifest. | ||
|
r27219 | |||
|
r27239 | SIDE EFFECT: | ||
|
r27432 | 1) fclnodes gets populated with the list of relevant | ||
file nodes if we're not using fastpathlinkrev | ||||
2) When treemanifests are in use, collects treemanifest nodes | ||||
to send | ||||
|
r27219 | |||
|
r27432 | Note that this means manifests must be completely sent to | ||
the client before you can trust the list of files and | ||||
treemanifests to send. | ||||
|
r27239 | """ | ||
clnode = mfs[x] | ||||
|
r27432 | # We no longer actually care about reading deltas of | ||
# the manifest here, because we already know the list | ||||
# of changed files, so for treemanifests (which | ||||
# lazily-load anyway to *generate* a readdelta) we can | ||||
# just load them with read() and then we'll actually | ||||
# be able to correctly load node IDs from the | ||||
# submanifest entries. | ||||
if 'treemanifest' in repo.requirements: | ||||
mdata = ml.read(x) | ||||
else: | ||||
mdata = ml.readfast(x) | ||||
|
r27237 | for f in mfchangedfiles[x]: | ||
|
r27238 | try: | ||
n = mdata[f] | ||||
except KeyError: | ||||
continue | ||||
# record the first changeset introducing this filelog | ||||
# version | ||||
fclnodes = fnodes.setdefault(f, {}) | ||||
fclnode = fclnodes.setdefault(n, clnode) | ||||
if clrevorder[clnode] < clrevorder[fclnode]: | ||||
fclnodes[n] = clnode | ||||
|
r27432 | # gather list of changed treemanifest nodes | ||
if 'treemanifest' in repo.requirements: | ||||
submfs = {'/': mdata} | ||||
for dn, bn in _moddirs(mfchangedfiles[x]): | ||||
submf = submfs[dn] | ||||
submf = submf._dirs[bn] | ||||
submfs[submf.dir()] = submf | ||||
tmfclnodes = tmfnodes.setdefault(submf.dir(), {}) | ||||
tmfclnodes.setdefault(submf._node, clnode) | ||||
if clrevorder[clnode] < clrevorder[fclnode]: | ||||
tmfclnodes[n] = clnode | ||||
|
r27239 | return clnode | ||
|
r19206 | |||
|
r24899 | mfnodes = self.prune(ml, mfs, commonrevs) | ||
|
r27432 | for x in self._packmanifests( | ||
mfnodes, tmfnodes, lookupmflinknode): | ||||
|
r26711 | yield x | ||
|
r19206 | |||
mfs.clear() | ||||
|
r24898 | clrevs = set(cl.rev(x) for x in clnodes) | ||
|
r19206 | |||
|
r27240 | if not fastpathlinkrev: | ||
def linknodes(unused, fname): | ||||
return fnodes.get(fname, {}) | ||||
else: | ||||
|
r27241 | cln = cl.node | ||
|
r27240 | def linknodes(filerevlog, fname): | ||
|
r20936 | llr = filerevlog.linkrev | ||
|
r27241 | fln = filerevlog.node | ||
revs = ((r, llr(r)) for r in filerevlog) | ||||
return dict((fln(r), cln(lr)) for r, lr in revs if lr in clrevs) | ||||
|
r19207 | |||
|
r27237 | changedfiles = set() | ||
for x in mfchangedfiles.itervalues(): | ||||
changedfiles.update(x) | ||||
|
r19334 | for chunk in self.generatefiles(changedfiles, linknodes, commonrevs, | ||
source): | ||||
yield chunk | ||||
yield self.close() | ||||
if clnodes: | ||||
repo.hook('outgoing', node=hex(clnodes[0]), source=source) | ||||
|
r24897 | # The 'source' parameter is useful for extensions | ||
|
r19334 | def generatefiles(self, changedfiles, linknodes, commonrevs, source): | ||
repo = self._repo | ||||
progress = self._progress | ||||
msgbundling = _('bundling') | ||||
total = len(changedfiles) | ||||
# for progress output | ||||
msgfiles = _('files') | ||||
for i, fname in enumerate(sorted(changedfiles)): | ||||
filerevlog = repo.file(fname) | ||||
if not filerevlog: | ||||
|
r26587 | raise error.Abort(_("empty or missing revlog for %s") % fname) | ||
|
r19334 | |||
linkrevnodes = linknodes(filerevlog, fname) | ||||
|
r19207 | # Lookup for filenodes, we collected the linkrev nodes above in the | ||
# fastpath case and with lookupmf in the slowpath case. | ||||
def lookupfilelog(x): | ||||
return linkrevnodes[x] | ||||
|
r24896 | filenodes = self.prune(filerevlog, linkrevnodes, commonrevs) | ||
|
r19206 | if filenodes: | ||
|
r19208 | progress(msgbundling, i + 1, item=fname, unit=msgfiles, | ||
total=total) | ||||
|
r23748 | h = self.fileheader(fname) | ||
size = len(h) | ||||
yield h | ||||
|
r24912 | for chunk in self.group(filenodes, filerevlog, lookupfilelog): | ||
|
r23748 | size += len(chunk) | ||
|
r19202 | yield chunk | ||
|
r23748 | self._verbosenote(_('%8.i %s\n') % (size, fname)) | ||
|
r24901 | progress(msgbundling, None) | ||
|
r19200 | |||
|
r23181 | def deltaparent(self, revlog, rev, p1, p2, prev): | ||
return prev | ||||
|
r19207 | def revchunk(self, revlog, rev, prev, linknode): | ||
|
r14143 | node = revlog.node(rev) | ||
p1, p2 = revlog.parentrevs(rev) | ||||
|
r23181 | base = self.deltaparent(revlog, rev, p1, p2, prev) | ||
|
r14143 | |||
prefix = '' | ||||
|
r24190 | if revlog.iscensored(base) or revlog.iscensored(rev): | ||
try: | ||||
delta = revlog.revision(node) | ||||
|
r25660 | except error.CensoredNodeError as e: | ||
|
r24190 | delta = e.tombstone | ||
if base == nullrev: | ||||
prefix = mdiff.trivialdiffheader(len(delta)) | ||||
else: | ||||
baselen = revlog.rawsize(base) | ||||
prefix = mdiff.replacediffheader(baselen, len(delta)) | ||||
elif base == nullrev: | ||||
|
r14143 | delta = revlog.revision(node) | ||
prefix = mdiff.trivialdiffheader(len(delta)) | ||||
else: | ||||
delta = revlog.revdiff(base, rev) | ||||
p1n, p2n = revlog.parents(node) | ||||
basenode = revlog.node(base) | ||||
|
r27433 | flags = revlog.flags(rev) | ||
meta = self.builddeltaheader(node, p1n, p2n, basenode, linknode, flags) | ||||
|
r14143 | meta += prefix | ||
l = len(meta) + len(delta) | ||||
|
r13831 | yield chunkheader(l) | ||
yield meta | ||||
|
r14143 | yield delta | ||
|
r27433 | def builddeltaheader(self, node, p1n, p2n, basenode, linknode, flags): | ||
|
r14143 | # do nothing with basenode, it is implicitly the previous one in HG10 | ||
|
r27433 | # do nothing with flags, it is implicitly 0 for cg1 and cg2 | ||
|
r14143 | return struct.pack(self.deltaheader, node, p1n, p2n, linknode) | ||
|
r20925 | |||
|
r23181 | class cg2packer(cg1packer): | ||
|
r23896 | version = '02' | ||
|
r23181 | deltaheader = _CHANGEGROUPV2_DELTA_HEADER | ||
|
r24911 | def __init__(self, repo, bundlecaps=None): | ||
super(cg2packer, self).__init__(repo, bundlecaps) | ||||
if self._reorder is None: | ||||
# Since generaldelta is directly supported by cg2, reordering | ||||
# generally doesn't help, so we disable it by default (treating | ||||
# bundle.reorder=auto just like bundle.reorder=False). | ||||
self._reorder = False | ||||
|
r23181 | |||
def deltaparent(self, revlog, rev, p1, p2, prev): | ||||
dp = revlog.deltaparent(rev) | ||||
# avoid storing full revisions; pick prev in those cases | ||||
# also pick prev when we can't be sure remote has dp | ||||
if dp == nullrev or (dp != p1 and dp != p2 and dp != prev): | ||||
return prev | ||||
return dp | ||||
|
r27433 | def builddeltaheader(self, node, p1n, p2n, basenode, linknode, flags): | ||
# Do nothing with flags, it is implicitly 0 in cg1 and cg2 | ||||
|
r23181 | return struct.pack(self.deltaheader, node, p1n, p2n, basenode, linknode) | ||
|
r27432 | class cg3packer(cg2packer): | ||
version = '03' | ||||
|
r27433 | deltaheader = _CHANGEGROUPV3_DELTA_HEADER | ||
|
r27432 | |||
def _packmanifests(self, mfnodes, tmfnodes, lookuplinknode): | ||||
# Note that debug prints are super confusing in this code, as | ||||
# tmfnodes gets populated by the calls to lookuplinknode in | ||||
# the superclass's manifest packer. In the future we should | ||||
# probably see if we can refactor this somehow to be less | ||||
# confusing. | ||||
for x in super(cg3packer, self)._packmanifests( | ||||
mfnodes, {}, lookuplinknode): | ||||
yield x | ||||
dirlog = self._repo.manifest.dirlog | ||||
for name, nodes in tmfnodes.iteritems(): | ||||
# For now, directory headers are simply file headers with | ||||
|
r27691 | # a trailing '/' on the path (already in the name). | ||
yield self.fileheader(name) | ||||
|
r27432 | for chunk in self.group(nodes, dirlog(name), nodes.get): | ||
yield chunk | ||||
|
r27753 | yield self.close() | ||
|
r27432 | |||
|
r27433 | def builddeltaheader(self, node, p1n, p2n, basenode, linknode, flags): | ||
return struct.pack( | ||||
self.deltaheader, node, p1n, p2n, basenode, linknode, flags) | ||||
|
r27432 | |||
|
r27751 | _packermap = {'01': (cg1packer, cg1unpacker), | ||
|
r26709 | # cg2 adds support for exchanging generaldelta | ||
'02': (cg2packer, cg2unpacker), | ||||
|
r27753 | # cg3 adds support for exchanging revlog flags and treemanifests | ||
|
r27432 | '03': (cg3packer, cg3unpacker), | ||
|
r26709 | } | ||
|
r23168 | |||
|
r27751 | def supportedversions(repo): | ||
|
r27752 | versions = _packermap.keys() | ||
cg3 = ('treemanifest' in repo.requirements or | ||||
repo.ui.configbool('experimental', 'changegroup3') or | ||||
repo.ui.configbool('experimental', 'treemanifest')) | ||||
if not cg3: | ||||
versions.remove('03') | ||||
return versions | ||||
|
r27751 | |||
def getbundler(version, repo, bundlecaps=None): | ||||
assert version in supportedversions(repo) | ||||
return _packermap[version][0](repo, bundlecaps) | ||||
def getunbundler(version, fh, alg): | ||||
return _packermap[version][1](fh, alg) | ||||
|
r20926 | def _changegroupinfo(repo, nodes, source): | ||
if repo.ui.verbose or source == 'bundle': | ||||
repo.ui.status(_("%d changesets found\n") % len(nodes)) | ||||
if repo.ui.debugflag: | ||||
repo.ui.debug("list of changesets:\n") | ||||
for node in nodes: | ||||
repo.ui.debug("%s\n" % hex(node)) | ||||
|
r23177 | def getsubsetraw(repo, outgoing, bundler, source, fastpath=False): | ||
|
r20925 | repo = repo.unfiltered() | ||
commonrevs = outgoing.common | ||||
csets = outgoing.missing | ||||
heads = outgoing.missingheads | ||||
# We go through the fast path if we get told to, or if all (unfiltered | ||||
# heads have been requested (since we then know there all linkrevs will | ||||
# be pulled by the client). | ||||
heads.sort() | ||||
fastpathlinkrev = fastpath or ( | ||||
repo.filtername is None and heads == sorted(repo.heads())) | ||||
repo.hook('preoutgoing', throw=True, source=source) | ||||
|
r20926 | _changegroupinfo(repo, csets, source) | ||
|
r23177 | return bundler.generate(commonrevs, csets, fastpathlinkrev, source) | ||
|
r26595 | def getsubset(repo, outgoing, bundler, source, fastpath=False): | ||
|
r23177 | gengroup = getsubsetraw(repo, outgoing, bundler, source, fastpath) | ||
|
r27751 | return getunbundler(bundler.version, util.chunkbuffer(gengroup), None) | ||
|
r20927 | |||
|
r23897 | def changegroupsubset(repo, roots, heads, source, version='01'): | ||
|
r20927 | """Compute a changegroup consisting of all the nodes that are | ||
descendants of any of the roots and ancestors of any of the heads. | ||||
Return a chunkbuffer object whose read() method will return | ||||
successive changegroup chunks. | ||||
It is fairly complex as determining which filenodes and which | ||||
manifest nodes need to be included for the changeset to be complete | ||||
is non-trivial. | ||||
Another wrinkle is doing the reverse, figuring out which changeset in | ||||
the changegroup a particular filenode or manifestnode belongs to. | ||||
""" | ||||
cl = repo.changelog | ||||
if not roots: | ||||
roots = [nullid] | ||||
discbases = [] | ||||
for n in roots: | ||||
discbases.extend([p for p in cl.parents(n) if p != nullid]) | ||||
|
r25677 | # TODO: remove call to nodesbetween. | ||
csets, roots, heads = cl.nodesbetween(roots, heads) | ||||
included = set(csets) | ||||
discbases = [n for n in discbases if n not in included] | ||||
|
r20927 | outgoing = discovery.outgoing(cl, discbases, heads) | ||
|
r27751 | bundler = getbundler(version, repo) | ||
|
r26595 | return getsubset(repo, outgoing, bundler, source) | ||
|
r20927 | |||
|
r23178 | def getlocalchangegroupraw(repo, source, outgoing, bundlecaps=None, | ||
version='01'): | ||||
|
r23177 | """Like getbundle, but taking a discovery.outgoing as an argument. | ||
This is only implemented for local repos and reuses potentially | ||||
precomputed sets in outgoing. Returns a raw changegroup generator.""" | ||||
if not outgoing.missing: | ||||
return None | ||||
|
r27751 | bundler = getbundler(version, repo, bundlecaps) | ||
|
r23177 | return getsubsetraw(repo, outgoing, bundler, source) | ||
|
r26508 | def getlocalchangegroup(repo, source, outgoing, bundlecaps=None, | ||
version='01'): | ||||
|
r20928 | """Like getbundle, but taking a discovery.outgoing as an argument. | ||
This is only implemented for local repos and reuses potentially | ||||
precomputed sets in outgoing.""" | ||||
if not outgoing.missing: | ||||
return None | ||||
|
r27751 | bundler = getbundler(version, repo, bundlecaps) | ||
|
r20928 | return getsubset(repo, outgoing, bundler, source) | ||
|
r25400 | def computeoutgoing(repo, heads, common): | ||
|
r21260 | """Computes which revs are outgoing given a set of common | ||
and a set of heads. | ||||
This is a separate function so extensions can have access to | ||||
the logic. | ||||
Returns a discovery.outgoing object. | ||||
""" | ||||
cl = repo.changelog | ||||
if common: | ||||
hasnode = cl.hasnode | ||||
common = [n for n in common if hasnode(n)] | ||||
else: | ||||
common = [nullid] | ||||
if not heads: | ||||
heads = cl.heads() | ||||
return discovery.outgoing(cl, common, heads) | ||||
|
r26509 | def getchangegroup(repo, source, heads=None, common=None, bundlecaps=None, | ||
version='01'): | ||||
|
r20930 | """Like changegroupsubset, but returns the set difference between the | ||
ancestors of heads and the ancestors common. | ||||
If heads is None, use the local heads. If common is None, use [nullid]. | ||||
The nodes in common might not all be known locally due to the way the | ||||
current discovery protocol works. | ||||
""" | ||||
|
r25400 | outgoing = computeoutgoing(repo, heads, common) | ||
|
r26509 | return getlocalchangegroup(repo, source, outgoing, bundlecaps=bundlecaps, | ||
version=version) | ||||
|
r20930 | |||
|
r20931 | def changegroup(repo, basenodes, source): | ||
# to avoid a race we use changegroupsubset() (issue1320) | ||||
return changegroupsubset(repo, basenodes, repo.heads(), source) | ||||
|
r27735 | def _addchangegroupfiles(repo, source, revmap, trp, pr, needfiles): | ||
|
r20932 | revisions = 0 | ||
files = 0 | ||||
while True: | ||||
chunkdata = source.filelogheader() | ||||
if not chunkdata: | ||||
break | ||||
f = chunkdata["filename"] | ||||
repo.ui.debug("adding %s revisions\n" % f) | ||||
pr() | ||||
|
r27754 | fl = repo.file(f) | ||
|
r20932 | o = len(fl) | ||
|
r24120 | try: | ||
if not fl.addgroup(source, revmap, trp): | ||||
|
r26587 | raise error.Abort(_("received file revlog group is empty")) | ||
|
r25660 | except error.CensoredBaseError as e: | ||
|
r26587 | raise error.Abort(_("received delta base is censored: %s") % e) | ||
|
r27754 | revisions += len(fl) - o | ||
files += 1 | ||||
|
r20932 | if f in needfiles: | ||
needs = needfiles[f] | ||||
for new in xrange(o, len(fl)): | ||||
n = fl.node(new) | ||||
if n in needs: | ||||
needs.remove(n) | ||||
else: | ||||
|
r26587 | raise error.Abort( | ||
|
r20932 | _("received spurious file revlog entry")) | ||
if not needs: | ||||
del needfiles[f] | ||||
repo.ui.progress(_('files'), None) | ||||
for f, needs in needfiles.iteritems(): | ||||
fl = repo.file(f) | ||||
for n in needs: | ||||
try: | ||||
fl.rev(n) | ||||
except error.LookupError: | ||||
|
r26587 | raise error.Abort( | ||
|
r20932 | _('missing file data for %s:%s - run hg verify') % | ||
(f, hex(n))) | ||||
return revisions, files | ||||