changeset 574:8e025a6f0db4

add basic branchmap functionality, to rename branches
author Dirkjan Ochtman <dirkjan@ochtman.nl>
date Mon, 01 Mar 2010 22:10:18 +0100
parents 00393e9abff8
children c278a225b750
files hgsubversion/__init__.py hgsubversion/maps.py hgsubversion/replay.py hgsubversion/stupid.py hgsubversion/svnmeta.py hgsubversion/wrappers.py tests/fixtures/branchmap.sh tests/fixtures/branchmap.svndump tests/test_fetch_mappings.py
diffstat 9 files changed, 279 insertions(+), 11 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/__init__.py
+++ b/hgsubversion/__init__.py
@@ -73,6 +73,7 @@ wrapcmds = { # cmd: generic, target, fix
          'file containing rules for remapping Subversion repository paths'),
         ('', 'layout', 'auto', ('import standard layout or single '
                                 'directory? Can be standard, single, or auto.')),
+        ('', 'branchmap', '', 'file containing rules for branch conversion'),
     ]),
 }
 
--- a/hgsubversion/maps.py
+++ b/hgsubversion/maps.py
@@ -292,3 +292,60 @@ class FileMap(object):
                 msg = 'ignoring bad line in filemap %s: %s\n'
                 self.ui.warn(msg % (fn, line.rstrip()))
         f.close()
+
+class BranchMap(dict):
+    '''Facility for controlled renaming of branch names. Example:
+
+    oldname = newname
+    other = default
+
+    All changes on the oldname branch will now be on the newname branch; all
+    changes on other will now be on default (have no branch name set).
+    '''
+
+    def __init__(self, ui, path):
+        self.ui = ui
+        self.path = path
+        self.super = super(BranchMap, 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 branchmap 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 branch map %s: %s\n'
+                self.ui.warn(msg % (number, path, line.rstrip()))
+                continue
+
+            src = src.strip()
+            dst = dst.strip()
+            self.ui.debug('adding branch %s to branch map\n' % src)
+            if src in self and dst != self[src]:
+                msg = 'overriding branch: "%s" to "%s" (%s)\n'
+                self.ui.warn(msg % (self[src], dst, src))
+            self[src] = dst
+
+        f.close()
+        if writing:
+            writing.flush()
+            writing.close()
--- a/hgsubversion/replay.py
+++ b/hgsubversion/replay.py
@@ -163,8 +163,7 @@ def convert_rev(ui, meta, svn, r, tbdelt
                                       islink=is_link, isexec=is_exec,
                                       copied=copied)
 
-        if not meta.usebranchnames or extra.get('branch', None) == 'default':
-            extra.pop('branch', None)
+        meta.mapbranch(extra)
         current_ctx = context.memctx(meta.repo,
                                      parents,
                                      rev.message or '...',
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -637,8 +637,7 @@ def convert_rev(ui, meta, svn, r, tbdelt
             extra.update({'branch': parentctx.extra().get('branch', None),
                           'close': 1})
 
-        if not meta.usebranchnames or extra.get('branch', None) == 'default':
-            extra.pop('branch', None)
+        meta.mapbranch(extra)
         current_ctx = context.memctx(meta.repo,
                                      [parentctx.node(), revlog.nullid],
                                      r.message or util.default_commit_msg,
--- a/hgsubversion/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -86,6 +86,9 @@ class SVNMeta(object):
         self.authors = maps.AuthorMap(self.ui, self.authors_file,
                                  defaulthost=author_host)
         if authors: self.authors.load(authors)
+        self.branchmap = maps.BranchMap(self.ui, self.branchmapfile)
+        if self.ui.config('hgsubversion', 'branchmap'):
+            self.branchmap.load(self.ui.config('hgsubversion', 'branchmap'))
 
         self.lastdate = '1970-01-01 00:00:00 -0000'
         self.filemap = maps.FileMap(repo)
@@ -152,6 +155,10 @@ class SVNMeta(object):
     def authors_file(self):
         return os.path.join(self.meta_data_dir, 'authors')
 
+    @property
+    def branchmapfile(self):
+        return os.path.join(self.meta_data_dir, 'branchmap')
+
     @property
     def layoutfile(self):
         return os.path.join(self.meta_data_dir, 'layout')
@@ -218,6 +225,17 @@ class SVNMeta(object):
         }
         return extra
 
+    def mapbranch(self, extra, close=False):
+        if close:
+            extra['close'] = 1
+        if extra.get('branch') == 'default':
+            extra.pop('branch', None)
+        mapped = self.branchmap.get(extra.get('branch'))
+        if not self.usebranchnames or mapped == 'default':
+            extra.pop('branch', None)
+        elif mapped:
+            extra['branch'] = mapped
+
     def normalize(self, path):
         '''Normalize a path to strip of leading slashes and our subdir if we
         have one.
@@ -601,13 +619,12 @@ class SVNMeta(object):
                 return context.memfilectx(path='.hgtags', data=src,
                                           islink=False, isexec=False,
                                           copied=None)
+
             extra = self.genextra(rev.revnum, b)
             if fromtag:
                 extra['branch'] = parent.extra().get('branch', 'default')
-            if not self.usebranchnames:
-                extra.pop('branch', None)
-            if b in endbranches or fromtag:
-                extra['close'] = 1
+            self.mapbranch(extra, b in endbranches or fromtag)
+
             ctx = context.memctx(self.repo,
                                  (parent.node(), node.nullid),
                                  rev.message or ' ',
@@ -629,9 +646,7 @@ class SVNMeta(object):
         pctx = self.repo[node]
         files = pctx.manifest().keys()
         extra = self.genextra(rev.revnum, branch)
-        extra['close'] = 1
-        if self.usebranchnames:
-            extra['branch'] = branch or 'default'
+        self.mapbranch(extra, True)
         ctx = context.memctx(self.repo,
                              (node, revlog.nullid),
                              rev.message or util.default_commit_msg,
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -375,6 +375,7 @@ optionmap = {
     'tagpaths': ('hgsubversion', 'tagpaths'),
     'authors': ('hgsubversion', 'authormap'),
     'filemap': ('hgsubversion', 'filemap'),
+    'branchmap': ('hgsubversion', 'branchmap'),
     'stupid': ('hgsubversion', 'stupid'),
     'defaulthost': ('hgsubversion', 'defaulthost'),
     'defaultauthors': ('hgsubversion', 'defaultauthors'),
new file mode 100755
--- /dev/null
+++ b/tests/fixtures/branchmap.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+# inspired by Python r62868
+
+mkdir temp
+cd temp
+svnadmin create repo
+svn co file://`pwd`/repo wc
+export REPO=file://`pwd`/repo
+cd wc
+mkdir branches trunk tags
+svn add *
+svn ci -m 'btt'
+
+echo a > trunk/a
+svn add trunk/a
+svn ci -m 'Add file.'
+svn up
+
+svn cp trunk branches/badname
+svn ci -m 'Branch to be renamed.'
+svn up
+
+svn cp trunk branches/feature
+svn ci -m 'Branch to be unnamed.'
+svn up
+
+cd ../..
+svnadmin dump temp/repo > branchmap.svndump
+echo
+echo 'Complete.'
+echo 'You probably want to clean up temp now.'
+echo 'Dump in branchmap.svndump'
+exit 0
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/branchmap.svndump
@@ -0,0 +1,140 @@
+SVN-fs-dump-format-version: 2
+
+UUID: e45f170d-c19e-4c14-836b-556bb41c9429
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2010-02-23T17:55:37.753400Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 100
+Content-length: 100
+
+K 7
+svn:log
+V 3
+btt
+K 10
+svn:author
+V 3
+djc
+K 8
+svn:date
+V 27
+2010-02-23T17:55:38.066481Z
+PROPS-END
+
+Node-path: branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 106
+Content-length: 106
+
+K 7
+svn:log
+V 9
+Add file.
+K 10
+svn:author
+V 3
+djc
+K 8
+svn:date
+V 27
+2010-02-23T17:55:39.058424Z
+PROPS-END
+
+Node-path: trunk/a
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+Content-length: 12
+
+PROPS-END
+a
+
+
+Revision-number: 3
+Prop-content-length: 119
+Content-length: 119
+
+K 7
+svn:log
+V 21
+Branch to be renamed.
+K 10
+svn:author
+V 3
+djc
+K 8
+svn:date
+V 27
+2010-02-23T17:55:42.051658Z
+PROPS-END
+
+Node-path: branches/badname
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk
+
+
+Revision-number: 4
+Prop-content-length: 119
+Content-length: 119
+
+K 7
+svn:log
+V 21
+Branch to be unnamed.
+K 10
+svn:author
+V 3
+djc
+K 8
+svn:date
+V 27
+2010-02-23T17:55:45.050455Z
+PROPS-END
+
+Node-path: branches/feature
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 3
+Node-copyfrom-path: trunk
+
+
--- a/tests/test_fetch_mappings.py
+++ b/tests/test_fetch_mappings.py
@@ -20,6 +20,10 @@ class MapTests(test_util.TestBase):
     def filemap(self):
         return os.path.join(self.tmpdir, 'filemap')
 
+    @property
+    def branchmap(self):
+        return os.path.join(self.tmpdir, 'branchmap')
+
     def test_author_map(self, stupid=False):
         test_util.load_svndump_fixture(self.repo_path, 'replace_trunk_with_branch.svndump')
         authormap = open(self.authors, 'w')
@@ -101,6 +105,25 @@ class MapTests(test_util.TestBase):
     def test_file_map_exclude_stupid(self):
         self.test_file_map_exclude(True)
 
+    def test_branchmap(self, stupid=False):
+        test_util.load_svndump_fixture(self.repo_path, 'branchmap.svndump')
+        branchmap = open(self.branchmap, 'w')
+        branchmap.write("badname = good-name # stuffy\n")
+        branchmap.write("feature = default\n")
+        branchmap.close()
+        _ui = ui.ui()
+        _ui.setconfig('hgsubversion', 'stupid', str(stupid))
+        _ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
+        commands.clone(_ui, test_util.fileurl(self.repo_path),
+                       self.wc_path, branchmap=self.branchmap)
+        branches = set(self.repo[i].branch() for i in self.repo)
+        self.assert_('badname' not in branches)
+        self.assert_('good-name' in branches)
+        self.assertEquals(self.repo[2].branch(), 'default')
+
+    def test_branchmap_stupid(self):
+        self.test_branchmap(True)
+
 
 def suite():
     return unittest.TestLoader().loadTestsFromTestCase(MapTests)