# HG changeset patch # User David Schleimer # Date 1378922101 25200 # Node ID 9a7e3dbd0f6ee021ae3e580a3d536484282620d5 # Parent 791382a21cc4c55114fafe251cbfada95fd7a59e layouts: add support for an infix between tbt and the hg root diff --git a/hgsubversion/__init__.py b/hgsubversion/__init__.py --- a/hgsubversion/__init__.py +++ b/hgsubversion/__init__.py @@ -86,6 +86,8 @@ wrapcmds = { # cmd: generic, target, fix 'list of paths to search for tags in Subversion repositories'), ('', 'branchdir', '', 'path to search for branches in subversion repositories'), + ('', 'infix', '', + 'path relative to trunk, branch an tag dirs to import'), ('A', 'authors', '', 'file mapping Subversion usernames to Mercurial authors'), ('', 'filemap', '', diff --git a/hgsubversion/help/subversion.rst b/hgsubversion/help/subversion.rst --- a/hgsubversion/help/subversion.rst +++ b/hgsubversion/help/subversion.rst @@ -34,7 +34,10 @@ with repositories that use the conventio directories. By default, hgsubversion will use this layout whenever it finds any of these directories at the specified directory on the server. Standard layout also supports alternate names for the ``branches`` directory and multiple tags -locations. +locations. Finally, Standard Layout supports selecting a subdirectory relative +to ``trunk``, and each branch and tag dir. This is useful if you have a single +``trunk``, ``branches``, and ``tags`` with several projects inside, and you wish +to import only a single project. If you instead want to clone just a single directory or branch, clone the specific directory path. In the example above, to get *only* trunk, you would @@ -308,6 +311,13 @@ settings: default is ``branches``. This option has no effect for single-directory clones. + ``hgsubversion.infix`` + + Specifies a path to strip between relative to the trunk/branch/tag + root as the mercurial root. This can be used to import a single + sub-project when you have several sub-projects under a single + trunk/branches/tags layout in subversion. + ``hgsubversion.filemap`` Path to a file for filtering files during the conversion. Files may either diff --git a/hgsubversion/layouts/base.py b/hgsubversion/layouts/base.py --- a/hgsubversion/layouts/base.py +++ b/hgsubversion/layouts/base.py @@ -82,5 +82,11 @@ class BaseLayout(object): local_path should be relative to the root of the Mercurial working dir + Note that it is permissible to return a longer branch_path + than is passed in iff the path that is passed in is a parent + directory of exactly one branch. This is intended to handle + the case where we are importing a particular subdirectory of + asubversion branch structure. + """ self.__unimplemented('split_remote_name') diff --git a/hgsubversion/layouts/standard.py b/hgsubversion/layouts/standard.py --- a/hgsubversion/layouts/standard.py +++ b/hgsubversion/layouts/standard.py @@ -18,29 +18,41 @@ class StandardLayout(base.BaseLayout): if self._branch_dir[-1] != '/': self._branch_dir += '/' + self._infix = ui.config('hgsubversion', 'infix', '').strip('/') + if self._infix: + self._infix = '/' + self._infix + + self._trunk = 'trunk%s' % self._infix + def localname(self, path): - if path == 'trunk': + if path == self._trunk: return None - elif path.startswith(self._branch_dir): - return path[len(self._branch_dir):] + elif path.startswith(self._branch_dir) and path.endswith(self._infix): + path = path[len(self._branch_dir):] + if self._infix: + path = path[:-len(self._infix)] + return path return '../%s' % path def remotename(self, branch): if branch == 'default' or branch is None: - return 'trunk' + path = self._trunk elif branch.startswith('../'): - return branch[3:] - return '%s%s' % (self._branch_dir, branch) + path = branch[3:] + else: + path = ''.join((self._branch_dir, branch, self._infix)) + + return path def remotepath(self, branch, subdir='/'): if subdir == '/': subdir = '' - branchpath = 'trunk' + branchpath = self._trunk if branch and branch != 'default': if branch.startswith('../'): branchpath = branch[3:] else: - branchpath = '%s%s' % (self._branch_dir, branch) + branchpath = ''.join((self._branch_dir, branch, self._infix)) return '%s/%s' % (subdir or '', branchpath) @@ -94,16 +106,22 @@ class StandardLayout(base.BaseLayout): return candidate, '/'.join(components) if path == 'trunk' or path.startswith('trunk/'): - return 'trunk', path[len('trunk/'):] + return self._trunk, path[len(self._trunk) + 1:] if path.startswith(self._branch_dir): path = path[len(self._branch_dir):] components = path.split('/', 1) - branch_path = '%s%s' % (self._branch_dir, components[0]) + branch_path = ''.join((self._branch_dir, components[0])) if len(components) == 1: local_path = '' else: local_path = components[1] + + if local_path == '': + branch_path += self._infix + elif local_path.startswith(self._infix[1:] + '/'): + branch_path += self._infix + local_path = local_path[len(self._infix):] return branch_path, local_path components = path.split('/') diff --git a/hgsubversion/svnmeta.py b/hgsubversion/svnmeta.py --- a/hgsubversion/svnmeta.py +++ b/hgsubversion/svnmeta.py @@ -312,7 +312,17 @@ class SVNMeta(object): src_file, src_branch = self.split_branch_path(src_path)[:2] src_tag = self.get_path_tag(src_path) if src_tag or src_file == '': - ln = self.localname(p) + brpath, fpath = self.layoutobj.split_remote_name(p, + self.branches) + # we'll sometimes get a different path out of + # split_remate_name than the one we passed in, but + # only for the root of a branch, since the svn copies + # of those will sometimes be of parent directories of + # our root + if fpath == '': + ln = self.localname(brpath) + else: + ln = self.localname(p) if src_tag in self.tags: changeid = self.tags[src_tag] src_rev, src_branch = self.get_source_rev(changeid)[:2] diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py --- a/hgsubversion/wrappers.py +++ b/hgsubversion/wrappers.py @@ -534,6 +534,7 @@ optionmap = { 'tagpaths': ('hgsubversion', 'tagpaths'), 'authors': ('hgsubversion', 'authormap'), 'branchdir': ('hgsubversion', 'branchdir'), + 'infix': ('hgsubversion', 'infix'), 'filemap': ('hgsubversion', 'filemap'), 'branchmap': ('hgsubversion', 'branchmap'), 'tagmap': ('hgsubversion', 'tagmap'), diff --git a/tests/fixtures/subprojects.sh b/tests/fixtures/subprojects.sh new file mode 100755 --- /dev/null +++ b/tests/fixtures/subprojects.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -e + +mkdir temp +cd temp + +svnadmin create testrepo +svn checkout file://`pwd`/testrepo client + +cd client +mkdir trunk +mkdir -p branches +mkdir -p tags + +svn add trunk branches tags +svn commit -m "Initial commit" + +mkdir trunk/project trunk/other +echo "project trunk" > trunk/project/file +echo "other trunk" > trunk/other/phile +svn add trunk/project trunk/other +svn commit -m "Added file and phile in trunk" + +svn up + +svn cp trunk tags/tag_from_trunk +svn ci -m 'created tag from trunk' + +svn up + +svn cp trunk branches/branch +svn ci -m 'created branch from trunk' + +svn up + +echo "project branch" > branches/branch/project/file +svn ci -m "committed to the project branch" + +svn up + +echo "trunk2" > trunk/project/file +svn ci -m "committed to trunk again" + +svn up + +echo "other branch" > branches/branch/other/phile +svn ci -m "committed to the other branch" + +svn up + +svn cp branches/branch tags/tag_from_branch +svn ci -m "create tag from branch" + +cd .. +svnadmin dump testrepo > ../subprojects.svndump + +echo "Created subprojects.svndump" +echo "You might want to clean up ${PWD} now" diff --git a/tests/fixtures/subprojects.svndump b/tests/fixtures/subprojects.svndump new file mode 100644 --- /dev/null +++ b/tests/fixtures/subprojects.svndump @@ -0,0 +1,283 @@ +SVN-fs-dump-format-version: 2 + +UUID: 03c99a5f-42f9-43e0-bb0d-03549a88a7e4 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2013-07-23T22:47:56.963334Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 120 +Content-length: 120 + +K 10 +svn:author +V 10 +dschleimer +K 8 +svn:date +V 27 +2013-07-23T22:47:57.401454Z +K 7 +svn:log +V 14 +Initial commit +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: 135 +Content-length: 135 + +K 10 +svn:author +V 10 +dschleimer +K 8 +svn:date +V 27 +2013-07-23T22:47:57.849874Z +K 7 +svn:log +V 29 +Added file and phile in trunk +PROPS-END + +Node-path: trunk/other +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk/other/phile +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 12 +Text-content-md5: fe5279547ba9d8c257b67c1938853896 +Text-content-sha1: 6c94bf284aa7bc931c358ae3dfcfb4fc9f335579 +Content-length: 22 + +PROPS-END +other trunk + + +Node-path: trunk/project +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk/project/file +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 14 +Text-content-md5: d61b3a5935cb974e41082d9eb8eb912e +Text-content-sha1: 1e7f7740062dc540ab20fb6cf395cad3c55f396f +Content-length: 24 + +PROPS-END +project trunk + + +Revision-number: 3 +Prop-content-length: 128 +Content-length: 128 + +K 10 +svn:author +V 10 +dschleimer +K 8 +svn:date +V 27 +2013-07-23T22:47:58.281764Z +K 7 +svn:log +V 22 +created tag from trunk +PROPS-END + +Node-path: tags/tag_from_trunk +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: trunk + + +Revision-number: 4 +Prop-content-length: 131 +Content-length: 131 + +K 10 +svn:author +V 10 +dschleimer +K 8 +svn:date +V 27 +2013-07-23T22:47:59.456625Z +K 7 +svn:log +V 25 +created branch from trunk +PROPS-END + +Node-path: branches/branch +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 3 +Node-copyfrom-path: trunk + + +Revision-number: 5 +Prop-content-length: 137 +Content-length: 137 + +K 10 +svn:author +V 10 +dschleimer +K 8 +svn:date +V 27 +2013-07-23T22:47:59.862054Z +K 7 +svn:log +V 31 +committed to the project branch +PROPS-END + +Node-path: branches/branch/project/file +Node-kind: file +Node-action: change +Text-content-length: 15 +Text-content-md5: 64cdb38c10361681c4c2918a222a3102 +Text-content-sha1: 545ef3bb672a1dd01fb9bd2a2eb7621882a4c701 +Content-length: 15 + +project branch + + +Revision-number: 6 +Prop-content-length: 130 +Content-length: 130 + +K 10 +svn:author +V 10 +dschleimer +K 8 +svn:date +V 27 +2013-07-23T22:48:00.345069Z +K 7 +svn:log +V 24 +committed to trunk again +PROPS-END + +Node-path: trunk/project/file +Node-kind: file +Node-action: change +Text-content-length: 7 +Text-content-md5: 28d0a7e7ef2864416b7a9398623e4d09 +Text-content-sha1: 91454e2d3487f712490f17481157e389c11a6fe0 +Content-length: 7 + +trunk2 + + +Revision-number: 7 +Prop-content-length: 135 +Content-length: 135 + +K 10 +svn:author +V 10 +dschleimer +K 8 +svn:date +V 27 +2013-07-23T22:48:00.751804Z +K 7 +svn:log +V 29 +committed to the other branch +PROPS-END + +Node-path: branches/branch/other/phile +Node-kind: file +Node-action: change +Text-content-length: 13 +Text-content-md5: 7c133b867f55c0ba8688e1f111ddebaf +Text-content-sha1: aee59a1c349cedc1ab035263bd7f14d58c6ab33b +Content-length: 13 + +other branch + + +Revision-number: 8 +Prop-content-length: 128 +Content-length: 128 + +K 10 +svn:author +V 10 +dschleimer +K 8 +svn:date +V 27 +2013-07-23T22:48:01.199203Z +K 7 +svn:log +V 22 +create tag from branch +PROPS-END + +Node-path: tags/tag_from_branch +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 7 +Node-copyfrom-path: branches/branch + + diff --git a/tests/test_fetch_branches.py b/tests/test_fetch_branches.py --- a/tests/test_fetch_branches.py +++ b/tests/test_fetch_branches.py @@ -166,6 +166,30 @@ class TestFetchBranches(test_util.TestBa expected_tags = set(['tip', 'tag_from_trunk', 'tag_from_branch']) self.assertEqual(tags, expected_tags) + def test_subproject_fetch(self): + config = { + 'hgsubversion.infix': 'project', + } + repo = self._load_fixture_and_fetch('subprojects.svndump', + layout='standard', + config=config) + + heads = set([repo[n].branch() for n in repo.heads()]) + expected_heads = set(['default', 'branch']) + self.assertEqual(heads, expected_heads) + + tags = set(repo.tags()) + expected_tags = set(['tip', 'tag_from_trunk', 'tag_from_branch']) + self.assertEqual(tags, expected_tags) + + for head in repo.heads(): + ctx = repo[head] + self.assertFalse('project/file' in ctx, 'failed to strip infix') + self.assertTrue('file' in ctx, 'failed to track a simple file') + self.assertFalse('other/phile' in ctx, 'pulled in other project') + self.assertFalse('phile' in ctx, 'merged other project in repo') + + def suite(): all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchBranches), ]