filemap.py
479 lines
| 17.6 KiB
| text/x-python
|
PythonLexer
Alexis S. L. Carvalho
|
r5376 | # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com> | ||
Alexis S. L. Carvalho
|
r5377 | # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br> | ||
Alexis S. L. Carvalho
|
r5376 | # | ||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Yuya Nishihara
|
r34139 | |||
from __future__ import absolute_import, print_function | ||||
Alexis S. L. Carvalho
|
r5376 | |||
Huayang
|
r17797 | import posixpath | ||
Yuya Nishihara
|
r29205 | |||
from mercurial.i18n import _ | ||||
timeless
|
r28367 | from mercurial import ( | ||
error, | ||||
Augie Fackler
|
r36578 | pycompat, | ||
timeless
|
r28367 | ) | ||
from . import common | ||||
SKIPREV = common.SKIPREV | ||||
Alexis S. L. Carvalho
|
r5376 | |||
Mads Kiilerich
|
r20048 | def rpairs(path): | ||
'''Yield tuples with path split at '/', starting with the full path. | ||||
No leading, trailing or double '/', please. | ||||
Yuya Nishihara
|
r34139 | >>> for x in rpairs(b'foo/bar/baz'): print(x) | ||
Mads Kiilerich
|
r20048 | ('foo/bar/baz', '') | ||
('foo/bar', 'baz') | ||||
('foo', 'bar/baz') | ||||
('.', 'foo/bar/baz') | ||||
''' | ||||
i = len(path) | ||||
while i != -1: | ||||
yield path[:i], path[i + 1:] | ||||
i = path.rfind('/', 0, i) | ||||
yield '.', path | ||||
Alexis S. L. Carvalho
|
r5376 | |||
Huayang
|
r17797 | def normalize(path): | ||
''' We use posixpath.normpath to support cross-platform path format. | ||||
However, it doesn't handle None input. So we wrap it up. ''' | ||||
if path is None: | ||||
return None | ||||
return posixpath.normpath(path) | ||||
Alexis S. L. Carvalho
|
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 = {} | ||||
Durham Goode
|
r26036 | self.targetprefixes = None | ||
Alexis S. L. Carvalho
|
r5376 | if path: | ||
if self.parse(path): | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('errors in filemap')) | ||
Alexis S. L. Carvalho
|
r5376 | |||
def parse(self, path): | ||||
errs = 0 | ||||
def check(name, mapping, listname): | ||||
Mads Kiilerich
|
r11680 | if not name: | ||
self.ui.warn(_('%s:%d: path to %s is missing\n') % | ||||
(lex.infile, lex.lineno, listname)) | ||||
return 1 | ||||
Alexis S. L. Carvalho
|
r5376 | if name in mapping: | ||
self.ui.warn(_('%s:%d: %r already in %s list\n') % | ||||
(lex.infile, lex.lineno, name, listname)) | ||||
return 1 | ||||
Mads Kiilerich
|
r11680 | if (name.startswith('/') or | ||
name.endswith('/') or | ||||
'//' in name): | ||||
self.ui.warn(_('%s:%d: superfluous / in %s %r\n') % | ||||
Augie Fackler
|
r36578 | (lex.infile, lex.lineno, listname, | ||
pycompat.bytestr(name))) | ||||
Mads Kiilerich
|
r11680 | return 1 | ||
Alexis S. L. Carvalho
|
r5376 | return 0 | ||
Augie Fackler
|
r36577 | lex = common.shlexer( | ||
filepath=path, wordchars='!@#$%^&*()-=+[]{}|;:,./<>?') | ||||
Alexis S. L. Carvalho
|
r5376 | cmd = lex.get_token() | ||
while cmd: | ||||
if cmd == 'include': | ||||
Huayang
|
r17797 | name = normalize(lex.get_token()) | ||
Alexis S. L. Carvalho
|
r5376 | errs += check(name, self.exclude, 'exclude') | ||
self.include[name] = name | ||||
elif cmd == 'exclude': | ||||
Huayang
|
r17797 | name = normalize(lex.get_token()) | ||
Alexis S. L. Carvalho
|
r5376 | errs += check(name, self.include, 'include') | ||
errs += check(name, self.rename, 'rename') | ||||
self.exclude[name] = name | ||||
elif cmd == 'rename': | ||||
Huayang
|
r17797 | src = normalize(lex.get_token()) | ||
dest = normalize(lex.get_token()) | ||||
Alexis S. L. Carvalho
|
r5376 | errs += check(src, self.exclude, 'exclude') | ||
self.rename[src] = dest | ||||
elif cmd == 'source': | ||||
Huayang
|
r17797 | errs += self.parse(normalize(lex.get_token())) | ||
Alexis S. L. Carvalho
|
r5376 | else: | ||
self.ui.warn(_('%s:%d: unknown directive %r\n') % | ||||
Augie Fackler
|
r36578 | (lex.infile, lex.lineno, pycompat.bytestr(cmd))) | ||
Alexis S. L. Carvalho
|
r5376 | errs += 1 | ||
cmd = lex.get_token() | ||||
return errs | ||||
def lookup(self, name, mapping): | ||||
Huayang
|
r17797 | name = normalize(name) | ||
Alexis S. L. Carvalho
|
r5376 | for pre, suf in rpairs(name): | ||
try: | ||||
return mapping[pre], pre, suf | ||||
Peter Arrenbrecht
|
r7875 | except KeyError: | ||
Alexis S. L. Carvalho
|
r5376 | pass | ||
return '', name, '' | ||||
Durham Goode
|
r26036 | def istargetfile(self, filename): | ||
"""Return true if the given target filename is covered as a destination | ||||
of the filemap. This is useful for identifying what parts of the target | ||||
repo belong to the source repo and what parts don't.""" | ||||
if self.targetprefixes is None: | ||||
self.targetprefixes = set() | ||||
for before, after in self.rename.iteritems(): | ||||
self.targetprefixes.add(after) | ||||
# If "." is a target, then all target files are considered from the | ||||
# source. | ||||
if not self.targetprefixes or '.' in self.targetprefixes: | ||||
return True | ||||
filename = normalize(filename) | ||||
for pre, suf in rpairs(filename): | ||||
# This check is imperfect since it doesn't account for the | ||||
# include/exclude list, but it should work in filemaps that don't | ||||
# apply include/exclude to the same source directories they are | ||||
# renaming. | ||||
if pre in self.targetprefixes: | ||||
return True | ||||
return False | ||||
Alexis S. L. Carvalho
|
r5376 | 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 = '' | ||||
Stefan Simek
|
r9884 | if (not self.include and exc) or (len(inc) <= len(exc)): | ||
Alexis S. L. Carvalho
|
r5376 | return None | ||
newpre, pre, suf = self.lookup(name, self.rename) | ||||
if newpre: | ||||
if newpre == '.': | ||||
return suf | ||||
if suf: | ||||
Matt Mackall
|
r15565 | if newpre.endswith('/'): | ||
return newpre + suf | ||||
Alexis S. L. Carvalho
|
r5376 | return newpre + '/' + suf | ||
return newpre | ||||
return name | ||||
def active(self): | ||||
return bool(self.include or self.exclude or self.rename) | ||||
Alexis S. L. Carvalho
|
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 | ||||
Patrick Mezard
|
r11134 | # returned by getchanges to be able to find things later in getfile. | ||
Alexis S. L. Carvalho
|
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. | ||||
timeless
|
r28367 | class filemap_source(common.converter_source): | ||
Alexis S. L. Carvalho
|
r5377 | def __init__(self, ui, baseconverter, filemap): | ||
Matt Harbison
|
r35168 | super(filemap_source, self).__init__(ui, baseconverter.repotype) | ||
Alexis S. L. Carvalho
|
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 = {} | ||||
Alexis S. L. Carvalho
|
r5401 | self.children = {} | ||
self.seenchildren = {} | ||||
Durham Goode
|
r25742 | # experimental config: convert.ignoreancestorcheck | ||
self.ignoreancestorcheck = self.ui.configbool('convert', | ||||
'ignoreancestorcheck') | ||||
Alexis S. L. Carvalho
|
r5377 | |||
Patrick Mezard
|
r5799 | def before(self): | ||
self.base.before() | ||||
def after(self): | ||||
self.base.after() | ||||
Bryan O'Sullivan
|
r5510 | def setrevmap(self, revmap): | ||
Alexis S. L. Carvalho
|
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} | ||||
Martin Geisler
|
r8150 | dummyset = set() | ||
Alexis S. L. Carvalho
|
r5377 | converted = [] | ||
Bryan O'Sullivan
|
r5510 | for rev in revmap.order: | ||
Alexis S. L. Carvalho
|
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 | ||||
Bryan O'Sullivan
|
r5510 | return self.base.setrevmap(revmap) | ||
Alexis S. L. Carvalho
|
r5377 | |||
def rebuild(self): | ||||
if self._rebuilt: | ||||
return True | ||||
self._rebuilt = True | ||||
self.parentmap.clear() | ||||
self.wantedancestors.clear() | ||||
Alexis S. L. Carvalho
|
r5401 | self.seenchildren.clear() | ||
Alexis S. L. Carvalho
|
r5377 | for rev, wanted, arg in self.convertedorder: | ||
Alexis S. L. Carvalho
|
r5401 | if rev not in self.origparents: | ||
Mads Kiilerich
|
r19863 | try: | ||
self.origparents[rev] = self.getcommit(rev).parents | ||||
except error.RepoLookupError: | ||||
self.ui.debug("unknown revmap source: %s\n" % rev) | ||||
continue | ||||
Alexis S. L. Carvalho
|
r5401 | if arg is not None: | ||
self.children[arg] = self.children.get(arg, 0) + 1 | ||||
for rev, wanted, arg in self.convertedorder: | ||||
Mads Kiilerich
|
r19863 | try: | ||
parents = self.origparents[rev] | ||||
except KeyError: | ||||
continue # unknown revmap source | ||||
Alexis S. L. Carvalho
|
r5377 | if wanted: | ||
self.mark_wanted(rev, parents) | ||||
else: | ||||
self.mark_not_wanted(rev, arg) | ||||
Alexis S. L. Carvalho
|
r5401 | self._discard(arg, *parents) | ||
Alexis S. L. Carvalho
|
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. | ||||
Alexis S. L. Carvalho
|
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 | ||||
Matt Harbison
|
r41215 | def numcommits(self): | ||
return self.base.numcommits() | ||||
Patrick Mezard
|
r13968 | def _cachedcommit(self, rev): | ||
if rev in self.commits: | ||||
return self.commits[rev] | ||||
return self.base.getcommit(rev) | ||||
Alexis S. L. Carvalho
|
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]: | ||||
Mads Kiilerich
|
r19862 | self.wantedancestors.pop(r, None) | ||
self.parentmap.pop(r, None) | ||||
Alexis S. L. Carvalho
|
r5401 | del self.seenchildren[r] | ||
if self._rebuilt: | ||||
del self.children[r] | ||||
Alexis S. L. Carvalho
|
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: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_("source repository doesn't support --filemap")) | ||
Alexis S. L. Carvalho
|
r5377 | for f in files: | ||
if self.filemapper(f): | ||||
return True | ||||
Matt Harbison
|
r41216 | |||
# The include directive is documented to include nothing else (though | ||||
# valid branch closes are included). | ||||
if self.filemapper.include: | ||||
return False | ||||
# Allow empty commits in the source revision through. The getchanges() | ||||
# method doesn't even bother calling this if it determines that the | ||||
# close marker is significant (i.e. all of the branch ancestors weren't | ||||
# eliminated). Therefore if there *is* a close marker, getchanges() | ||||
# doesn't consider it significant, and this revision should be dropped. | ||||
return not files and 'close' not in self.commits[rev].extra | ||||
Alexis S. L. Carvalho
|
r5377 | |||
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 | ||||
Martin von Zweigbergk
|
r32291 | self.wantedancestors[rev] = {SKIPREV} | ||
Alexis S. L. Carvalho
|
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. | ||||
Martin Geisler
|
r8150 | wrev = set() | ||
Alexis S. L. Carvalho
|
r5377 | for p in parents: | ||
Mads Kiilerich
|
r19862 | if p in self.wantedancestors: | ||
wrev.update(self.wantedancestors[p]) | ||||
else: | ||||
self.ui.warn(_('warning: %s parent %s is missing\n') % | ||||
(rev, p)) | ||||
Alexis S. L. Carvalho
|
r5377 | wrev.add(rev) | ||
self.wantedancestors[rev] = wrev | ||||
Mads Kiilerich
|
r22300 | def getchanges(self, rev, full): | ||
Alexis S. L. Carvalho
|
r5377 | parents = self.commits[rev].parents | ||
Durham Goode
|
r25742 | if len(parents) > 1 and not self.ignoreancestorcheck: | ||
Alexis S. L. Carvalho
|
r5377 | 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) | ||||
Patrick Mezard
|
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. | ||||
Alexis S. L. Carvalho
|
r5377 | mparents = [] | ||
Patrick Mezard
|
r17103 | knownparents = set() | ||
branch = self.commits[rev].branch | ||||
hasbranchparent = False | ||||
Alexis S. L. Carvalho
|
r5377 | for i, p1 in enumerate(parents): | ||
mp1 = self.parentmap[p1] | ||||
Patrick Mezard
|
r17103 | if mp1 == SKIPREV or mp1 in knownparents: | ||
Alexis S. L. Carvalho
|
r5377 | continue | ||
Durham Goode
|
r25742 | |||
isancestor = (not self.ignoreancestorcheck and | ||||
any(p2 for p2 in parents | ||||
if p1 != p2 and mp1 != self.parentmap[p2] | ||||
and mp1 in self.wantedancestors[p2])) | ||||
Patrick Mezard
|
r17103 | 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: | ||||
Alexis S. L. Carvalho
|
r5377 | wp = 0 | ||
self.origparents[rev] = parents | ||||
Patrick Mezard
|
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 | ||||
Matt Mackall
|
r11673 | |||
if len(mparents) < 2 and not closed and not self.wanted(rev, wp): | ||||
Alexis S. L. Carvalho
|
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)) | ||||
Alexis S. L. Carvalho
|
r5401 | self._discard(*parents) | ||
Alexis S. L. Carvalho
|
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)) | ||||
Alexis S. L. Carvalho
|
r5401 | self._discard(*parents) | ||
Alexis S. L. Carvalho
|
r5377 | |||
Patrick Mezard
|
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. | ||||
Mads Kiilerich
|
r24395 | changes, copies, cleanp2 = self.base.getchanges(rev, full) | ||
Wagner Bruna
|
r17174 | files = {} | ||
Mads Kiilerich
|
r24395 | ncleanp2 = set(cleanp2) | ||
Alexis S. L. Carvalho
|
r5377 | for f, r in changes: | ||
newf = self.filemapper(f) | ||||
Wagner Bruna
|
r17174 | if newf and (newf != f or newf not in files): | ||
files[newf] = (f, r) | ||||
Mads Kiilerich
|
r24395 | if newf != f: | ||
ncleanp2.discard(f) | ||||
Wagner Bruna
|
r17174 | files = sorted(files.items()) | ||
Alexis S. L. Carvalho
|
r5377 | |||
ncopies = {} | ||||
for c in copies: | ||||
newc = self.filemapper(c) | ||||
if newc: | ||||
newsource = self.filemapper(copies[c]) | ||||
if newsource: | ||||
ncopies[newc] = newsource | ||||
Mads Kiilerich
|
r24395 | return files, ncopies, ncleanp2 | ||
Alexis S. L. Carvalho
|
r5377 | |||
Durham Goode
|
r26036 | def targetfilebelongstosource(self, targetfilename): | ||
return self.filemapper.istargetfile(targetfilename) | ||||
Alexis S. L. Carvalho
|
r5377 | def getfile(self, name, rev): | ||
realname, realrev = rev | ||||
return self.base.getfile(realname, realrev) | ||||
def gettags(self): | ||||
return self.base.gettags() | ||||
Patrick Mezard
|
r8691 | |||
def hasnativeorder(self): | ||||
return self.base.hasnativeorder() | ||||
Patrick Mezard
|
r8693 | |||
def lookuprev(self, rev): | ||||
return self.base.lookuprev(rev) | ||||
etienne
|
r15107 | |||
def getbookmarks(self): | ||||
return self.base.getbookmarks() | ||||
Mads Kiilerich
|
r19892 | |||
def converted(self, rev, sinkrev): | ||||
self.base.converted(rev, sinkrev) | ||||