[PATCH 2 of 6] convert: implement --startrev, support mercurial source

Patrick Mezard pmezard at gmail.com
Tue Jan 1 17:03:12 CST 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