subrepo.py
370 lines
| 12.8 KiB
| text/x-python
|
PythonLexer
/ mercurial / subrepo.py
|
r8812 | # subrepo.py - sub-repository handling for Mercurial | ||
# | ||||
|
r10324 | # Copyright 2009-2010 Matt Mackall <mpm@selenic.com> | ||
|
r8812 | # | ||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r8812 | |||
|
r10299 | import errno, os, re, xml.dom.minidom, shutil | ||
|
r8814 | from i18n import _ | ||
|
r8813 | import config, util, node, error | ||
|
r9092 | hg = None | ||
|
r8813 | |||
|
r10177 | nullstate = ('', '', 'empty') | ||
|
r8812 | |||
def state(ctx): | ||||
p = config.config() | ||||
def read(f, sections=None, remap=None): | ||||
if f in ctx: | ||||
|
r10174 | p.parse(f, ctx[f].data(), sections, remap, read) | ||
else: | ||||
raise util.Abort(_("subrepo spec file %s not found") % f) | ||||
if '.hgsub' in ctx: | ||||
read('.hgsub') | ||||
|
r8812 | |||
rev = {} | ||||
if '.hgsubstate' in ctx: | ||||
try: | ||||
for l in ctx['.hgsubstate'].data().splitlines(): | ||||
|
r9752 | revision, path = l.split(" ", 1) | ||
|
r8812 | rev[path] = revision | ||
except IOError, err: | ||||
if err.errno != errno.ENOENT: | ||||
raise | ||||
state = {} | ||||
for path, src in p[''].items(): | ||||
|
r10177 | kind = 'hg' | ||
if src.startswith('['): | ||||
if ']' not in src: | ||||
raise util.Abort(_('missing ] in subrepo source')) | ||||
kind, src = src.split(']', 1) | ||||
kind = kind[1:] | ||||
|
r10457 | state[path] = (src.strip(), rev.get(path, ''), kind) | ||
|
r8812 | |||
return state | ||||
|
r8813 | |||
def writestate(repo, state): | ||||
repo.wwrite('.hgsubstate', | ||||
''.join(['%s %s\n' % (state[s][1], s) | ||||
for s in sorted(state)]), '') | ||||
|
r8814 | def submerge(repo, wctx, mctx, actx): | ||
|
r10175 | # working context, merging context, ancestor context | ||
|
r8814 | if mctx == actx: # backwards? | ||
actx = wctx.p1() | ||||
s1 = wctx.substate | ||||
s2 = mctx.substate | ||||
sa = actx.substate | ||||
sm = {} | ||||
|
r9782 | repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx)) | ||
|
r9779 | def debug(s, msg, r=""): | ||
if r: | ||||
|
r10177 | r = "%s:%s:%s" % r | ||
|
r9782 | repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r)) | ||
|
r9779 | |||
|
r8814 | for s, l in s1.items(): | ||
|
r9783 | if wctx != actx and wctx.sub(s).dirty(): | ||
|
r9780 | l = (l[0], l[1] + "+") | ||
|
r8814 | a = sa.get(s, nullstate) | ||
if s in s2: | ||||
r = s2[s] | ||||
if l == r or r == a: # no change or local is newer | ||||
sm[s] = l | ||||
continue | ||||
elif l == a: # other side changed | ||||
|
r9782 | debug(s, "other changed, get", r) | ||
|
r8814 | wctx.sub(s).get(r) | ||
sm[s] = r | ||||
elif l[0] != r[0]: # sources differ | ||||
|
r9048 | if repo.ui.promptchoice( | ||
|
r8814 | _(' subrepository sources for %s differ\n' | ||
|
r8908 | 'use (l)ocal source (%s) or (r)emote source (%s)?') | ||
|
r8814 | % (s, l[0], r[0]), | ||
|
r9048 | (_('&Local'), _('&Remote')), 0): | ||
|
r9782 | debug(s, "prompt changed, get", r) | ||
|
r8814 | wctx.sub(s).get(r) | ||
sm[s] = r | ||||
elif l[1] == a[1]: # local side is unchanged | ||||
|
r9782 | debug(s, "other side changed, get", r) | ||
|
r8814 | wctx.sub(s).get(r) | ||
sm[s] = r | ||||
else: | ||||
|
r9782 | debug(s, "both sides changed, merge with", r) | ||
|
r8814 | wctx.sub(s).merge(r) | ||
sm[s] = l | ||||
elif l == a: # remote removed, local unchanged | ||||
|
r9782 | debug(s, "remote removed, remove") | ||
|
r8814 | wctx.sub(s).remove() | ||
else: | ||||
|
r9048 | if repo.ui.promptchoice( | ||
|
r8814 | _(' local changed subrepository %s which remote removed\n' | ||
|
r8908 | 'use (c)hanged version or (d)elete?') % s, | ||
|
r9049 | (_('&Changed'), _('&Delete')), 0): | ||
|
r9782 | debug(s, "prompt remove") | ||
|
r8814 | wctx.sub(s).remove() | ||
for s, r in s2.items(): | ||||
if s in s1: | ||||
continue | ||||
elif s not in sa: | ||||
|
r9782 | debug(s, "remote added, get", r) | ||
|
r10175 | mctx.sub(s).get(r) | ||
|
r8814 | sm[s] = r | ||
elif r != sa[s]: | ||||
|
r9048 | if repo.ui.promptchoice( | ||
|
r8814 | _(' remote changed subrepository %s which local removed\n' | ||
|
r8908 | 'use (c)hanged version or (d)elete?') % s, | ||
|
r9049 | (_('&Changed'), _('&Delete')), 0) == 0: | ||
|
r9782 | debug(s, "prompt recreate", r) | ||
|
r8814 | wctx.sub(s).get(r) | ||
sm[s] = r | ||||
# record merged .hgsubstate | ||||
writestate(repo, sm) | ||||
|
r8815 | def _abssource(repo, push=False): | ||
|
r8814 | if hasattr(repo, '_subparent'): | ||
source = repo._subsource | ||||
if source.startswith('/') or '://' in source: | ||||
return source | ||||
|
r10665 | parent = _abssource(repo._subparent, push) | ||
|
r9186 | if '://' in parent: | ||
if parent[-1] == '/': | ||||
parent = parent[:-1] | ||||
return parent + '/' + source | ||||
return os.path.join(parent, repo._subsource) | ||||
|
r8815 | if push and repo.ui.config('paths', 'default-push'): | ||
return repo.ui.config('paths', 'default-push', repo.root) | ||||
|
r8814 | return repo.ui.config('paths', 'default', repo.root) | ||
|
r8813 | def subrepo(ctx, path): | ||
# subrepo inherently violates our import layering rules | ||||
# because it wants to make repo objects from deep inside the stack | ||||
# so we manually delay the circular imports to not break | ||||
# scripts that don't use our demand-loading | ||||
|
r9092 | global hg | ||
import hg as h | ||||
|
r8814 | hg = h | ||
|
r8813 | |||
|
r8997 | util.path_auditor(ctx._repo.root)(path) | ||
|
r8813 | state = ctx.substate.get(path, nullstate) | ||
|
r10177 | if state[2] not in types: | ||
|
r10299 | raise util.Abort(_('unknown subrepo type %s') % state[2]) | ||
|
r10177 | return types[state[2]](ctx, path, state[:2]) | ||
|
r8813 | |||
|
r10027 | # subrepo classes need to implement the following methods: | ||
# __init__(self, ctx, path, state) | ||||
# dirty(self): returns true if the dirstate of the subrepo | ||||
# does not match current stored state | ||||
# commit(self, text, user, date): commit the current changes | ||||
# to the subrepo with the given log message. Use given | ||||
# user and date if possible. Return the new state of the subrepo. | ||||
# remove(self): remove the subrepo (should verify the dirstate | ||||
# is not dirty first) | ||||
# get(self, state): run whatever commands are needed to put the | ||||
# subrepo into this state | ||||
# merge(self, state): merge currently-saved state with the new state. | ||||
# push(self, force): perform whatever action is analagous to 'hg push' | ||||
# This may be a no-op on some systems. | ||||
|
r8813 | |||
class hgsubrepo(object): | ||||
def __init__(self, ctx, path, state): | ||||
self._path = path | ||||
self._state = state | ||||
r = ctx._repo | ||||
root = r.wjoin(path) | ||||
|
r10666 | create = False | ||
if not os.path.exists(os.path.join(root, '.hg')): | ||||
create = True | ||||
|
r8814 | util.makedirs(root) | ||
|
r10666 | self._repo = hg.repository(r.ui, root, create=create) | ||
|
r8814 | self._repo._subparent = r | ||
self._repo._subsource = state[0] | ||||
|
r8813 | |||
|
r10666 | if create: | ||
fp = self._repo.opener("hgrc", "w", text=True) | ||||
fp.write('[paths]\n') | ||||
def addpathconfig(key, value): | ||||
fp.write('%s = %s\n' % (key, value)) | ||||
self._repo.ui.setconfig('paths', key, value) | ||||
defpath = os.path.join(_abssource(ctx._repo), path) | ||||
addpathconfig('default', defpath) | ||||
fp.close() | ||||
|
r8813 | def dirty(self): | ||
r = self._state[1] | ||||
if r == '': | ||||
return True | ||||
w = self._repo[None] | ||||
|
r10666 | if w.p1() != self._repo[r]: # version checked out change | ||
|
r8813 | return True | ||
return w.dirty() # working directory changed | ||||
def commit(self, text, user, date): | ||||
|
r9782 | self._repo.ui.debug("committing subrepo %s\n" % self._path) | ||
|
r8813 | n = self._repo.commit(text, user, date) | ||
if not n: | ||||
return self._repo['.'].hex() # different version checked out | ||||
return node.hex(n) | ||||
|
r8814 | |||
def remove(self): | ||||
# we can't fully delete the repository as it may contain | ||||
# local-only history | ||||
self._repo.ui.note(_('removing subrepo %s\n') % self._path) | ||||
hg.clean(self._repo, node.nullid, False) | ||||
|
r9507 | def _get(self, state): | ||
|
r10177 | source, revision, kind = state | ||
|
r8814 | try: | ||
self._repo.lookup(revision) | ||||
except error.RepoError: | ||||
self._repo._subsource = source | ||||
srcurl = _abssource(self._repo) | ||||
|
r10671 | self._repo.ui.status(_('pulling subrepo %s from %s\n') | ||
% (self._path, srcurl)) | ||||
|
r8814 | other = hg.repository(self._repo.ui, srcurl) | ||
self._repo.pull(other) | ||||
|
r9507 | def get(self, state): | ||
self._get(state) | ||||
|
r10177 | source, revision, kind = state | ||
|
r9782 | self._repo.ui.debug("getting subrepo %s\n" % self._path) | ||
|
r8814 | hg.clean(self._repo, revision, False) | ||
def merge(self, state): | ||||
|
r9507 | self._get(state) | ||
|
r9781 | cur = self._repo['.'] | ||
dst = self._repo[state[1]] | ||||
|
r10251 | anc = dst.ancestor(cur) | ||
if anc == cur: | ||||
|
r9782 | self._repo.ui.debug("updating subrepo %s\n" % self._path) | ||
|
r9781 | hg.update(self._repo, state[1]) | ||
|
r10251 | elif anc == dst: | ||
self._repo.ui.debug("skipping subrepo %s\n" % self._path) | ||||
|
r9781 | else: | ||
|
r9782 | self._repo.ui.debug("merging subrepo %s\n" % self._path) | ||
|
r9781 | hg.merge(self._repo, state[1], remind=False) | ||
|
r8815 | |||
def push(self, force): | ||||
# push subrepos depth-first for coherent ordering | ||||
c = self._repo[''] | ||||
subs = c.substate # only repos that are committed | ||||
for s in sorted(subs): | ||||
c.sub(s).push(force) | ||||
self._repo.ui.status(_('pushing subrepo %s\n') % self._path) | ||||
dsturl = _abssource(self._repo, True) | ||||
other = hg.repository(self._repo.ui, dsturl) | ||||
self._repo.push(other, force) | ||||
|
r10177 | |||
|
r10178 | class svnsubrepo(object): | ||
def __init__(self, ctx, path, state): | ||||
self._path = path | ||||
self._state = state | ||||
self._ctx = ctx | ||||
self._ui = ctx._repo.ui | ||||
def _svncommand(self, commands): | ||||
cmd = ['svn'] + commands + [self._path] | ||||
cmd = [util.shellquote(arg) for arg in cmd] | ||||
cmd = util.quotecommand(' '.join(cmd)) | ||||
|
r10199 | env = dict(os.environ) | ||
|
r10271 | # Avoid localized output, preserve current locale for everything else. | ||
env['LC_MESSAGES'] = 'C' | ||||
|
r10199 | write, read, err = util.popen3(cmd, env=env, newlines=True) | ||
|
r10178 | retdata = read.read() | ||
err = err.read().strip() | ||||
if err: | ||||
raise util.Abort(err) | ||||
return retdata | ||||
def _wcrev(self): | ||||
|
r10272 | output = self._svncommand(['info', '--xml']) | ||
doc = xml.dom.minidom.parseString(output) | ||||
entries = doc.getElementsByTagName('entry') | ||||
if not entries: | ||||
|
r10178 | return 0 | ||
|
r10272 | return int(entries[0].getAttribute('revision') or 0) | ||
|
r10178 | |||
|
r10273 | def _wcchanged(self): | ||
"""Return (changes, extchanges) where changes is True | ||||
if the working directory was changed, and extchanges is | ||||
True if any of these changes concern an external entry. | ||||
""" | ||||
|
r10272 | output = self._svncommand(['status', '--xml']) | ||
|
r10273 | externals, changes = [], [] | ||
|
r10272 | doc = xml.dom.minidom.parseString(output) | ||
|
r10273 | for e in doc.getElementsByTagName('entry'): | ||
s = e.getElementsByTagName('wc-status') | ||||
if not s: | ||||
continue | ||||
item = s[0].getAttribute('item') | ||||
props = s[0].getAttribute('props') | ||||
path = e.getAttribute('path') | ||||
if item == 'external': | ||||
externals.append(path) | ||||
if (item not in ('', 'normal', 'unversioned', 'external') | ||||
or props not in ('', 'none')): | ||||
changes.append(path) | ||||
for path in changes: | ||||
for ext in externals: | ||||
if path == ext or path.startswith(ext + os.sep): | ||||
return True, True | ||||
return bool(changes), False | ||||
|
r10178 | |||
def dirty(self): | ||||
|
r10273 | if self._wcrev() == self._state[1] and not self._wcchanged()[0]: | ||
|
r10178 | return False | ||
return True | ||||
def commit(self, text, user, date): | ||||
# user and date are out of our hands since svn is centralized | ||||
|
r10273 | changed, extchanged = self._wcchanged() | ||
if not changed: | ||||
|
r10178 | return self._wcrev() | ||
|
r10273 | if extchanged: | ||
# Do not try to commit externals | ||||
raise util.Abort(_('cannot commit svn externals')) | ||||
|
r10178 | commitinfo = self._svncommand(['commit', '-m', text]) | ||
self._ui.status(commitinfo) | ||||
newrev = re.search('Committed revision ([\d]+).', commitinfo) | ||||
if not newrev: | ||||
raise util.Abort(commitinfo.splitlines()[-1]) | ||||
newrev = newrev.groups()[0] | ||||
self._ui.status(self._svncommand(['update', '-r', newrev])) | ||||
return newrev | ||||
def remove(self): | ||||
if self.dirty(): | ||||
|
r10299 | self._ui.warn(_('not removing repo %s because ' | ||
'it has changes.\n' % self._path)) | ||||
|
r10178 | return | ||
|
r10510 | self._ui.note(_('removing subrepo %s\n') % self._path) | ||
|
r10178 | shutil.rmtree(self._ctx.repo.join(self._path)) | ||
def get(self, state): | ||||
status = self._svncommand(['checkout', state[0], '--revision', state[1]]) | ||||
if not re.search('Checked out revision [\d]+.', status): | ||||
raise util.Abort(status.splitlines()[-1]) | ||||
self._ui.status(status) | ||||
def merge(self, state): | ||||
old = int(self._state[1]) | ||||
new = int(state[1]) | ||||
if new > old: | ||||
self.get(state) | ||||
def push(self, force): | ||||
# nothing for svn | ||||
pass | ||||
|
r10177 | types = { | ||
'hg': hgsubrepo, | ||||
|
r10178 | 'svn': svnsubrepo, | ||
|
r10177 | } | ||