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
This commit is contained in:
Patrick Mezard 2010-11-25 21:55:21 +01:00
parent a0bae52693
commit fe5402cb96
7 changed files with 145 additions and 17 deletions

View File

@ -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)

View File

@ -97,8 +97,8 @@ def commit(ui, repo, rev_ctx, meta, base_revision, svn):
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)

View File

@ -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

View File

@ -343,13 +343,13 @@ def getcopies(svn, meta, branch, branchpath, r, files, parentctx):
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, tbdelta):
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)

View File

@ -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)

View File

@ -139,6 +139,72 @@ class TestFetchExternals(test_util.TestBase):
['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)

View File

@ -172,7 +172,7 @@ def load_svndump_fixture(path, fixture_name):
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, repo_path, wc_path, stupid=False,
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'''