changeset 762:c31a1f92e1c6

svnexternals: preliminary support for subrepos based externals At this point, only pulling externals definitions into .hgsub and .hgsubstate is supported. One difference between subrepos and svn:externals is the former separate the source definition and target revision in two files, while svn:externals definitions contain both. To handle this, the svn:externals revision references is replaced with a {REV} placeholder and stored in .hgsub, prefixed with the external base directory separated with a ':', while the revision is extracted in .hgsubstate. For instance, the following external: -r3 ^/externals/proj2@2 deps/proj2 Becomes: (.hgsub) deps/proj2 = [hgsubversion] :-r{REV} ^/externals/proj2@2 deps/proj2 (.hgsubstate) 3 deps/proj2
author Patrick Mezard <pmezard@gmail.com>
date Thu, 25 Nov 2010 21:55:21 +0100
parents 979148947967
children 6463b34bbcb6
files hgsubversion/__init__.py hgsubversion/pushmod.py hgsubversion/replay.py hgsubversion/stupid.py hgsubversion/svnexternals.py tests/test_externals.py tests/test_util.py
diffstat 7 files changed, 145 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/hgsubversion/__init__.py
+++ b/hgsubversion/__init__.py
@@ -47,10 +47,19 @@ try:
 except ImportError:
    revset = None
 
+try:
+    from mercurial import subrepo
+    # require svnsubrepo and hg >= 1.7.1
+    subrepo.svnsubrepo
+    hgutil.checklink
+except ImportError:
+    subrepo = None
+
 import svncommands
 import util
 import svnrepo
 import wrappers
+import svnexternals
 
 svnopts = [
     ('', 'stupid', None,
@@ -155,6 +164,9 @@ def extsetup():
     if revset:
         revset.symbols.update(util.revsets)
 
+    if subrepo:
+        subrepo.types['hgsubversion'] = svnexternals.svnsubrepo
+
 def reposetup(ui, repo):
     if repo.local():
        svnrepo.generate_repo_class(ui, repo)
--- a/hgsubversion/pushmod.py
+++ b/hgsubversion/pushmod.py
@@ -97,8 +97,8 @@ def commit(ui, repo, rev_ctx, meta, base
     elif parent_branch and parent_branch != 'default':
         branch_path = 'branches/%s' % parent_branch
 
-    extchanges = svnexternals.diff(svnexternals.parse(parent),
-                                   svnexternals.parse(rev_ctx))
+    extchanges = svnexternals.diff(svnexternals.parse(ui, parent),
+                                   svnexternals.parse(ui, rev_ctx))
     addeddirs, deleteddirs = _getdirchanges(svn, branch_path, parent, rev_ctx,
                                             rev_ctx.files(), extchanges)
     deleteddirs = set(deleteddirs)
--- a/hgsubversion/replay.py
+++ b/hgsubversion/replay.py
@@ -37,7 +37,7 @@ def updateexternals(ui, meta, current):
         if bp not in branches:
             parent = meta.get_parent_revision(revnum, b)
             pctx = meta.repo[parent]
-            branches[bp] = (svnexternals.parse(pctx), pctx)
+            branches[bp] = (svnexternals.parse(ui, pctx), pctx)
         branches[bp][0][p] = entry
 
     # register externals file changes
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -343,13 +343,13 @@ def getcopies(svn, meta, branch, branchp
             hgcopies.update({k: v})
     return hgcopies
 
-def fetch_externals(svn, branchpath, r, parentctx):
+def fetch_externals(ui, svn, branchpath, r, parentctx):
     """Extract svn:externals for the current revision and branch
 
     Return an externalsfile instance or None if there are no externals
     to convert and never were.
     """
-    externals = svnexternals.parse(parentctx)
+    externals = svnexternals.parse(ui, parentctx)
     # Detect property additions only, changes are handled by checking
     # existing entries individually. Projects are unlikely to store
     # externals on many different root directories, so we trade code
@@ -600,7 +600,7 @@ def convert_rev(ui, meta, svn, r, tbdelt
 
         externals = {}
         if meta.layout != 'single':
-            externals = fetch_externals(svn, branches[b], r, parentctx)
+            externals = fetch_externals(ui, svn, branches[b], r, parentctx)
             externals = svnexternals.getchanges(ui, meta.repo, parentctx,
                                                 externals)
             files_touched.extend(externals)
--- a/hgsubversion/svnexternals.py
+++ b/hgsubversion/svnexternals.py
@@ -3,6 +3,15 @@ import cStringIO
 import os, re, shutil, stat, subprocess
 from mercurial import util as hgutil
 from mercurial.i18n import _
+
+try:
+    from mercurial import subrepo
+    # require svnsubrepo and hg >= 1.7.1
+    subrepo.svnsubrepo
+    hgutil.checklink
+except ImportError:
+    subrepo = None
+
 import util
 
 class externalsfile(dict):
@@ -301,11 +310,32 @@ def getchanges(ui, repo, parentctx, exts
     hgsubversion needs for externals bookkeeping, to their new content
     as raw bytes or None if the file has to be removed.
     """
-    files = {
-        '.hgsvnexternals': None,
-        }
-    if exts:
-        files['.hgsvnexternals'] = exts.write()
+    mode = ui.config('hgsubversion', 'externals', 'svnexternals')
+    if mode == 'svnexternals':
+        files = {
+            '.hgsvnexternals': None,
+            }
+        if exts:
+            files['.hgsvnexternals'] = exts.write()
+    elif mode == 'subrepos':
+        # XXX: clobering the subrepos files is good enough for now
+        files = {
+            '.hgsub': None,
+            '.hgsubstate': None,
+            }
+        if exts:
+            defs = parsedefinitions(ui, repo, '', exts)
+            hgsub, hgsubstate = [], []
+            for path, rev, source, pegrev, norevline, base in sorted(defs):
+                hgsub.append('%s = [hgsubversion] %s:%s\n'
+                             % (path, base, norevline))
+                if rev is None:
+                    rev = 'HEAD'
+                hgsubstate.append('%s %s\n' % (rev, path))
+            files['.hgsub'] = ''.join(hgsub)
+            files['.hgsubstate'] = ''.join(hgsubstate)
+    else:
+        raise hgutil.Abort(_('unknown externals modes: %s') % mode)
 
     # Should the really be updated?
     updates = {}
@@ -318,11 +348,29 @@ def getchanges(ui, repo, parentctx, exts
                 updates[fn] = None
     return updates
 
-def parse(ctx):
+def parse(ui, ctx):
     """Return the externals definitions stored in ctx as a (possibly empty)
     externalsfile().
     """
     external = externalsfile()
-    if '.hgsvnexternals' in ctx:
-        external.read(ctx['.hgsvnexternals'].data())
+    mode = ui.config('hgsubversion', 'externals', 'svnexternals')
+    if mode == 'svnexternals':
+        if '.hgsvnexternals' in ctx:
+            external.read(ctx['.hgsvnexternals'].data())
+    elif mode == 'subrepos':
+        for path in ctx.substate:
+            src, rev = ctx.substate[path][:2]
+            base, norevline = src.split(':', 1)
+            base = base.strip()
+            if rev is None:
+                rev = 'HEAD'
+            line = norevline.replace('{REV}', rev)
+            external.setdefault(base, []).append(line)
+    else:
+        raise hgutil.Abort(_('unknown externals modes: %s') % mode)
     return external
+
+if subrepo:
+    class svnsubrepo(subrepo.svnsubrepo):
+        def __init__(self, ctx, path, state):
+            super(svnsubrepo, self).__init__(ctx, path, state)
--- a/tests/test_externals.py
+++ b/tests/test_externals.py
@@ -139,6 +139,72 @@ class TestFetchExternals(test_util.TestB
                   ['subdir2/deps/project1'], repo, 3)
         checkdeps(['subdir/deps/project1'], ['deps/project2'], repo, 4)
 
+    def test_hgsub(self, stupid=False):
+        repo = self._load_fixture_and_fetch('externals.svndump',
+                                            externals='subrepos',
+                                            stupid=stupid)
+        self.assertEqual("""\
+deps/project1 = [hgsubversion] :^/externals/project1 deps/project1
+""", repo[0]['.hgsub'].data())
+        self.assertEqual("""\
+HEAD deps/project1
+""", repo[0]['.hgsubstate'].data())
+
+        self.assertEqual("""\
+deps/project1 = [hgsubversion] :^/externals/project1 deps/project1
+deps/project2 = [hgsubversion] :-r{REV} ^/externals/project2@2 deps/project2
+""", repo[1]['.hgsub'].data())
+        self.assertEqual("""\
+HEAD deps/project1
+2 deps/project2
+""", repo[1]['.hgsubstate'].data())
+
+        self.assertEqual("""\
+deps/project2 = [hgsubversion] :-r{REV} ^/externals/project2@2 deps/project2
+subdir/deps/project1 = [hgsubversion] subdir:^/externals/project1 deps/project1
+subdir2/deps/project1 = [hgsubversion] subdir2:^/externals/project1 deps/project1
+""", repo[2]['.hgsub'].data())
+        self.assertEqual("""\
+2 deps/project2
+HEAD subdir/deps/project1
+HEAD subdir2/deps/project1
+""", repo[2]['.hgsubstate'].data())
+
+        self.assertEqual("""\
+deps/project2 = [hgsubversion] :-r{REV} ^/externals/project2@2 deps/project2
+subdir/deps/project1 = [hgsubversion] subdir:^/externals/project1 deps/project1
+""", repo[3]['.hgsub'].data())
+        self.assertEqual("""\
+2 deps/project2
+HEAD subdir/deps/project1
+""", repo[3]['.hgsubstate'].data())
+
+        self.assertEqual("""\
+subdir/deps/project1 = [hgsubversion] subdir:^/externals/project1 deps/project1
+""", repo[4]['.hgsub'].data())
+        self.assertEqual("""\
+HEAD subdir/deps/project1
+""", repo[4]['.hgsubstate'].data())
+
+        self.assertEqual("""\
+deps/project2 = [hgsubversion] :-r{REV} ^/externals/project2@2 deps/project2
+subdir2/deps/project1 = [hgsubversion] subdir2:^/externals/project1 deps/project1
+""", repo[5]['.hgsub'].data())
+        self.assertEqual("""\
+2 deps/project2
+HEAD subdir2/deps/project1
+""", repo[5]['.hgsubstate'].data())
+
+        self.assertEqual("""\
+deps/project2 = [hgsubversion] :-r{REV} ^/externals/project2@2 deps/project2
+""", repo[6]['.hgsub'].data())
+        self.assertEqual("""\
+2 deps/project2
+""", repo[6]['.hgsubstate'].data())
+
+    def test_hgsub_stupid(self):
+        self.test_hgsub(True)
+
 class TestPushExternals(test_util.TestBase):
     def setUp(self):
         test_util.TestBase.setUp(self)
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -172,7 +172,7 @@ def load_svndump_fixture(path, fixture_n
 
 def load_fixture_and_fetch(fixture_name, repo_path, wc_path, stupid=False,
                            subdir='', noupdate=True, layout='auto',
-                           startrev=0):
+                           startrev=0, externals=None):
     load_svndump_fixture(repo_path, fixture_name)
     if subdir:
         repo_path += '/' + subdir
@@ -188,6 +188,8 @@ def load_fixture_and_fetch(fixture_name,
         cmd.append('--stupid')
     if noupdate:
         cmd.append('--noupdate')
+    if externals:
+        cmd[:0] = ['--config', 'hgsubversion.externals=%s' % externals]
 
     dispatch.dispatch(cmd)
 
@@ -275,7 +277,7 @@ class TestBase(unittest.TestCase):
         return testui(stupid, layout)
 
     def _load_fixture_and_fetch(self, fixture_name, subdir=None, stupid=False,
-                                layout='auto', startrev=0):
+                                layout='auto', startrev=0, externals=None):
         if layout == 'single':
             if subdir is None:
                 subdir = 'trunk'
@@ -284,7 +286,7 @@ class TestBase(unittest.TestCase):
         return load_fixture_and_fetch(fixture_name, self.repo_path,
                                       self.wc_path, subdir=subdir,
                                       stupid=stupid, layout=layout,
-                                      startrev=startrev)
+                                      startrev=startrev, externals=externals)
 
     def _add_svn_rev(self, changes):
         '''changes is a dict of filename -> contents'''