##// END OF EJS Templates
graphlog: extract revset/support functions into cmdutil
graphlog: extract revset/support functions into cmdutil

File last commit:

r16688:cfb66829 default
r17180:ae062916 default
Show More
common.py
445 lines | 13.7 KiB | text/x-python | PythonLexer
Martin Geisler
convert: add copyright and license headers to back-ends
r8250 # common.py - common code for the convert extension
#
# Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
#
# This software may be used and distributed according to the terms of the
Matt Mackall
Update license to GPLv2+
r10263 # GNU General Public License version 2 or any later version.
Martin Geisler
convert: add copyright and license headers to back-ends
r8250
Bryan O'Sullivan
convert: abstract map files into a class
r5510 import base64, errno
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832 import os
Patrick Mezard
convert: replace fork with subprocess call.
r5127 import cPickle as pickle
Martin Geisler
removed unused imports
r8656 from mercurial import util
Bryan O'Sullivan
convert: add support for Subversion as a sink
r5513 from mercurial.i18n import _
Patrick Mezard
convert: replace fork with subprocess call.
r5127
Patrick Mezard
convert: simplify getargmax() with propertycache
r15606 propertycache = util.propertycache
Patrick Mezard
convert: replace fork with subprocess call.
r5127 def encodeargs(args):
def encodearg(s):
lines = base64.encodestring(s)
lines = [l.splitlines()[0] for l in lines]
return ''.join(lines)
Thomas Arendsen Hein
Remove trailing spaces, fix indentation
r5143
Patrick Mezard
convert: replace fork with subprocess call.
r5127 s = pickle.dumps(args)
return encodearg(s)
def decodeargs(s):
s = base64.decodestring(s)
return pickle.loads(s)
Brendan Cully
Split convert extension into common and repository type modules
r4536
Matt Mackall
many, many trivial check-code fixups
r10282 class MissingTool(Exception):
pass
Patrick Mezard
convert: allow missing tools not to stop source type detection
r6332
def checktool(exe, name=None, abort=True):
Patrick Mezard
convert: fail if an external required tool is not found
r5497 name = name or exe
Adrian Buehlmann
rename util.find_exe to findexe
r14271 if not util.findexe(exe):
Patrick Mezard
convert: allow missing tools not to stop source type detection
r6332 exc = abort and util.Abort or MissingTool
raise exc(_('cannot find required "%s" tool') % name)
Patrick Mezard
convert: fail if an external required tool is not found
r5497
Matt Mackall
many, many trivial check-code fixups
r10282 class NoRepo(Exception):
pass
Brendan Cully
Split convert extension into common and repository type modules
r4536
Alexis S. L. Carvalho
convert: change SKIPREV to 'SKIP'
r5400 SKIPREV = 'SKIP'
Alexis S. L. Carvalho
convert: allow the converter_source to say "skip this revision"...
r5374
Brendan Cully
Split convert extension into common and repository type modules
r4536 class commit(object):
Bryan O'Sullivan
convert: make contents of "extra" dict available from sources, for sinks....
r5439 def __init__(self, author, date, desc, parents, branch=None, rev=None,
Patrick Mezard
convert: add --sourcesort option for source specific sort...
r8690 extra={}, sortkey=None):
Alexis S. L. Carvalho
convert: use 'unknown' and '0 0' if commit author or date weren't specified...
r5984 self.author = author or 'unknown'
self.date = date or '0 0'
Bryan O'Sullivan
convert: empty log messages are OK as of 7f5c3fb0a37d
r5024 self.desc = desc
Bryan O'Sullivan
convert: make commit constructor clearer and less magical
r5012 self.parents = parents
self.branch = branch
self.rev = rev
Bryan O'Sullivan
convert: make contents of "extra" dict available from sources, for sinks....
r5439 self.extra = extra
Patrick Mezard
convert: add --sourcesort option for source specific sort...
r8690 self.sortkey = sortkey
Brendan Cully
Split convert extension into common and repository type modules
r4536
class converter_source(object):
"""Conversion source interface"""
Bryan O'Sullivan
convert: some tidyups, doc improvements, and test fixes...
r5556 def __init__(self, ui, path=None, rev=None):
Brendan Cully
Split convert extension into common and repository type modules
r4536 """Initialize conversion source (or raise NoRepo("message")
exception if path is not a valid repository)"""
Brendan Cully
convert: move some code into common init function
r4810 self.ui = ui
self.path = path
self.rev = rev
self.encoding = 'utf-8'
Brendan Cully
convert: export revmap to source....
r4812
Bryan O'Sullivan
convert: add before/after hooks for converter sources
r5356 def before(self):
pass
def after(self):
pass
Bryan O'Sullivan
convert: abstract map files into a class
r5510 def setrevmap(self, revmap):
"""set the map of already-converted revisions"""
Brendan Cully
convert: svn: use revmap to parse only new revisions in incremental conversions
r4813 pass
Brendan Cully
Split convert extension into common and repository type modules
r4536
def getheads(self):
"""Return a list of this repository's heads"""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Brendan Cully
Split convert extension into common and repository type modules
r4536
def getfile(self, name, rev):
Patrick Mezard
convert: merge sources getmode() into getfile()
r11134 """Return a pair (data, mode) where data is the file content
as a string and mode one of '', 'x' or 'l'. rev is the
identifier returned by a previous call to getchanges(). Raise
IOError to indicate that name was deleted in rev.
Patrick Mezard
convert: improve convert_source documentation
r7055 """
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Brendan Cully
Split convert extension into common and repository type modules
r4536
def getchanges(self, version):
Dirkjan Ochtman
clean up trailing spaces, leading spaces in C
r7186 """Returns a tuple of (files, copies).
Patrick Mezard
convert: improve convert_source documentation
r7055
files is a sorted list of (filename, id) tuples for all files
Greg Ward
convert: improve docstrings, comments.
r8444 changed between version and its first parent returned by
Patrick Mezard
convert: improve convert_source documentation
r7055 getcommit(). id is the source revision id of the file.
Brendan Cully
Split convert extension into common and repository type modules
r4536
Brendan Cully
convert: look up copies in getchanges instead of getcommit...
r5121 copies is a dictionary of dest: source
"""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Brendan Cully
Split convert extension into common and repository type modules
r4536
def getcommit(self, version):
"""Return the commit object for version"""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Brendan Cully
Split convert extension into common and repository type modules
r4536
def gettags(self):
Patrick Mezard
convert/svn: test svn tags encoding
r8887 """Return the tags as a dictionary of name: revision
Tag names must be UTF-8 strings.
"""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Brendan Cully
Split convert extension into common and repository type modules
r4536
Brendan Cully
convert: ove recode method into converter_source
r4759 def recode(self, s, encoding=None):
if not encoding:
Brendan Cully
convert: move some code into common init function
r4810 encoding = self.encoding or 'utf-8'
Thomas Arendsen Hein
removed trailing whitespace
r4957
Thomas Arendsen Hein
Don't decode unicode strings....
r5287 if isinstance(s, unicode):
return s.encode("utf-8")
Brendan Cully
convert: ove recode method into converter_source
r4759 try:
return s.decode(encoding).encode("utf-8")
Brodie Rao
cleanup: replace naked excepts with more specific ones
r16688 except UnicodeError:
Brendan Cully
convert: ove recode method into converter_source
r4759 try:
return s.decode("latin-1").encode("utf-8")
Brodie Rao
cleanup: replace naked excepts with more specific ones
r16688 except UnicodeError:
Brendan Cully
convert: ove recode method into converter_source
r4759 return s.decode(encoding, "replace").encode("utf-8")
Alexis S. L. Carvalho
convert: readd --filemap...
r5377 def getchangedfiles(self, rev, i):
"""Return the files changed by rev compared to parent[i].
Thomas Arendsen Hein
Removed tabs and trailing whitespace in python files
r5760
Alexis S. L. Carvalho
convert: readd --filemap...
r5377 i is an index selecting one of the parents of rev. The return
value should be the list of files that are different in rev and
this parent.
If rev has no parents, i is None.
Thomas Arendsen Hein
Removed tabs and trailing whitespace in python files
r5760
Alexis S. L. Carvalho
convert: readd --filemap...
r5377 This function is only needed to support --filemap
"""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Alexis S. L. Carvalho
convert: readd --filemap...
r5377
Bryan O'Sullivan
convert: tell the source repository when a rev has been converted...
r5554 def converted(self, rev, sinkrev):
'''Notify the source that a revision has been converted.'''
pass
Patrick Mezard
convert: fail fast if source does not support --sourcesort
r8691 def hasnativeorder(self):
"""Return true if this source has a meaningful, native revision
order. For instance, Mercurial revisions are store sequentially
while there is no such global ordering with Darcs.
"""
return False
Patrick Mezard
convert: rewrite tags when converting from hg to hg
r8693 def lookuprev(self, rev):
"""If rev is a meaningful revision reference in source, return
the referenced identifier in the same format used by getcommit().
return None otherwise.
"""
return None
Bryan O'Sullivan
convert: tell the source repository when a rev has been converted...
r5554
Edouard Gomez
convert: add bookmark support to common sink/source implementation
r13744 def getbookmarks(self):
"""Return the bookmarks as a dictionary of name: revision
Bookmark names are to be UTF-8 strings.
"""
return {}
Brendan Cully
Split convert extension into common and repository type modules
r4536 class converter_sink(object):
"""Conversion sink (target) interface"""
def __init__(self, ui, path):
"""Initialize conversion sink (or raise NoRepo("message")
Bryan O'Sullivan
convert: refactor sink initialisation, to remove hardcoding of hg...
r5441 exception if path is not a valid repository)
created is a list of paths to remove if a fatal error occurs
later"""
self.ui = ui
Bryan O'Sullivan
convert: add default constructor for converter_sink
r5440 self.path = path
Bryan O'Sullivan
convert: refactor sink initialisation, to remove hardcoding of hg...
r5441 self.created = []
Brendan Cully
Split convert extension into common and repository type modules
r4536
def getheads(self):
"""Return a list of this repository's heads"""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Brendan Cully
Split convert extension into common and repository type modules
r4536
Bryan O'Sullivan
convert: rename mapfile to revmapfile, so we can map more than just revs
r5011 def revmapfile(self):
Brendan Cully
Split convert extension into common and repository type modules
r4536 """Path to a file that will contain lines
source_rev_id sink_rev_id
mapping equivalent revision identifiers for each system."""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Brendan Cully
Split convert extension into common and repository type modules
r4536
Edouard Gomez
convert extension: Add support for username mapping...
r4589 def authorfile(self):
"""Path to a file that will contain lines
srcauthor=dstauthor
mapping equivalent authors identifiers for each system."""
Brendan Cully
convert: fix various authormap handling bugs
r4590 return None
Edouard Gomez
convert extension: Add support for username mapping...
r4589
Patrick Mezard
convert: rewrite tags when converting from hg to hg
r8693 def putcommit(self, files, copies, parents, commit, source, revmap):
Brendan Cully
Split convert extension into common and repository type modules
r4536 """Create a revision with all changed files listed in 'files'
Patrick Mezard
convert: rewrite tags when converting from hg to hg
r8693 and having listed parents. 'commit' is a commit object
containing at a minimum the author, date, and message for this
changeset. 'files' is a list of (path, version) tuples,
'copies' is a dictionary mapping destinations to sources,
'source' is the source repository, and 'revmap' is a mapfile
Patrick Mezard
convert: merge sources getmode() into getfile()
r11134 of source revisions to converted revisions. Only getfile() and
lookuprev() should be called on 'source'.
Patrick Mezard
convert: reintegrate file retrieval code in sinks...
r6716
Note that the sink repository is not told to update itself to
a particular revision (or even what that revision would be)
before it receives the file data.
"""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Brendan Cully
Split convert extension into common and repository type modules
r4536
def puttags(self, tags):
"""Put tags into sink.
Patrick Mezard
convert/svn: test svn tags encoding
r8887
tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
Patrick Mezard
convert: fix history topology when using hg.tagsbranch...
r9431 Return a pair (tag_revision, tag_parent_revision), or (None, None)
if nothing was changed.
Patrick Mezard
convert/svn: test svn tags encoding
r8887 """
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Patrick Mezard
convert: replace fork with subprocess call.
r5127
Patrick Mezard
convert: hg.clonebranches must pull missing parents (issue941)
r5934 def setbranch(self, branch, pbranches):
Patrick Mezard
convert: reintegrate file retrieval code in sinks...
r6716 """Set the current branch name. Called before the first putcommit
Brendan Cully
convert: hg: optionally create branches as clones...
r5173 on the branch.
branch: branch name for subsequent commits
Patrick Mezard
convert: hg.clonebranches must pull missing parents (issue941)
r5934 pbranches: (converted parent revision, parent branch) tuples"""
Brendan Cully
convert: hg: optionally create branches as clones...
r5173 pass
Alexis S. L. Carvalho
convert: add a mode where mercurial_sink skips empty revisions....
r5378
def setfilemapmode(self, active):
"""Tell the destination that we're using a filemap
Some converter_sources (svn in particular) can claim that a file
was changed in a revision, even if there was no change. This method
tells the destination that we're using a filemap and that it should
filter empty revisions.
"""
pass
Bryan O'Sullivan
convert: abstract map files into a class
r5510
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512 def before(self):
pass
def after(self):
pass
Edouard Gomez
convert: add bookmark support to common sink/source implementation
r13744 def putbookmarks(self, bookmarks):
"""Put bookmarks into sink.
bookmarks: {bookmarkname: sink_rev_id, ...}
where bookmarkname is an UTF-8 string.
"""
pass
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512
Patrick Mezard
convert: use splicemap entries when sorting revisions (issue1748)...
r16106 def hascommit(self, rev):
"""Return True if the sink contains rev"""
Brodie Rao
cleanup: "raise SomeException()" -> "raise SomeException"
r16687 raise NotImplementedError
Patrick Mezard
convert: use splicemap entries when sorting revisions (issue1748)...
r16106
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512 class commandline(object):
def __init__(self, ui, command):
self.ui = ui
self.command = command
def prerun(self):
pass
def postrun(self):
pass
Daniel Atallah
convert: add support to common commandline to access stdin of the process
r13759 def _cmdline(self, cmd, closestdin, *args, **kwargs):
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512 cmdline = [self.command, cmd] + list(args)
for k, v in kwargs.iteritems():
if len(k) == 1:
cmdline.append('-' + k)
else:
cmdline.append('--' + k.replace('_', '-'))
try:
if len(k) == 1:
cmdline.append('' + v)
else:
cmdline[-1] += '=' + v
except TypeError:
pass
cmdline = [util.shellquote(arg) for arg in cmdline]
Patrick Mezard
convert: display child command output if --debug (fix 878466138b57)
r7611 if not self.ui.debugflag:
Bryan O'Sullivan
convert: print darcs error messages iff --debug
r7610 cmdline += ['2>', util.nulldev]
Daniel Atallah
convert: add support to common commandline to access stdin of the process
r13759 if closestdin:
cmdline += ['<', util.nulldev]
Patrick Mezard
convert: fix util.popen regression in darcs converter
r5529 cmdline = ' '.join(cmdline)
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832 return cmdline
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832 def _run(self, cmd, *args, **kwargs):
Daniel Atallah
convert: add support to common commandline to access stdin of the process
r13759 return self._dorun(util.popen, cmd, True, *args, **kwargs)
def _run2(self, cmd, *args, **kwargs):
return self._dorun(util.popen2, cmd, False, *args, **kwargs)
def _dorun(self, openfunc, cmd, closestdin, *args, **kwargs):
cmdline = self._cmdline(cmd, closestdin, *args, **kwargs)
Martin Geisler
do not attempt to translate ui.debug output
r9467 self.ui.debug('running: %s\n' % (cmdline,))
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512 self.prerun()
try:
Daniel Atallah
convert: add support to common commandline to access stdin of the process
r13759 return openfunc(cmdline)
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512 finally:
self.postrun()
def run(self, cmd, *args, **kwargs):
fp = self._run(cmd, *args, **kwargs)
output = fp.read()
self.ui.debug(output)
return output, fp.close()
Aleix Conchillo Flaque
convert: added GNU Arch source converter
r6035 def runlines(self, cmd, *args, **kwargs):
fp = self._run(cmd, *args, **kwargs)
output = fp.readlines()
Aleix Conchillo Flaque
convert: improve gnu arch source performance and other fixes...
r6049 self.ui.debug(''.join(output))
Aleix Conchillo Flaque
convert: added GNU Arch source converter
r6035 return output, fp.close()
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512 def checkexit(self, status, output=''):
if status:
if output:
self.ui.warn(_('%s error:\n') % self.command)
self.ui.warn(output)
Adrian Buehlmann
rename explain_exit to explainexit
r14234 msg = util.explainexit(status)[0]
Martin Geisler
convert: do not ask for translation of "%s %s"
r8970 raise util.Abort('%s %s' % (self.command, msg))
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512
def run0(self, cmd, *args, **kwargs):
output, status = self.run(cmd, *args, **kwargs)
self.checkexit(status, output)
return output
Aleix Conchillo Flaque
convert: added GNU Arch source converter
r6035 def runlines0(self, cmd, *args, **kwargs):
output, status = self.runlines(cmd, *args, **kwargs)
Aleix Conchillo Flaque
convert: improve gnu arch source performance and other fixes...
r6049 self.checkexit(status, ''.join(output))
Aleix Conchillo Flaque
convert: added GNU Arch source converter
r6035 return output
Patrick Mezard
convert: simplify getargmax() with propertycache
r15606 @propertycache
def argmax(self):
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832 # POSIX requires at least 4096 bytes for ARG_MAX
Patrick Mezard
convert: simplify getargmax() with propertycache
r15606 argmax = 4096
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832 try:
Patrick Mezard
convert: simplify getargmax() with propertycache
r15606 argmax = os.sysconf("SC_ARG_MAX")
Brodie Rao
cleanup: replace naked excepts with more specific ones
r16688 except (AttributeError, ValueError):
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832 pass
# Windows shells impose their own limits on command line length,
# down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
# for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
# details about cmd.exe limitations.
# Since ARG_MAX is for command line _and_ environment, lower our limit
# (and make happy Windows shells while doing this).
Martin Geisler
Use explicit integer division...
r15791 return argmax // 2 - 1
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832
Daniel Atallah
convert: add support to common commandline to access stdin of the process
r13759 def limit_arglist(self, arglist, cmd, closestdin, *args, **kwargs):
cmdlen = len(self._cmdline(cmd, closestdin, *args, **kwargs))
Patrick Mezard
convert: simplify getargmax() with propertycache
r15606 limit = self.argmax - cmdlen
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832 bytes = 0
fl = []
for fn in arglist:
b = len(fn) + 3
if bytes + b < limit or len(fl) == 0:
fl.append(fn)
bytes += b
else:
yield fl
fl = [fn]
bytes = b
if fl:
yield fl
def xargs(self, arglist, cmd, *args, **kwargs):
Daniel Atallah
convert: add support to common commandline to access stdin of the process
r13759 for l in self.limit_arglist(arglist, cmd, True, *args, **kwargs):
Maxim Dounin
convert: add commandline.xargs(), use it in svn_sink class...
r5832 self.run0(cmd, *(list(args) + l), **kwargs)
Bryan O'Sullivan
convert: abstract map files into a class
r5510
class mapfile(dict):
def __init__(self, ui, path):
super(mapfile, self).__init__()
self.ui = ui
self.path = path
self.fp = None
self.order = []
self._read()
def _read(self):
Stefan Rusek
Handle when the slicemap option is an empty string...
r7774 if not self.path:
Bryan O'Sullivan
convert: allow synthetic history to be spliced in....
r5996 return
Bryan O'Sullivan
convert: abstract map files into a class
r5510 try:
fp = open(self.path, 'r')
except IOError, err:
if err.errno != errno.ENOENT:
raise
return
Patrick Mezard
convert: better mapfile parsing errors (issue1581/1)
r8047 for i, line in enumerate(fp):
Patrick Mezard
convert: ignore blank lines in mapfiles (issue3286)
r16190 line = line.splitlines()[0].rstrip()
if not line:
# Ignore blank lines
continue
Patrick Mezard
convert: better mapfile parsing errors (issue1581/1)
r8047 try:
Patrick Mezard
convert: ignore blank lines in mapfiles (issue3286)
r16190 key, value = line.rsplit(' ', 1)
Patrick Mezard
convert: better mapfile parsing errors (issue1581/1)
r8047 except ValueError:
Matt Mackall
many, many trivial check-code fixups
r10282 raise util.Abort(
_('syntax error in %s(%d): key/value pair expected')
% (self.path, i + 1))
Bryan O'Sullivan
convert: abstract map files into a class
r5510 if key not in self:
self.order.append(key)
super(mapfile, self).__setitem__(key, value)
fp.close()
Thomas Arendsen Hein
Removed tabs and trailing whitespace in python files
r5760
Bryan O'Sullivan
convert: abstract map files into a class
r5510 def __setitem__(self, key, value):
if self.fp is None:
try:
self.fp = open(self.path, 'a')
except IOError, err:
raise util.Abort(_('could not open map file %r: %s') %
(self.path, err.strerror))
self.fp.write('%s %s\n' % (key, value))
self.fp.flush()
super(mapfile, self).__setitem__(key, value)
def close(self):
Bryan O'Sullivan
convert: abstract darcs's commandline handling
r5512 if self.fp:
self.fp.close()
self.fp = None
Patrick Mezard
convert: turn splicemap into a simple dictionary...
r16105
def parsesplicemap(path):
"""Parse a splicemap, return a child/parents dictionary."""
Matt Mackall
convert: deal with empty splicemap path (issue3311)
r16265 if not path:
return {}
Patrick Mezard
convert: turn splicemap into a simple dictionary...
r16105 m = {}
try:
fp = open(path, 'r')
for i, line in enumerate(fp):
Patrick Mezard
convert: ignore blank lines in mapfiles (issue3286)
r16190 line = line.splitlines()[0].rstrip()
if not line:
# Ignore blank lines
continue
Patrick Mezard
convert: turn splicemap into a simple dictionary...
r16105 try:
Patrick Mezard
convert: ignore blank lines in mapfiles (issue3286)
r16190 child, parents = line.split(' ', 1)
Patrick Mezard
convert: turn splicemap into a simple dictionary...
r16105 parents = parents.replace(',', ' ').split()
except ValueError:
raise util.Abort(_('syntax error in %s(%d): child parent1'
'[,parent2] expected') % (path, i + 1))
pp = []
for p in parents:
if p not in pp:
pp.append(p)
m[child] = pp
except IOError, e:
if e.errno != errno.ENOENT:
raise
return m