filemap.py
393 lines
| 14.1 KiB
| text/x-python
|
PythonLexer
|
r5376 | # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com> | ||
|
r5377 | # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br> | ||
|
r5376 | # | ||
|
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. | ||
|
r5376 | |||
import shlex | ||||
from mercurial.i18n import _ | ||||
from mercurial import util | ||||
|
r5556 | from common import SKIPREV, converter_source | ||
|
r5376 | |||
def rpairs(name): | ||||
e = len(name) | ||||
while e != -1: | ||||
|
r10282 | yield name[:e], name[e + 1:] | ||
|
r5376 | e = name.rfind('/', 0, e) | ||
|
r9884 | yield '.', name | ||
|
r5376 | |||
class filemapper(object): | ||||
'''Map and filter filenames when importing. | ||||
A name can be mapped to itself, a new name, or None (omit from new | ||||
repository).''' | ||||
def __init__(self, ui, path=None): | ||||
self.ui = ui | ||||
self.include = {} | ||||
self.exclude = {} | ||||
self.rename = {} | ||||
if path: | ||||
if self.parse(path): | ||||
raise util.Abort(_('errors in filemap')) | ||||
def parse(self, path): | ||||
errs = 0 | ||||
def check(name, mapping, listname): | ||||
|
r11680 | if not name: | ||
self.ui.warn(_('%s:%d: path to %s is missing\n') % | ||||
(lex.infile, lex.lineno, listname)) | ||||
return 1 | ||||
|
r5376 | if name in mapping: | ||
self.ui.warn(_('%s:%d: %r already in %s list\n') % | ||||
(lex.infile, lex.lineno, name, listname)) | ||||
return 1 | ||||
|
r11680 | if (name.startswith('/') or | ||
name.endswith('/') or | ||||
'//' in name): | ||||
self.ui.warn(_('%s:%d: superfluous / in %s %r\n') % | ||||
(lex.infile, lex.lineno, listname, name)) | ||||
return 1 | ||||
|
r5376 | return 0 | ||
lex = shlex.shlex(open(path), path, True) | ||||
lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?' | ||||
cmd = lex.get_token() | ||||
while cmd: | ||||
if cmd == 'include': | ||||
name = lex.get_token() | ||||
errs += check(name, self.exclude, 'exclude') | ||||
self.include[name] = name | ||||
elif cmd == 'exclude': | ||||
name = lex.get_token() | ||||
errs += check(name, self.include, 'include') | ||||
errs += check(name, self.rename, 'rename') | ||||
self.exclude[name] = name | ||||
elif cmd == 'rename': | ||||
src = lex.get_token() | ||||
dest = lex.get_token() | ||||
errs += check(src, self.exclude, 'exclude') | ||||
self.rename[src] = dest | ||||
elif cmd == 'source': | ||||
errs += self.parse(lex.get_token()) | ||||
else: | ||||
self.ui.warn(_('%s:%d: unknown directive %r\n') % | ||||
(lex.infile, lex.lineno, cmd)) | ||||
errs += 1 | ||||
cmd = lex.get_token() | ||||
return errs | ||||
def lookup(self, name, mapping): | ||||
for pre, suf in rpairs(name): | ||||
try: | ||||
return mapping[pre], pre, suf | ||||
|
r7875 | except KeyError: | ||
|
r5376 | pass | ||
return '', name, '' | ||||
def __call__(self, name): | ||||
if self.include: | ||||
inc = self.lookup(name, self.include)[0] | ||||
else: | ||||
inc = name | ||||
if self.exclude: | ||||
exc = self.lookup(name, self.exclude)[0] | ||||
else: | ||||
exc = '' | ||||
|
r9884 | if (not self.include and exc) or (len(inc) <= len(exc)): | ||
|
r5376 | return None | ||
newpre, pre, suf = self.lookup(name, self.rename) | ||||
if newpre: | ||||
if newpre == '.': | ||||
return suf | ||||
if suf: | ||||
|
r15565 | if newpre.endswith('/'): | ||
return newpre + suf | ||||
|
r5376 | return newpre + '/' + suf | ||
return newpre | ||||
return name | ||||
def active(self): | ||||
return bool(self.include or self.exclude or self.rename) | ||||
|
r5377 | |||
# This class does two additional things compared to a regular source: | ||||
# | ||||
# - Filter and rename files. This is mostly wrapped by the filemapper | ||||
# class above. We hide the original filename in the revision that is | ||||
|
r11134 | # returned by getchanges to be able to find things later in getfile. | ||
|
r5377 | # | ||
# - Return only revisions that matter for the files we're interested in. | ||||
# This involves rewriting the parents of the original revision to | ||||
# create a graph that is restricted to those revisions. | ||||
# | ||||
# This set of revisions includes not only revisions that directly | ||||
# touch files we're interested in, but also merges that merge two | ||||
# or more interesting revisions. | ||||
|
r5556 | class filemap_source(converter_source): | ||
|
r5377 | def __init__(self, ui, baseconverter, filemap): | ||
|
r5556 | super(filemap_source, self).__init__(ui) | ||
|
r5377 | self.base = baseconverter | ||
self.filemapper = filemapper(ui, filemap) | ||||
self.commits = {} | ||||
# if a revision rev has parent p in the original revision graph, then | ||||
# rev will have parent self.parentmap[p] in the restricted graph. | ||||
self.parentmap = {} | ||||
# self.wantedancestors[rev] is the set of all ancestors of rev that | ||||
# are in the restricted graph. | ||||
self.wantedancestors = {} | ||||
self.convertedorder = None | ||||
self._rebuilt = False | ||||
self.origparents = {} | ||||
|
r5401 | self.children = {} | ||
self.seenchildren = {} | ||||
|
r5377 | |||
|
r5799 | def before(self): | ||
self.base.before() | ||||
def after(self): | ||||
self.base.after() | ||||
|
r5510 | def setrevmap(self, revmap): | ||
|
r5377 | # rebuild our state to make things restartable | ||
# | ||||
# To avoid calling getcommit for every revision that has already | ||||
# been converted, we rebuild only the parentmap, delaying the | ||||
# rebuild of wantedancestors until we need it (i.e. until a | ||||
# merge). | ||||
# | ||||
# We assume the order argument lists the revisions in | ||||
# topological order, so that we can infer which revisions were | ||||
# wanted by previous runs. | ||||
self._rebuilt = not revmap | ||||
seen = {SKIPREV: SKIPREV} | ||||
|
r8150 | dummyset = set() | ||
|
r5377 | converted = [] | ||
|
r5510 | for rev in revmap.order: | ||
|
r5377 | mapped = revmap[rev] | ||
wanted = mapped not in seen | ||||
if wanted: | ||||
seen[mapped] = rev | ||||
self.parentmap[rev] = rev | ||||
else: | ||||
self.parentmap[rev] = seen[mapped] | ||||
self.wantedancestors[rev] = dummyset | ||||
arg = seen[mapped] | ||||
if arg == SKIPREV: | ||||
arg = None | ||||
converted.append((rev, wanted, arg)) | ||||
self.convertedorder = converted | ||||
|
r5510 | return self.base.setrevmap(revmap) | ||
|
r5377 | |||
def rebuild(self): | ||||
if self._rebuilt: | ||||
return True | ||||
self._rebuilt = True | ||||
self.parentmap.clear() | ||||
self.wantedancestors.clear() | ||||
|
r5401 | self.seenchildren.clear() | ||
|
r5377 | for rev, wanted, arg in self.convertedorder: | ||
|
r5401 | if rev not in self.origparents: | ||
self.origparents[rev] = self.getcommit(rev).parents | ||||
if arg is not None: | ||||
self.children[arg] = self.children.get(arg, 0) + 1 | ||||
for rev, wanted, arg in self.convertedorder: | ||||
parents = self.origparents[rev] | ||||
|
r5377 | if wanted: | ||
self.mark_wanted(rev, parents) | ||||
else: | ||||
self.mark_not_wanted(rev, arg) | ||||
|
r5401 | self._discard(arg, *parents) | ||
|
r5377 | |||
return True | ||||
def getheads(self): | ||||
return self.base.getheads() | ||||
def getcommit(self, rev): | ||||
# We want to save a reference to the commit objects to be able | ||||
# to rewrite their parents later on. | ||||
|
r5401 | c = self.commits[rev] = self.base.getcommit(rev) | ||
for p in c.parents: | ||||
self.children[p] = self.children.get(p, 0) + 1 | ||||
return c | ||||
|
r13968 | def _cachedcommit(self, rev): | ||
if rev in self.commits: | ||||
return self.commits[rev] | ||||
return self.base.getcommit(rev) | ||||
|
r5401 | def _discard(self, *revs): | ||
for r in revs: | ||||
if r is None: | ||||
continue | ||||
self.seenchildren[r] = self.seenchildren.get(r, 0) + 1 | ||||
if self.seenchildren[r] == self.children[r]: | ||||
del self.wantedancestors[r] | ||||
del self.parentmap[r] | ||||
del self.seenchildren[r] | ||||
if self._rebuilt: | ||||
del self.children[r] | ||||
|
r5377 | |||
def wanted(self, rev, i): | ||||
# Return True if we're directly interested in rev. | ||||
# | ||||
# i is an index selecting one of the parents of rev (if rev | ||||
# has no parents, i is None). getchangedfiles will give us | ||||
# the list of files that are different in rev and in the parent | ||||
# indicated by i. If we're interested in any of these files, | ||||
# we're interested in rev. | ||||
try: | ||||
files = self.base.getchangedfiles(rev, i) | ||||
except NotImplementedError: | ||||
raise util.Abort(_("source repository doesn't support --filemap")) | ||||
for f in files: | ||||
if self.filemapper(f): | ||||
return True | ||||
return False | ||||
def mark_not_wanted(self, rev, p): | ||||
# Mark rev as not interesting and update data structures. | ||||
if p is None: | ||||
# A root revision. Use SKIPREV to indicate that it doesn't | ||||
# map to any revision in the restricted graph. Put SKIPREV | ||||
# in the set of wanted ancestors to simplify code elsewhere | ||||
self.parentmap[rev] = SKIPREV | ||||
|
r8150 | self.wantedancestors[rev] = set((SKIPREV,)) | ||
|
r5377 | return | ||
# Reuse the data from our parent. | ||||
self.parentmap[rev] = self.parentmap[p] | ||||
self.wantedancestors[rev] = self.wantedancestors[p] | ||||
def mark_wanted(self, rev, parents): | ||||
# Mark rev ss wanted and update data structures. | ||||
# rev will be in the restricted graph, so children of rev in | ||||
# the original graph should still have rev as a parent in the | ||||
# restricted graph. | ||||
self.parentmap[rev] = rev | ||||
# The set of wanted ancestors of rev is the union of the sets | ||||
# of wanted ancestors of its parents. Plus rev itself. | ||||
|
r8150 | wrev = set() | ||
|
r5377 | for p in parents: | ||
wrev.update(self.wantedancestors[p]) | ||||
wrev.add(rev) | ||||
self.wantedancestors[rev] = wrev | ||||
def getchanges(self, rev): | ||||
parents = self.commits[rev].parents | ||||
if len(parents) > 1: | ||||
self.rebuild() | ||||
# To decide whether we're interested in rev we: | ||||
# | ||||
# - calculate what parents rev will have if it turns out we're | ||||
# interested in it. If it's going to have more than 1 parent, | ||||
# we're interested in it. | ||||
# | ||||
# - otherwise, we'll compare it with the single parent we found. | ||||
# If any of the files we're interested in is different in the | ||||
# the two revisions, we're interested in rev. | ||||
# A parent p is interesting if its mapped version (self.parentmap[p]): | ||||
# - is not SKIPREV | ||||
# - is still not in the list of parents (we don't want duplicates) | ||||
|
r17103 | # - is not an ancestor of the mapped versions of the other parents or | ||
# there is no parent in the same branch than the current revision. | ||||
|
r5377 | mparents = [] | ||
|
r17103 | knownparents = set() | ||
branch = self.commits[rev].branch | ||||
hasbranchparent = False | ||||
|
r5377 | for i, p1 in enumerate(parents): | ||
mp1 = self.parentmap[p1] | ||||
|
r17103 | if mp1 == SKIPREV or mp1 in knownparents: | ||
|
r5377 | continue | ||
|
r17103 | isancestor = util.any(p2 for p2 in parents | ||
if p1 != p2 and mp1 != self.parentmap[p2] | ||||
and mp1 in self.wantedancestors[p2]) | ||||
if not isancestor and not hasbranchparent and len(parents) > 1: | ||||
# This could be expensive, avoid unnecessary calls. | ||||
if self._cachedcommit(p1).branch == branch: | ||||
hasbranchparent = True | ||||
mparents.append((p1, mp1, i, isancestor)) | ||||
knownparents.add(mp1) | ||||
# Discard parents ancestors of other parents if there is a | ||||
# non-ancestor one on the same branch than current revision. | ||||
if hasbranchparent: | ||||
mparents = [p for p in mparents if not p[3]] | ||||
wp = None | ||||
if mparents: | ||||
wp = max(p[2] for p in mparents) | ||||
mparents = [p[1] for p in mparents] | ||||
elif parents: | ||||
|
r5377 | wp = 0 | ||
self.origparents[rev] = parents | ||||
|
r13968 | closed = False | ||
if 'close' in self.commits[rev].extra: | ||||
# A branch closing revision is only useful if one of its | ||||
# parents belong to the branch being closed | ||||
pbranches = [self._cachedcommit(p).branch for p in mparents] | ||||
if branch in pbranches: | ||||
closed = True | ||||
|
r11673 | |||
if len(mparents) < 2 and not closed and not self.wanted(rev, wp): | ||||
|
r5377 | # We don't want this revision. | ||
# Update our state and tell the convert process to map this | ||||
# revision to the same revision its parent as mapped to. | ||||
p = None | ||||
if parents: | ||||
p = parents[wp] | ||||
self.mark_not_wanted(rev, p) | ||||
self.convertedorder.append((rev, False, p)) | ||||
|
r5401 | self._discard(*parents) | ||
|
r5377 | return self.parentmap[rev] | ||
# We want this revision. | ||||
# Rewrite the parents of the commit object | ||||
self.commits[rev].parents = mparents | ||||
self.mark_wanted(rev, parents) | ||||
self.convertedorder.append((rev, True, None)) | ||||
|
r5401 | self._discard(*parents) | ||
|
r5377 | |||
|
r11134 | # Get the real changes and do the filtering/mapping. To be | ||
# able to get the files later on in getfile, we hide the | ||||
# original filename in the rev part of the return value. | ||||
|
r5377 | changes, copies = self.base.getchanges(rev) | ||
newnames = {} | ||||
|
r17174 | files = {} | ||
|
r5377 | for f, r in changes: | ||
newf = self.filemapper(f) | ||||
|
r17174 | if newf and (newf != f or newf not in files): | ||
files[newf] = (f, r) | ||||
|
r5377 | newnames[f] = newf | ||
|
r17174 | files = sorted(files.items()) | ||
|
r5377 | |||
ncopies = {} | ||||
for c in copies: | ||||
newc = self.filemapper(c) | ||||
if newc: | ||||
newsource = self.filemapper(copies[c]) | ||||
if newsource: | ||||
ncopies[newc] = newsource | ||||
return files, ncopies | ||||
def getfile(self, name, rev): | ||||
realname, realrev = rev | ||||
return self.base.getfile(realname, realrev) | ||||
def gettags(self): | ||||
return self.base.gettags() | ||||
|
r8691 | |||
def hasnativeorder(self): | ||||
return self.base.hasnativeorder() | ||||
|
r8693 | |||
def lookuprev(self, rev): | ||||
return self.base.lookuprev(rev) | ||||
|
r15107 | |||
def getbookmarks(self): | ||||
return self.base.getbookmarks() | ||||