[PATCH] support creating historically correct merge points
Hi folks,
I've been converting my fairly large, multi-branch repository with
hgsubversion and it's worked great. The one thing that was missing for
me was the ability to record merges, because I have several branches
which have been merged at several points, and having the correct
history is very useful.
The 'gold standard' solution would be to parse the svn:merge and
svnmerge properties looking for merge commits and to generate this
dynamically, however I'm a little too short of time to tackle that.
Instead I've implemented an equivalent of the --splicemap option in
the regular 'hg convert' - a feature that I hasten to add I could
never get to work. My version is called --mergemap instead and is
considerably easier to use, all you need to do is supply a text file
with 2 branch <at> revision entries - the first is the revision at which
the merge took place, and the second is the 'second parent' from which
the merge is happening. The first parent does not need to be supplied,
unlike in --splicemap, since that's available anyway.
I've tried to stick to your conventions, I hope it's useful. I've just
run my repository through it (9200+ revisions, 31 branches) with a
representative 14 merge points, sometimes multiple times between the
same branches, and it all seems to work great.
# HG changeset patch
# User Steve Streeting <steve@...>
# Date 1256561021 0
# Node ID 94b2e5bad7c4c8976a92e1c0aaf9a1c48a00db8f
# Parent 1fd3cfa47c5e21b0e9a1211ee6c677abad459a33
Added support for 'mergemap' file. This allows you to specify merge
points in your SVN repository and have those reflected in your
converted Mercurial repository.
diff -r 1fd3cfa47c5e -r 94b2e5bad7c4 hgsubversion/__init__.py
--- a/hgsubversion/__init__.py Fri Oct 16 23:33:41 2009 -0400
+++ b/hgsubversion/__init__.py Mon Oct 26 12:43:41 2009 +0000
<at> <at> -64,6 +64,8 <at> <at>
'list of paths to search for tags in Subversion
repositories'),
('A', 'authors', '',
'file mapping Subversion usernames to Mercurial authors'),
+ ('M', 'mergemap', '',
+ 'file mapping Subversion merge points to Mercurial merges'),
('', 'filemap', '',
'file containing rules for remapping Subversion repository
paths'),
('', 'layout', 'auto', ('import standard layout or single '
<at> <at> -163,6 +165,7 <at> <at>
[('u', 'svn-url', '', 'path to the Subversion server.'),
('', 'stupid', False, 'be stupid and use diffy replay.'),
('A', 'authors', '', 'username mapping filename'),
+ ('M', 'mergemap', '', 'merge mapping filename'),
('', 'filemap', '',
'remap file to exclude paths or include only certain
paths'),
('', 'force', False, 'force an operation to happen'),
diff -r 1fd3cfa47c5e -r 94b2e5bad7c4 hgsubversion/maps.py
--- a/hgsubversion/maps.py Fri Oct 16 23:33:41 2009 -0400
+++ b/hgsubversion/maps.py Mon Oct 26 12:43:41 2009 +0000
<at> <at> -284,3 +284,85 <at> <at>
msg = 'ignoring bad line in filemap %s: %s\n'
self.ui.warn(msg % (fn, line.rstrip()))
f.close()
+
+class MergeMap(dict):
+ '''A mapping from Subversion revision number to _second_ parent
+ that indicates a merge has taken place here.
+ This allows merge points to be imported.
+
+ The format of the mergemap file is one merge per line, with the
+ first parameter identifying the revision number and branch of the
merge
+ target, and the second parameter identifying the revision number and
branch
+ of the source of the merge, which will become the 'second parent'.
+ For example:
+
+ trunk <at> 9203 release_1.0 <at> 9200
+
+ This means that revision 9203 in trunk will have a second parent
added
+ which is revision 9200 from the release_1.0 branch.
+
+ At some point it would be good to pick this information up
automatically
+ from the svnmerge and svn:merge properties in svn.
+ '''
+
+ def __init__(self, ui, path):
+ '''Initialise a new MergeMap.
+
+ The ui argument is used to print diagnostic messages.
+
+ The path argument is the location of the backing store,
+ typically .hg/mergemap.
+ '''
+ self.ui = ui
+ self.path = path
+ self.super = super(MergeMap, self)
+ self.super.__init__()
+ self.load(path)
+
+ def load(self, path):
+ ''' Load mappings from a file at the specified path. '''
+ if not os.path.exists(path):
+ return
+
+ writing = False
+ if path != self.path:
+ writing = open(self.path, 'a')
+
+ self.ui.note('reading mergemap from %s\n' % path)
+ f = open(path, 'r')
+ for number, line in enumerate(f):
+
+ if writing:
+ writing.write(line)
+
+ line = line.split('#')[0]
+ if not line.strip():
+ continue
+
+ try:
+ src, dst = line.split(' ', 1)
+ except (IndexError, ValueError):
+ msg = 'ignoring line %i in merge map %s: %s\n'
+ self.ui.warn(msg % (number, path, line.rstrip()))
+ continue
+
+ src = src.strip()
+ dst = dst.strip()
+ srcbranch, srcrev = src.split(' <at> ', 1)
+ dstbranch, dstrev = dst.split(' <at> ', 1)
+ srcrev = int(srcrev.strip())
+ srcbranch = srcbranch.strip()
+ dstrev = int(dstrev.strip())
+ dstbranch = dstbranch.strip()
+
+ self.ui.debug('adding merge %s <at> %s %s <at> %s to merge map\n' %
(srcbranch, srcrev, dstbranch, dstrev))
+ if (srcrev,srcbranch) in self and (dstrev,dstbranch) !=
self[srcrev, srcbranch]:
+ msg = 'overriding merge: "%s" to "%s" (%s)\n'
+ self.ui.warn(msg % (self[srcrev, srcbranch], dst,
src))
+ self[srcrev, srcbranch] = dstrev, dstbranch
+
+ f.close()
+ if writing:
+ writing.flush()
+ writing.close()
+
diff -r 1fd3cfa47c5e -r 94b2e5bad7c4 hgsubversion/replay.py
--- a/hgsubversion/replay.py Fri Oct 16 23:33:41 2009 -0400
+++ b/hgsubversion/replay.py Mon Oct 26 12:43:41 2009 +0000
<at> <at> -123,7 +123,8 <at> <at>
del current.emptybranches[branch]
files = dict(files)
- parents = meta.get_parent_revision(rev.revnum, branch),
revlog.nullid
+ secondparent = meta.getsecondparentctx(rev.revnum, branch)
+ parents = meta.get_parent_revision(rev.revnum, branch),
secondparent
if parents[0] in closedrevs and branch in meta.closebranches:
continue
diff -r 1fd3cfa47c5e -r 94b2e5bad7c4 hgsubversion/svnmeta.py
--- a/hgsubversion/svnmeta.py Fri Oct 16 23:33:41 2009 -0400
+++ b/hgsubversion/svnmeta.py Mon Oct 26 12:43:41 2009 +0000
<at> <at> -49,6 +49,7 <at> <at>
author_host = self.ui.config('hgsubversion', 'defaulthost',
uuid)
authors = self.ui.config('hgsubversion', 'authormap')
+ merges = self.ui.config('hgsubversion', 'mergemap')
tag_locations = self.ui.configlist('hgsubversion',
'tagpaths', ['tags'])
self.usebranchnames = self.ui.configbool('hgsubversion',
'usebranchnames',
True)
<at> <at> -86,6 +87,9 <at> <at>
defaulthost=author_host)
if authors: self.authors.load(authors)
+ self.merges = maps.MergeMap(self.ui, self.merges_file)
+ if merges: self.merges.load(merges)
+
self.lastdate = '1970-01-01 00:00:00 -0000'
self.filemap = maps.FileMap(repo)
<at> <at> -150,6 +154,10 <at> <at>
return os.path.join(self.meta_data_dir, 'authors')
<at> property
+ def merges_file(self):
+ return os.path.join(self.meta_data_dir, 'merges')
+
+ <at> property
def layoutfile(self):
return os.path.join(self.meta_data_dir, 'layout')
<at> <at> -593,3 +601,11 <at> <at>
extra)
new = self.repo.commitctx(ctx)
self.ui.status('Marked branch %s as closed.\n' % (branch or
'default'))
+
+ def getsecondparentctx(self, rev, branch):
+ if (rev,branch) in self.merges:
+ return self.revmap[ self.merges[rev, branch] ]
+ # None branch is trunk
+ elif (rev,"trunk") in self.merges:
+ return self.revmap[ self.merges[rev, 'trunk'] ]
+ return revlog.nullid
diff -r 1fd3cfa47c5e -r 94b2e5bad7c4 hgsubversion/wrappers.py
--- a/hgsubversion/wrappers.py Fri Oct 16 23:33:41 2009 -0400
+++ b/hgsubversion/wrappers.py Mon Oct 26 12:43:41 2009 +0000
<at> <at> -356,6 +356,7 <at> <at>
optionmap = {
'tagpaths': ('hgsubversion', 'tagpaths'),
'authors': ('hgsubversion', 'authormap'),
+ 'mergemap': ('hgsubversion', 'mergemap'),
'filemap': ('hgsubversion', 'filemap'),
'stupid': ('hgsubversion', 'stupid'),
'defaulthost': ('hgsubversion', 'defaulthost'),
<at> <at> -364,7 +365,7 <at> <at>
'layout': ('hgsubversion', 'layout'),
}
-dontretain = { 'hgsubversion': set(['authormap', 'filemap',
'layout', ]) }
+dontretain = { 'hgsubversion': set(['authormap', 'mergemap',
'filemap', 'layout', ]) }
def clone(orig, ui, source, dest=None, **opts):
"""