[PATCH 2 of 6] convert: implement --startrev, support mercurial source
Patrick Mezard
pmezard at gmail.com
Tue Jan 1 23:03:12 UTC 2008
# HG changeset patch
# User Patrick Mezard <pmezard at gmail.com>
# Date 1199228238 -3600
# Node ID 72f64b6d90a18ac753419eebf93b078a0f3fc625
# Parent ffda8917bd5720caa2fe75efa14b55202130cc09
convert: implement --startrev, support mercurial source
diff --git a/hgext/convert/__init__.py b/hgext/convert/__init__.py
--- a/hgext/convert/__init__.py
+++ b/hgext/convert/__init__.py
@@ -26,7 +26,8 @@ def convert(ui, src, dest=None, revmapfi
If no revision is given, all revisions will be converted. Otherwise,
convert will only import up to the named revision (given in a format
- understood by the source).
+ understood by the source). If --startrev is set, only descendants of
+ the start revision will be converted, others are marked as skipped.
If no destination directory name is specified, it defaults to the
basename of the source with '-hg' appended. If the destination
@@ -98,6 +99,7 @@ cmdtable = {
('d', 'dest-type', '', 'destination repository type'),
('', 'filemap', '', 'remap file names using contents of file'),
('r', 'rev', '', 'import up to target revision REV'),
+ ('', 'startrev', '', 'import starting at target revision REV'),
('s', 'source-type', '', 'source repository type'),
('', 'datesort', None, 'try to sort changesets by date')],
'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
diff --git a/hgext/convert/common.py b/hgext/convert/common.py
--- a/hgext/convert/common.py
+++ b/hgext/convert/common.py
@@ -121,6 +121,14 @@ class converter_source(object):
"""
raise NotImplementedError()
+ def getcheckout(self, version):
+ """Return a sorted list of (filename, id) tuples for all files in
+ version, where id is the source revision id of the file.
+
+ This function is only needed to support --startrev
+ """
+ raise NotImplementedError()
+
def converted(self, rev, sinkrev):
'''Notify the source that a revision has been converted.'''
pass
diff --git a/hgext/convert/convcmd.py b/hgext/convert/convcmd.py
--- a/hgext/convert/convcmd.py
+++ b/hgext/convert/convcmd.py
@@ -12,6 +12,7 @@ from hg import mercurial_source, mercuri
from hg import mercurial_source, mercurial_sink
from subversion import debugsvnlog, svn_source, svn_sink
import filemap
+import revstart
import os, shutil
from mercurial import hg, util
@@ -233,8 +234,8 @@ class converter(object):
try:
self.source.before()
self.dest.before()
+ self.ui.status("scanning source...\n")
self.source.setrevmap(self.map)
- self.ui.status("scanning source...\n")
heads = self.source.getheads()
parents = self.walktree(heads)
self.ui.status("sorting...\n")
@@ -293,6 +294,10 @@ def convert(ui, src, dest=None, revmapfi
shutil.rmtree(path, True)
raise
+ if opts.get('startrev'):
+ srcc = revstart.startrev_source(ui, srcc,
+ opts.get('startrev'))
+
fmap = opts.get('filemap')
if fmap:
srcc = filemap.filemap_source(ui, srcc, fmap)
diff --git a/hgext/convert/filemap.py b/hgext/convert/filemap.py
--- a/hgext/convert/filemap.py
+++ b/hgext/convert/filemap.py
@@ -140,6 +140,7 @@ class filemap_source(converter_source):
# We assume the order argument lists the revisions in
# topological order, so that we can infer which revisions were
# wanted by previous runs.
+ res = self.base.setrevmap(revmap)
self._rebuilt = not revmap
seen = {SKIPREV: SKIPREV}
dummyset = util.set()
@@ -158,7 +159,7 @@ class filemap_source(converter_source):
arg = None
converted.append((rev, wanted, arg))
self.convertedorder = converted
- return self.base.setrevmap(revmap)
+ return res
def rebuild(self):
if self._rebuilt:
diff --git a/hgext/convert/hg.py b/hgext/convert/hg.py
--- a/hgext/convert/hg.py
+++ b/hgext/convert/hg.py
@@ -274,6 +274,12 @@ class mercurial_source(converter_source)
return changes[0] + changes[1] + changes[2]
+ def getcheckout(self, rev):
+ ctx = self.changectx(rev)
+ files = [(path, rev) for path in ctx.manifest()]
+ files.sort()
+ return files
+
def converted(self, rev, destrev):
if self.convertfp is None:
self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
diff --git a/hgext/convert/revstart.py b/hgext/convert/revstart.py
new file mode 100644
--- /dev/null
+++ b/hgext/convert/revstart.py
@@ -0,0 +1,133 @@
+# Copyright 2007 Patrick Mezard <pmezard at gmail.com>
+#
+# This software may be used and distributed according to the terms of
+# the GNU General Public License, incorporated herein by reference.
+
+from mercurial.i18n import _
+from mercurial import util
+from common import SKIPREV, converter_source
+
+class NotInitialized(util.Abort):
+ pass
+
+class startrev_source(converter_source):
+ """Restrict a source repository to descendants of a start revision.
+
+ The restricted graph is generated by setrevmap(). It must be called
+ before any operations making use of source revisions.
+ """
+ def __init__(self, ui, baseconverter, startrev):
+ super(startrev_source, self).__init__(ui)
+ self.startrev = startrev
+ self.built = False
+ self.commits = {}
+ self.heads = []
+ self.base = baseconverter
+ self.tags = None
+
+ def before(self):
+ self.base.before()
+
+ def after(self):
+ self.base.after()
+
+ def setrevmap(self, revmap):
+ def walktree(heads):
+ # List unprocessed revisions and non-skipped ones
+ visit = heads
+ known = {}
+ children = {}
+ while visit:
+ n = visit.pop(0)
+ if n in known or revmap.get(n) == SKIPREV:
+ continue
+ known[n] = 1
+ commit = self.base.getcommit(n)
+ self.commits[commit.rev] = commit
+ children.setdefault(commit.rev, [])
+ for p in commit.parents:
+ children.setdefault(p, []).append(commit.rev)
+ visit.append(p)
+ return children
+
+ self.base.setrevmap(revmap)
+ startrev = self.getrevid(self.startrev)
+ children = walktree(self.base.getheads())
+
+ # Compute descendants set
+ if startrev not in children:
+ raise util.Abort(_("cannot find start revision %r") % startrev)
+
+ visit = [startrev]
+ descendants = {}
+ heads = {}
+ while visit:
+ rev = visit.pop()
+ descendants[rev] = self.commits[rev]
+ if not children.get(rev):
+ heads[rev] = 1
+ else:
+ for c in children[rev]:
+ visit.append(c)
+ self.heads = heads.keys()
+
+ # Mark non-descendants as SKIPREV, fix descendants parents
+ for c in self.commits.itervalues():
+ if c.rev in revmap:
+ continue
+ if c.rev in descendants:
+ c.parents = [p for p in c.parents if p in descendants]
+ else:
+ revmap[c.rev] = SKIPREV
+ self.ui.note(_('ignoring revision %s\n') % c.rev)
+
+ self.commits = descendants
+ self.built = True
+
+ def getheads(self):
+ if not self.built:
+ raise NotInitialized()
+ return self.heads
+
+ def getfile(self, name, rev):
+ return self.base.getfile(name, rev)
+
+ def getmode(self, name, rev):
+ return self.base.getmode(name, rev)
+
+ def getchanges(self, version):
+ if not self.built:
+ raise NotInitialized()
+ commit = self.commits[version]
+ if commit.parents:
+ return self.base.getchanges(version)
+ try:
+ return self.base.getcheckout(version), {}
+ except NotImplementedError:
+ raise util.Abort(_("source repository doesn't support --startrev"))
+
+ def getcommit(self, version):
+ if not self.built:
+ raise NotInitialized()
+ return self.commits[version]
+
+ def gettags(self):
+ if not self.built:
+ raise NotInitialized()
+ if self.tags is None:
+ tags = self.base.gettags()
+ self.tags = dict([(n,r) for n,r in tags.iteritems()
+ if r in self.commits])
+ return self.tags
+
+ def getrevid(self, rev):
+ return self.base.getrevid(rev)
+
+ def getchangedfiles(self, rev, i):
+ if i is None:
+ return [f[0] for f in self.getchanges(rev)[0]]
+ return self.base.getchangedfiles(rev, i)
+
+ def converted(self, rev, sinkrev):
+ return self.base.converted(rev, sinkrev)
+
diff --git a/tests/test-convert-shallow b/tests/test-convert-shallow
new file mode 100755
--- /dev/null
+++ b/tests/test-convert-shallow
@@ -0,0 +1,81 @@
+#!/bin/sh
+
+echo "[extensions]" >> $HGRCPATH
+echo "convert = " >> $HGRCPATH
+echo "graphlog = " >> $HGRCPATH
+
+glog()
+{
+ hg glog --template '#rev# "#desc|firstline#" tags: #tags# files: #files#\n' "$@"
+}
+
+hg init source
+cd source
+echo a > a
+mkdir dir
+echo b > dir/b
+hg ci -Am init
+echo a >> a
+hg ci -m changea
+hg up -C 0
+echo b >> dir/b
+hg ci -m changeb
+hg tag -l startrev
+hg merge
+hg ci -m merge
+hg tag -l merge
+echo a >> a
+hg ci -m changea
+hg up -C null
+echo c >> c
+mkdir dir
+echo d > dir/d
+hg ci -Am addc
+hg tag -l single
+hg up -C merge
+hg merge single
+hg ci -m mergesingle
+hg tag -l tip1
+glog
+cd ..
+
+echo % convert last revision
+hg convert --startrev tip source dest-tip
+(cd dest-tip; glog)
+
+echo % full conversion from startrev
+hg convert --startrev startrev source dest
+(cd dest; glog)
+
+cd source
+hg up -C 0
+# Start a branch from startrev ancestors
+echo e > e
+hg ci -Am adde
+hg tag -l branchbefore
+# Start a branch after startrev
+hg up -C startrev
+echo f > f
+hg ci -Am addf
+hg tag -l branchafter
+# Merge back both branches with a startrev descendant
+hg up -C tip1
+hg merge branchbefore
+hg ci -m mergebefore
+hg merge branchafter
+hg ci -m mergeafter
+glog
+cd ..
+
+echo % incremental update from startrev
+hg convert --startrev startrev source dest
+hg convert source dest
+(cd dest; glog)
+
+echo % partial conversion from startrev
+cat > filemap <<EOF
+exclude dir
+EOF
+hg convert --startrev startrev --filemap filemap source dest-partial
+(cd dest-partial; glog)
+
diff --git a/tests/test-convert-shallow.out b/tests/test-convert-shallow.out
new file mode 100644
--- /dev/null
+++ b/tests/test-convert-shallow.out
@@ -0,0 +1,145 @@
+adding a
+adding dir/b
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+adding c
+adding dir/d
+2 files updated, 0 files merged, 2 files removed, 0 files unresolved
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+@ 6 "mergesingle" tags: tip1 tip files:
+|\
+| o 5 "addc" tags: single files: c dir/d
+|
+| o 4 "changea" tags: files: a
+|/
+o 3 "merge" tags: merge files:
+|\
+| o 2 "changeb" tags: startrev files: dir/b
+| |
+o | 1 "changea" tags: files: a
+|/
+o 0 "init" tags: files: a dir/b
+
+% convert last revision
+initializing destination dest-tip repository
+scanning source...
+sorting...
+converting...
+0 mergesingle
+updating tags
+o 1 "update tags" tags: tip files: .hgtags
+|
+o 0 "mergesingle" tags: tip1 files: a c dir/b dir/d
+
+% full conversion from startrev
+initializing destination dest repository
+scanning source...
+sorting...
+converting...
+3 changeb
+2 merge
+1 mergesingle
+0 changea
+updating tags
+o 4 "update tags" tags: tip files: .hgtags
+|
+o 3 "changea" tags: files: a
+|
+| o 2 "mergesingle" tags: tip1 files: c dir/d
+|/
+o 1 "merge" tags: merge files: a
+|
+o 0 "changeb" tags: startrev files: a dir/b
+
+2 files updated, 0 files merged, 2 files removed, 0 files unresolved
+adding e
+1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+adding f
+3 files updated, 0 files merged, 1 files removed, 0 files unresolved
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+@ 10 "mergeafter" tags: tip files:
+|\
+| o 9 "mergebefore" tags: files:
+| |\
+o | | 8 "addf" tags: branchafter files: f
+| | |
+| | o 7 "adde" tags: branchbefore files: e
+| | |
+| o | 6 "mergesingle" tags: tip1 files:
+| |\ \
+| | o | 5 "addc" tags: single files: c dir/d
+| | /
+| +---o 4 "changea" tags: files: a
+| | |
+| o | 3 "merge" tags: merge files:
+|/| |
+o---+ 2 "changeb" tags: startrev files: dir/b
+ / /
+o / 1 "changea" tags: files: a
+|/
+o 0 "init" tags: files: a dir/b
+
+% incremental update from startrev
+scanning source...
+sorting...
+converting...
+2 addf
+1 mergebefore
+0 mergeafter
+updating tags
+scanning source...
+sorting...
+converting...
+o 8 "update tags" tags: tip files: .hgtags
+|
+o 7 "mergeafter" tags: files:
+|\
+| o 6 "mergebefore" tags: files: e
+| |
+o | 5 "addf" tags: branchafter files: f
+| |
+| | o 4 "update tags" tags: files: .hgtags
+| | |
+| | o 3 "changea" tags: files: a
+| | |
+| o | 2 "mergesingle" tags: tip1 files: c dir/d
+| |/
+| o 1 "merge" tags: merge files: a
+|/
+o 0 "changeb" tags: startrev files: a dir/b
+
+% partial conversion from startrev
+initializing destination dest-partial repository
+scanning source...
+sorting...
+converting...
+6 changeb
+5 merge
+4 mergesingle
+3 mergebefore
+2 changea
+1 addf
+0 mergeafter
+updating tags
+o 7 "update tags" tags: tip files: .hgtags
+|
+o 6 "mergeafter" tags: files:
+|\
+| o 5 "addf" tags: branchafter files: f
+| |
+| | o 4 "changea" tags: files: a
+| | |
+o | | 3 "mergebefore" tags: files: e
+| | |
+o---+ 2 "mergesingle" tags: tip1 files: c
+ / /
+| o 1 "merge" tags: merge files: a
+|/
+o 0 "changeb" tags: startrev files: a
+
diff --git a/tests/test-convert.out b/tests/test-convert.out
--- a/tests/test-convert.out
+++ b/tests/test-convert.out
@@ -15,7 +15,8 @@ Convert a foreign SCM repository to a Me
If no revision is given, all revisions will be converted. Otherwise,
convert will only import up to the named revision (given in a format
- understood by the source).
+ understood by the source). If --startrev is set, only descendants of
+ the start revision will be converted, others are marked as skipped.
If no destination directory name is specified, it defaults to the
basename of the source with '-hg' appended. If the destination
@@ -79,6 +80,7 @@ options:
-d --dest-type destination repository type
--filemap remap file names using contents of file
-r --rev import up to target revision REV
+ --startrev import starting at target revision REV
-s --source-type source repository type
--datesort try to sort changesets by date
More information about the Mercurial-devel
mailing list