# HG changeset patch # User Augie Fackler # Date 1451584369 18000 # Node ID 5ddb35c310975392bbd1a0fdce7e03b362c5dfbb # Parent a279eb7185d4815781293ecfd31cb1e0cdbbc836# Parent 388511fe46be95f37cc209812c09d9f661aaeb8e Merge with stable. This includes the force-disabling of stupid mode on Subversion 1.9 and later. diff --git a/hgsubversion/editor.py b/hgsubversion/editor.py --- 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 diff --git a/hgsubversion/layouts/custom.py b/hgsubversion/layouts/custom.py --- 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: diff --git a/hgsubversion/maps.py b/hgsubversion/maps.py --- 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 diff --git a/hgsubversion/replay.py b/hgsubversion/replay.py --- 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 diff --git a/hgsubversion/stupid.py b/hgsubversion/stupid.py --- 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) diff --git a/hgsubversion/svnexternals.py b/hgsubversion/svnexternals.py --- 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() diff --git a/hgsubversion/svnmeta.py b/hgsubversion/svnmeta.py --- 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 diff --git a/hgsubversion/svnwrap/subvertpy_wrapper.py b/hgsubversion/svnwrap/subvertpy_wrapper.py --- 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] diff --git a/hgsubversion/svnwrap/svn_swig_wrapper.py b/hgsubversion/svnwrap/svn_swig_wrapper.py --- 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] diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py --- 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) diff --git a/tests/test_fetch_mappings.py b/tests/test_fetch_mappings.py --- 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')