subrepo.py
482 lines
| 16.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 | |||
|
r11109 | import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath | ||
|
r8814 | from i18n import _ | ||
|
r12167 | import config, util, node, error, cmdutil | ||
|
r9092 | hg = None | ||
|
r8813 | |||
|
r10177 | nullstate = ('', '', 'empty') | ||
|
r8812 | |||
|
r11775 | def state(ctx, ui): | ||
|
r11571 | """return a state dict, mapping subrepo paths configured in .hgsub | ||
to tuple: (source from .hgsub, revision from .hgsubstate, kind | ||||
(key in types dict)) | ||||
""" | ||||
|
r8812 | 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 | |||
|
r11775 | for path, src in ui.configitems('subpaths'): | ||
p.set('subpaths', path, src, ui.configsource('subpaths', path)) | ||||
|
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:] | ||||
|
r11775 | |||
for pattern, repl in p.items('subpaths'): | ||||
|
r11961 | # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub | ||
# does a string decode. | ||||
repl = repl.encode('string-escape') | ||||
# However, we still want to allow back references to go | ||||
# through unharmed, so we turn r'\\1' into r'\1'. Again, | ||||
# extra escapes are needed because re.sub string decodes. | ||||
repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl) | ||||
|
r11775 | try: | ||
src = re.sub(pattern, repl, src, 1) | ||||
except re.error, e: | ||||
raise util.Abort(_("bad subrepository pattern in %s: %s") | ||||
% (p.source('subpaths', pattern), e)) | ||||
|
r10457 | state[path] = (src.strip(), rev.get(path, ''), kind) | ||
|
r8812 | |||
return state | ||||
|
r8813 | |||
def writestate(repo, state): | ||||
|
r11571 | """rewrite .hgsubstate in (outer) repo with these subrepo states""" | ||
|
r8813 | repo.wwrite('.hgsubstate', | ||
''.join(['%s %s\n' % (state[s][1], s) | ||||
for s in sorted(state)]), '') | ||||
|
r8814 | def submerge(repo, wctx, mctx, actx): | ||
|
r11571 | """delegated from merge.applyupdates: merging of .hgsubstate file | ||
in working context, merging context and 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(): | ||
|
r11470 | a = sa.get(s, nullstate) | ||
|
r11463 | ld = l # local state with possible dirty flag for compares | ||
|
r11470 | if wctx.sub(s).dirty(): | ||
|
r11463 | ld = (l[0], l[1] + "+") | ||
|
r11470 | if wctx == actx: # overwrite | ||
a = ld | ||||
|
r11463 | |||
|
r8814 | if s in s2: | ||
r = s2[s] | ||||
|
r11463 | if ld == r or r == a: # no change or local is newer | ||
|
r8814 | sm[s] = l | ||
continue | ||||
|
r11463 | elif ld == a: # other side changed | ||
|
r9782 | debug(s, "other changed, get", r) | ||
|
r8814 | wctx.sub(s).get(r) | ||
sm[s] = r | ||||
|
r11463 | elif ld[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 | ||||
|
r11463 | elif ld[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 | ||||
|
r11463 | elif ld == 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) | ||||
|
r11112 | def relpath(sub): | ||
|
r11571 | """return path to this subrepo as seen from outermost repo""" | ||
|
r11112 | if not hasattr(sub, '_repo'): | ||
return sub._path | ||||
parent = sub._repo | ||||
while hasattr(parent, '_subparent'): | ||||
parent = parent._subparent | ||||
return sub._repo.root[len(parent.root)+1:] | ||||
|
r8815 | def _abssource(repo, push=False): | ||
|
r11571 | """return pull/push path of repo - either based on parent repo | ||
.hgsub info or on the subrepos own config""" | ||||
|
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] | ||||
|
r11109 | r = urlparse.urlparse(parent + '/' + source) | ||
r = urlparse.urlunparse((r[0], r[1], | ||||
|
r11117 | posixpath.normpath(r[2]), | ||
|
r11109 | r[3], r[4], r[5])) | ||
return r | ||||
return posixpath.normpath(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): | ||
|
r11571 | """return instance of the right subrepo class for subrepo in path""" | ||
|
r8813 | # 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 | |||
|
r11559 | # subrepo classes need to implement the following abstract class: | ||
class abstractsubrepo(object): | ||||
def dirty(self): | ||||
"""returns true if the dirstate of the subrepo does not match | ||||
current stored state | ||||
""" | ||||
raise NotImplementedError | ||||
|
r12162 | def checknested(path): | ||
"""check if path is a subrepository within this repository""" | ||||
return False | ||||
|
r11559 | def 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. | ||||
""" | ||||
raise NotImplementedError | ||||
def remove(self): | ||||
"""remove the subrepo | ||||
|
r8813 | |||
|
r11559 | (should verify the dirstate is not dirty first) | ||
""" | ||||
raise NotImplementedError | ||||
def get(self, state): | ||||
"""run whatever commands are needed to put the subrepo into | ||||
this state | ||||
""" | ||||
raise NotImplementedError | ||||
def merge(self, state): | ||||
"""merge currently-saved state with the new state.""" | ||||
raise NotImplementedError | ||||
def push(self, force): | ||||
|
r11572 | """perform whatever action is analogous to 'hg push' | ||
|
r11559 | |||
This may be a no-op on some systems. | ||||
""" | ||||
raise NotImplementedError | ||||
|
r12166 | def status(self, rev2, **opts): | ||
return [], [], [], [], [], [], [] | ||||
|
r12167 | def diff(self, diffopts, node2, match, prefix, **opts): | ||
pass | ||||
|
r11559 | class hgsubrepo(abstractsubrepo): | ||
|
r8813 | 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) | ||||
|
r10697 | defpath = _abssource(self._repo) | ||
defpushpath = _abssource(self._repo, True) | ||||
|
r10666 | addpathconfig('default', defpath) | ||
|
r10697 | if defpath != defpushpath: | ||
addpathconfig('default-push', defpushpath) | ||||
|
r10666 | fp.close() | ||
|
r12166 | def status(self, rev2, **opts): | ||
try: | ||||
rev1 = self._state[1] | ||||
ctx1 = self._repo[rev1] | ||||
ctx2 = self._repo[rev2] | ||||
return self._repo.status(ctx1, ctx2, **opts) | ||||
except error.RepoLookupError, inst: | ||||
self._repo.ui.warn(_("warning: %s in %s\n") | ||||
% (inst, relpath(self))) | ||||
return [], [], [], [], [], [], [] | ||||
|
r12167 | def diff(self, diffopts, node2, match, prefix, **opts): | ||
try: | ||||
node1 = node.bin(self._state[1]) | ||||
cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts, | ||||
node1, node2, match, | ||||
prefix=os.path.join(prefix, self._path), | ||||
listsubrepos=True, **opts) | ||||
except error.RepoLookupError, inst: | ||||
self._repo.ui.warn(_("warning: %s in %s\n") | ||||
% (inst, relpath(self))) | ||||
|
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 | ||||
|
r12162 | def checknested(self, path): | ||
return self._repo._checknested(self._repo.wjoin(path)) | ||||
|
r8813 | def commit(self, text, user, date): | ||
|
r11112 | self._repo.ui.debug("committing subrepo %s\n" % relpath(self)) | ||
|
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 | ||||
|
r11112 | self._repo.ui.note(_('removing subrepo %s\n') % relpath(self)) | ||
|
r8814 | 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') | ||
|
r11112 | % (relpath(self), 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: | ||||
|
r11112 | self._repo.ui.debug("updating subrepo %s\n" % relpath(self)) | ||
|
r9781 | hg.update(self._repo, state[1]) | ||
|
r10251 | elif anc == dst: | ||
|
r11112 | self._repo.ui.debug("skipping subrepo %s\n" % relpath(self)) | ||
|
r9781 | else: | ||
|
r11112 | self._repo.ui.debug("merging subrepo %s\n" % relpath(self)) | ||
|
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): | ||||
|
r11067 | if not c.sub(s).push(force): | ||
return False | ||||
|
r8815 | |||
dsturl = _abssource(self._repo, True) | ||||
|
r11111 | self._repo.ui.status(_('pushing subrepo %s to %s\n') % | ||
|
r11112 | (relpath(self), dsturl)) | ||
|
r8815 | other = hg.repository(self._repo.ui, dsturl) | ||
|
r11067 | return self._repo.push(other, force) | ||
|
r10177 | |||
|
r11559 | class svnsubrepo(abstractsubrepo): | ||
|
r10178 | def __init__(self, ctx, path, state): | ||
self._path = path | ||||
self._state = state | ||||
self._ctx = ctx | ||||
self._ui = ctx._repo.ui | ||||
|
r11560 | def _svncommand(self, commands, filename=''): | ||
path = os.path.join(self._ctx._repo.origroot, self._path, filename) | ||||
|
r10954 | cmd = ['svn'] + commands + [path] | ||
|
r10178 | 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) | ||||
|
r12060 | newrev = re.search('Committed revision ([0-9]+).', commitinfo) | ||
|
r10178 | 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]]) | ||||
|
r12060 | if not re.search('Checked out revision [0-9]+.', status): | ||
|
r10178 | 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): | ||||
|
r11455 | # push is a no-op for SVN | ||
return True | ||||
|
r10178 | |||
|
r10177 | types = { | ||
'hg': hgsubrepo, | ||||
|
r10178 | 'svn': svnsubrepo, | ||
|
r10177 | } | ||