# HG changeset patch # User Augie Fackler # Date 1457119982 18000 # Node ID e1619c051788692046f83b068fb063e6cef7a133 # Parent abc87a62ff51efcc3f71ba835e08b2fdb3f30b3c# Parent 2ae4fb5bfab90f93ebf074e382798a7d2e67137a Merge with stable. 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 @@ -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 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 @@ -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 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) @@ -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'), diff --git a/tests/fixtures/rename-closed-branch-dir.sh b/tests/fixtures/rename-closed-branch-dir.sh 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 diff --git a/tests/fixtures/rename-closed-branch-dir.svndump b/tests/fixtures/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 + + 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 @@ -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')