# HG changeset patch # User Patrick Mezard # Date 1290718521 -3600 # Node ID c31a1f92e1c654dc66c254c50d6c1e85bd253800 # Parent 979148947967a94b0d7859fdff040c0b1fbc520b 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 diff --git a/hgsubversion/__init__.py b/hgsubversion/__init__.py --- 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) diff --git a/hgsubversion/pushmod.py b/hgsubversion/pushmod.py --- 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) diff --git a/hgsubversion/replay.py b/hgsubversion/replay.py --- 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 diff --git a/hgsubversion/stupid.py b/hgsubversion/stupid.py --- 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) diff --git a/hgsubversion/svnexternals.py b/hgsubversion/svnexternals.py --- 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) diff --git a/tests/test_externals.py b/tests/test_externals.py --- 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) diff --git a/tests/test_util.py b/tests/test_util.py --- 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'''