changeset 1367:5ddb35c31097

Merge with stable. This includes the force-disabling of stupid mode on Subversion 1.9 and later.
author Augie Fackler <raf@durin42.com>
date Thu, 31 Dec 2015 12:52:49 -0500
parents a279eb7185d4 (diff) 388511fe46be (current diff)
children 84789e32b32e
files hgsubversion/stupid.py hgsubversion/svnwrap/svn_swig_wrapper.py
diffstat 11 files changed, 150 insertions(+), 18 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
@@ -34,7 +34,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 +49,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')
@@ -425,12 +425,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
@@ -94,6 +94,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 +286,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 +390,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)
 
--- a/tests/test_fetch_mappings.py
+++ b/tests/test_fetch_mappings.py
@@ -290,6 +290,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')