changeset 1094:9a7e3dbd0f6e

layouts: add support for an infix between tbt and the hg root
author David Schleimer <dschleimer@fb.com>
date Wed, 11 Sep 2013 10:55:01 -0700
parents 791382a21cc4
children 19ddc6d7cd6f
files hgsubversion/__init__.py hgsubversion/help/subversion.rst hgsubversion/layouts/base.py hgsubversion/layouts/standard.py hgsubversion/svnmeta.py hgsubversion/wrappers.py tests/fixtures/subprojects.sh tests/fixtures/subprojects.svndump tests/test_fetch_branches.py
diffstat 9 files changed, 425 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- 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', '',
--- 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
--- 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')
--- 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('/')
--- 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]
--- 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'),
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"
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
+
+
--- 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),
           ]