changeset 1378:e1619c051788

Merge with stable.
author Augie Fackler <raf@durin42.com>
date Fri, 04 Mar 2016 14:33:02 -0500
parents abc87a62ff51 (diff) 2ae4fb5bfab9 (current diff)
children 367e65989b41
files hgsubversion/wrappers.py
diffstat 13 files changed, 545 insertions(+), 25 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/editor.py
+++ b/hgsubversion/editor.py
@@ -578,6 +578,12 @@ class HgEditor(svnwrap.Editor):
             try:
                 if not self.meta.is_path_valid(path):
                     return
+
+                # are we skipping this branch entirely?
+                br_path, branch = self.meta.split_branch_path(path)[:2]
+                if self.meta.skipbranch(branch):
+                    return
+
                 try:
                     handler(window)
                 except AssertionError, e: # pragma: no cover
--- a/hgsubversion/layouts/custom.py
+++ b/hgsubversion/layouts/custom.py
@@ -18,7 +18,9 @@ class CustomLayout(base.BaseLayout):
         self.svn_to_hg = {}
         self.hg_to_svn = {}
 
-        for hg_branch, svn_path in meta.ui.configitems('hgsubversionbranch'):
+        meta._gen_cachedconfig('custombranches', {}, configname='hgsubversionbranch')
+
+        for hg_branch, svn_path in meta.custombranches.iteritems():
 
             hg_branch = hg_branch.strip()
             if hg_branch == 'default' or not hg_branch:
--- a/hgsubversion/maps.py
+++ b/hgsubversion/maps.py
@@ -5,6 +5,7 @@ import os
 from mercurial import util as hgutil
 from mercurial.node import bin, hex, nullid
 
+import subprocess
 import svncommands
 import util
 
@@ -34,7 +35,7 @@ class AuthorMap(dict):
 
         self.super = super(AuthorMap, self)
         self.super.__init__()
-        self.load(self.meta.authors_file)
+        self.load(self.meta.authormap_file)
 
         # append authors specified from the commandline
         clmap = util.configpath(self.meta.ui, 'authormap')
@@ -49,8 +50,8 @@ class AuthorMap(dict):
             return
 
         writing = False
-        if path != self.meta.authors_file:
-            writing = open(self.meta.authors_file, 'a')
+        if path != self.meta.authormap_file:
+            writing = open(self.meta.authormap_file, 'a')
 
         self.meta.ui.debug('reading authormap from %s\n' % path)
         f = open(path, 'r')
@@ -98,15 +99,26 @@ class AuthorMap(dict):
         if self.meta.caseignoreauthors:
             search_author = author.lower()
 
+        result = None
         if search_author in self:
             result = self.super.__getitem__(search_author)
-        elif self.meta.defaultauthors:
-            self[author] = result = '%s%s' % (author, self.defaulthost)
-            msg = 'substituting author "%s" for default "%s"\n'
-            self.meta.ui.debug(msg % (author, result))
-        else:
-            msg = 'author %s has no entry in the author map!'
-            raise hgutil.Abort(msg % author)
+        elif self.meta.mapauthorscmd:
+            cmd = self.meta.mapauthorscmd % author
+            process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+            output, err = process.communicate()
+            retcode = process.poll()
+            if retcode:
+                msg = 'map author command "%s" exited with error'
+                raise hgutil.Abort(msg % cmd)
+            self[author] = result = output.strip()
+        if not result:
+            if self.meta.defaultauthors:
+                self[author] = result = '%s%s' % (author, self.defaulthost)
+                msg = 'substituting author "%s" for default "%s"\n'
+                self.meta.ui.debug(msg % (author, result))
+            else:
+                msg = 'author %s has no entry in the author map!'
+                raise hgutil.Abort(msg % author)
         self.meta.ui.debug('mapping author "%s" to "%s"\n' % (author, result))
         return result
 
@@ -425,12 +437,7 @@ class BranchMap(dict):
             dst = dst.strip()
             self.meta.ui.debug('adding branch %s to branch map\n' % src)
 
-            if not dst:
-                # prevent people from assuming such lines are valid
-                raise hgutil.Abort('removing branches is not supported, yet\n'
-                                   '(line %i in branch map %s)'
-                                   % (number, path))
-            elif src in self and dst != self[src]:
+            if dst and src in self and dst != self[src]:
                 msg = 'overriding branch: "%s" to "%s" (%s)\n'
                 self.meta.ui.status(msg % (self[src], dst, src))
             self[src] = dst
--- a/hgsubversion/replay.py
+++ b/hgsubversion/replay.py
@@ -121,6 +121,12 @@ def _convert_rev(ui, meta, svn, r, tbdel
         if branch in current.emptybranches and files:
             del current.emptybranches[branch]
 
+        if meta.skipbranch(branch):
+            # make sure we also get rid of it from emptybranches
+            if branch in current.emptybranches:
+                del current.emptybranches[branch]
+            continue
+
         files = dict(files)
         parents = meta.get_parent_revision(rev.revnum, branch), revlog.nullid
         if parents[0] in closedrevs and branch in meta.closebranches:
@@ -195,6 +201,9 @@ def _convert_rev(ui, meta, svn, r, tbdel
     # 2. handle branches that need to be committed without any files
     for branch in current.emptybranches:
 
+        if meta.skipbranch(branch):
+            continue
+
         ha = meta.get_parent_revision(rev.revnum, branch)
         if ha == node.nullid:
             continue
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -689,6 +689,10 @@ def convert_rev(ui, meta, svn, r, tbdelt
     date = meta.fixdate(r.date)
     check_deleted_branches = set(tbdelta['branches'][1])
     for b in branches:
+
+        if meta.skipbranch(b):
+            continue
+
         parentctx = meta.repo[meta.get_parent_revision(r.revnum, b)]
         tag = meta.get_path_tag(meta.remotename(b))
         kind = svn.checkpath(branches[b], r.revnum)
--- a/hgsubversion/svnexternals.py
+++ b/hgsubversion/svnexternals.py
@@ -120,13 +120,84 @@ def parsedefinition(line):
 class RelativeSourceError(Exception):
     pass
 
+def resolvedots(url):
+    """
+    Fix references that include .. entries.
+    Scans a URL for .. type entries and resolves them but will not allow any
+    number of ..s to take us out of domain so http://.. will raise an exception.
+    
+    Tests, (Don't know how to construct a round trip for this so doctest):
+    >>> # Relative URL within servers svn area
+    >>> resolvedots(
+    ...    "http://some.svn.server/svn/some_repo/../other_repo")
+    'http://some.svn.server/svn/other_repo'
+    >>> # Complex One
+    >>> resolvedots(
+    ...    "http://some.svn.server/svn/repo/../other/repo/../../other_repo")
+    'http://some.svn.server/svn/other_repo'
+    >>> # Another Complex One
+    >>> resolvedots(
+    ...    "http://some.svn.server/svn/repo/dir/subdir/../../../other_repo/dir")
+    'http://some.svn.server/svn/other_repo/dir'
+    >>> # Last Complex One - SVN Allows this & seen it used even if it is BAD!
+    >>> resolvedots(
+    ...    "http://svn.server/svn/my_repo/dir/subdir/../../other_dir")
+    'http://svn.server/svn/my_repo/other_dir'
+    >>> # Outside the SVN Area might be OK
+    >>> resolvedots(
+    ...    "http://svn.server/svn/some_repo/../../other_svn_repo")
+    'http://svn.server/other_svn_repo'
+    >>> # Complex One
+    >>> resolvedots(
+    ...    "http://some.svn.server/svn/repo/../other/repo/../../other_repo")
+    'http://some.svn.server/svn/other_repo'
+    >>> # On another server is not a relative URL should give an exception
+    >>> resolvedots(
+    ...    "http://some.svn.server/svn/some_repo/../../../other_server")
+    Traceback (most recent call last):
+        ...
+    RelativeSourceError: Relative URL cannot be to another server
+    """
+    orig = url.split('/')
+    fixed = []
+    for item in orig:
+        if item != '..':
+            fixed.append(item)
+        elif len(fixed) > 3:  # Don't allow things to go out of domain
+            fixed.pop()
+        else:
+            raise RelativeSourceError(
+                'Relative URL cannot be to another server')
+    return '/'.join(fixed)
+
+
+
 def resolvesource(ui, svnroot, source):
+    """ Resolve the source as either matching the scheme re or by resolving
+    relative URLs which start with ^ and my include relative .. references.
+    
+    >>> root = 'http://some.svn.server/svn/some_repo'
+    >>> resolvesource(None, root, 'http://other.svn.server')
+    'http://other.svn.server'
+    >>> resolvesource(None, root, 'ssh://other.svn.server')
+    'ssh://other.svn.server'
+    >>> resolvesource(None, root, '^/other_repo')
+    'http://some.svn.server/svn/some_repo/other_repo'
+    >>> resolvesource(None, root, '^/sub_repo')
+    'http://some.svn.server/svn/some_repo/sub_repo'
+    >>> resolvesource(None, root, '^/../other_repo')
+    'http://some.svn.server/svn/other_repo'
+    >>> resolvesource(None, root, '^/../../../server/other_repo')
+    Traceback (most recent call last):
+        ...
+    RelativeSourceError: Relative URL cannot be to another server
+    """
     if re_scheme.search(source):
         return source
     if source.startswith('^/'):
         if svnroot is None:
             raise RelativeSourceError()
-        return svnroot + source[1:]
+        return resolvedots(svnroot + source[1:])
     ui.warn(_('ignoring unsupported non-fully qualified external: %r\n'
               % source))
     return None
@@ -440,3 +511,7 @@ class svnsubrepo(subrepo.svnsubrepo):
         if self._state[1] == 'HEAD':
             return 'HEAD'
         return super(svnsubrepo, self).basestate()
+    
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
--- a/hgsubversion/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -55,6 +55,7 @@ class SVNMeta(object):
         self._gen_cachedconfig('lastpulled', 0, configname=False)
         self._gen_cachedconfig('defaultauthors', True)
         self._gen_cachedconfig('caseignoreauthors', False)
+        self._gen_cachedconfig('mapauthorscmd', None)
         self._gen_cachedconfig('defaulthost', self.uuid)
         self._gen_cachedconfig('usebranchnames', True)
         self._gen_cachedconfig('defaultmessage', '')
@@ -94,6 +95,8 @@ class SVNMeta(object):
                     c = self.ui.configint('hgsubversion', configname, default)
                 elif isinstance(default, list):
                     c = self.ui.configlist('hgsubversion', configname, default)
+                elif isinstance(default, dict):
+                    c = dict(self.ui.configitems(configname))
                 else:
                     c = self.ui.config('hgsubversion', configname, default)
 
@@ -284,7 +287,7 @@ class SVNMeta(object):
         return os.path.join(self.metapath, 'branch_info')
 
     @property
-    def authors_file(self):
+    def authormap_file(self):
         return os.path.join(self.metapath, 'authors')
 
     @property
@@ -388,6 +391,19 @@ class SVNMeta(object):
         }
         return extra
 
+    def skipbranch(self, name):
+        '''Returns whether or not we're skipping a branch.'''
+        # sometimes it's easier to pass the path instead of just the branch
+        # name, so we test for that here
+        if name:
+            bname = self.split_branch_path(name)
+            if bname != (None, None, None):
+                name = bname[1]
+
+        # if the mapped branch == '' and the original branch name == '' then we
+        # won't commit this branch
+        return name and not self.branchmap.get(name, True)
+
     def mapbranch(self, extra, close=False):
         if close:
             extra['close'] = 1
--- a/hgsubversion/svnwrap/subvertpy_wrapper.py
+++ b/hgsubversion/svnwrap/subvertpy_wrapper.py
@@ -186,7 +186,8 @@ class SubversionRepo(object):
     Note that password stores do not work, the parameter is only here
     to ensure that the API is the same as for the SWIG wrapper.
     """
-    def __init__(self, url='', username='', password='', head=None, password_stores=None):
+    def __init__(self, url='', username='', password='', head=None,
+                 password_stores=None):
         parsed = common.parse_url(url, username, password)
         # --username and --password override URL credentials
         self.username = parsed[0]
--- a/hgsubversion/svnwrap/svn_swig_wrapper.py
+++ b/hgsubversion/svnwrap/svn_swig_wrapper.py
@@ -205,7 +205,8 @@ class SubversionRepo(object):
 
     It uses the SWIG Python bindings, see above for requirements.
     """
-    def __init__(self, url='', username='', password='', head=None, password_stores=None):
+    def __init__(self, url='', username='', password='', head=None,
+                 password_stores=None):
         parsed = common.parse_url(url, username, password)
         # --username and --password override URL credentials
         self.username = parsed[0]
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -204,6 +204,7 @@ def push(repo, dest, force, revs):
         hasobsolete = False
 
     temporary_commits = []
+    obsmarkers = []
     try:
         # TODO: implement --rev/#rev support
         # TODO: do credentials specified in the URL still work?
@@ -300,9 +301,7 @@ def push(repo, dest, force, revs):
                     if meta.get_source_rev(ctx=c)[0] == pushedrev.revnum:
                         # This is corresponds to the changeset we just pushed
                         if hasobsolete:
-                            ui.note('marking %s as obsoleted by %s\n' %
-                                    (original_ctx.hex(), c.hex()))
-                            obsolete.createmarkers(repo, [(original_ctx, [c])])
+                            obsmarkers.append([(original_ctx, [c])])
 
                     tip_ctx = c
 
@@ -343,7 +342,14 @@ def push(repo, dest, force, revs):
         finally:
             util.swap_out_encoding()
 
-        if not hasobsolete:
+        if hasobsolete:
+            for marker in obsmarkers:
+                obsolete.createmarkers(repo, marker)
+                beforepush = marker[0][0]
+                afterpush = marker[0][1][0]
+                ui.note('marking %s as obsoleted by %s\n' %
+                        (beforepush.hex(), afterpush.hex()))
+        else:
             # strip the original changesets since the push was
             # successful and changeset obsolescence is unavailable
             util.strip(ui, repo, outgoing, "all")
@@ -397,6 +403,7 @@ def pull(repo, source, heads=[], force=F
         svn = source.svn
         if meta is None:
             meta = repo.svnmeta(svn.uuid, svn.subdir)
+        svn.meta = meta
 
         stopat_rev = util.parse_revnum(svn, checkout)
 
@@ -590,6 +597,7 @@ def rebase(orig, ui, repo, **opts):
 optionmap = {
     'tagpaths': ('hgsubversion', 'tagpaths'),
     'authors': ('hgsubversion', 'authormap'),
+    'mapauthorscmd': ('hgsubversion', 'mapauthorscmd'),
     'branchdir': ('hgsubversion', 'branchdir'),
     'trunkdir': ('hgsubversion', 'trunkdir'),
     'infix': ('hgsubversion', 'infix'),
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/rename-closed-branch-dir.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+#
+# Generate rename-closed-branch-dir.svndump
+#
+
+mkdir temp
+cd temp
+
+mkdir project
+cd project
+mkdir trunk
+mkdir branches
+mkdir tags
+cd ..
+
+svnadmin create testrepo
+CURRENT_DIR=`pwd`
+svnurl=file://"$CURRENT_DIR"/testrepo
+#svn import project-orig $svnurl -m "init project"
+
+svn co $svnurl project
+cd project
+svn add *
+svn ci -m "init project"
+
+cd trunk
+echo a > a.txt
+svn add a.txt
+svn ci -m "add a.txt in trunk"
+
+# Create a branch
+svn up
+cd ../branches
+svn copy ../trunk async-db
+svn ci -m "add branch async-db"
+svn up
+
+# Implement feature
+cd async-db
+echo b > b.txt
+svn add b.txt
+svn ci -m "Async functionality"
+
+# Merge feature branch
+cd ../../trunk
+svn merge $svnurl/branches/async-db
+svn ci -m "Merged branch async-db"
+cd ..
+svn up
+
+# Create branch folder for unnecessary branches
+svn mkdir $svnurl/branches/dead -m "Create branch folder for unnecessary branches"
+svn up
+
+#  We don't need the 'async-db' branch, anymore.
+svn copy $svnurl/branches/async-db $svnurl/branches/dead -m "We don't need the 'async-db' branch, anymore."
+svn up
+
+# Rename 'dead' folder to 'closed'
+svn move $svnurl/branches/dead $svnurl/branches/closed -m "Renamed 'dead' folder to 'closed'"
+svn up
+
+# Move 'branches/closed' to 'tags/closed'
+svn move $svnurl/branches/closed $svnurl/tags/closed -m "Moved 'branches/closed' to 'tags/closed'."
+svn up
+
+# Dump repository
+cd ..
+svnadmin dump testrepo > ../rename-closed-branch-dir.svndump
new file mode 100644
--- /dev/null
+++ b/tests/fixtures/rename-closed-branch-dir.svndump
@@ -0,0 +1,296 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 2efdcfe9-9dfd-40a7-a9cc-bf5b70806ff3
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2016-01-27T15:35:29.673334Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 112
+Content-length: 112
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:30.079847Z
+K 7
+svn:log
+V 12
+init project
+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: 118
+Content-length: 118
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:31.065912Z
+K 7
+svn:log
+V 18
+add a.txt in trunk
+PROPS-END
+
+Node-path: trunk/a.txt
+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 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:34.051261Z
+K 7
+svn:log
+V 19
+add branch async-db
+PROPS-END
+
+Node-path: branches/async-db
+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 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:36.101507Z
+K 7
+svn:log
+V 19
+Async functionality
+PROPS-END
+
+Node-path: branches/async-db/b.txt
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 3b5d5c3712955042212316173ccf37be
+Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b
+Content-length: 12
+
+PROPS-END
+b
+
+
+Revision-number: 5
+Prop-content-length: 122
+Content-length: 122
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:38.055736Z
+K 7
+svn:log
+V 22
+Merged branch async-db
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: change
+Prop-content-length: 57
+Content-length: 57
+
+K 13
+svn:mergeinfo
+V 22
+/branches/async-db:3-4
+PROPS-END
+
+
+Node-path: trunk/b.txt
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 4
+Node-copyfrom-path: branches/async-db/b.txt
+Text-copy-source-md5: 3b5d5c3712955042212316173ccf37be
+Text-copy-source-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b
+
+
+Revision-number: 6
+Prop-content-length: 145
+Content-length: 145
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:40.046670Z
+K 7
+svn:log
+V 45
+Create branch folder for unnecessary branches
+PROPS-END
+
+Node-path: branches/dead
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 7
+Prop-content-length: 145
+Content-length: 145
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:41.048576Z
+K 7
+svn:log
+V 45
+We don't need the 'async-db' branch, anymore.
+PROPS-END
+
+Node-path: branches/dead/async-db
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 6
+Node-copyfrom-path: branches/async-db
+
+
+Revision-number: 8
+Prop-content-length: 133
+Content-length: 133
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:42.046536Z
+K 7
+svn:log
+V 33
+Renamed 'dead' folder to 'closed'
+PROPS-END
+
+Node-path: branches/closed
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 7
+Node-copyfrom-path: branches/dead
+
+
+Node-path: branches/dead
+Node-action: delete
+
+
+Revision-number: 9
+Prop-content-length: 141
+Content-length: 141
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:43.048056Z
+K 7
+svn:log
+V 41
+Moved 'branches/closed' to 'tags/closed'.
+PROPS-END
+
+Node-path: branches/closed
+Node-action: delete
+
+
+Node-path: tags/closed
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 8
+Node-copyfrom-path: branches/closed
+
+
--- a/tests/test_fetch_mappings.py
+++ b/tests/test_fetch_mappings.py
@@ -114,6 +114,15 @@ class MapTests(test_util.TestBase):
         self.assertEqual(self.repo['tip'].user(),
                         'evil@5b65bade-98f3-4993-a01f-b7a6710da339')
 
+    def test_author_map_mapauthorscmd(self):
+        repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
+        ui = self.ui()
+        ui.setconfig('hgsubversion', 'mapauthorscmd', 'echo "svn: %s"')
+        commands.clone(ui, test_util.fileurl(repo_path),
+                       self.wc_path)
+        self.assertEqual(self.repo[0].user(), 'svn: Augie')
+        self.assertEqual(self.repo['tip'].user(), 'svn: evil')
+
     def _loadwithfilemap(self, svndump, filemapcontent,
             failonmissing=True):
         repo_path = self.load_svndump(svndump)
@@ -290,6 +299,23 @@ class MapTests(test_util.TestBase):
         for r in repo:
             self.assertEquals(verify.verify(ui, repo, rev=r), 0)
 
+    def test_branchmap_no_replacement(self):
+        '''test that empty mappings are accepted
+
+        Empty mappings are lines like 'this ='. We check that such branches are
+        not converted.
+        '''
+        repo_path = self.load_svndump('branchmap.svndump')
+        branchmap = open(self.branchmap, 'w')
+        branchmap.write("badname =\n")
+        branchmap.close()
+        ui = self.ui()
+        ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
+        commands.clone(ui, test_util.fileurl(repo_path),
+                       self.wc_path, branchmap=self.branchmap)
+        branches = set(self.repo[i].branch() for i in self.repo)
+        self.assertEquals(sorted(branches), ['default', 'feature'])
+
     def test_tagmap(self):
         repo_path = self.load_svndump('basic_tag_tests.svndump')
         tagmap = open(self.tagmap, 'w')