subrepo.py
337 lines
| 11.3 KiB
| text/x-python
|
PythonLexer
/ mercurial / subrepo.py
|
r8812 | # subrepo.py - sub-repository handling for Mercurial | ||
# | ||||
# Copyright 2006, 2007 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2, incorporated herein by reference. | ||||
|
r10178 | import errno, os, re | ||
|
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:] | ||||
state[path] = (src, 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 | ||||
|
r9186 | parent = _abssource(repo._subparent) | ||
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: | ||
raise util.Abort(_('unknown subrepo type %s') % t) | ||||
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) | ||||
|
r8814 | if os.path.exists(os.path.join(root, '.hg')): | ||
|
r9092 | self._repo = hg.repository(r.ui, root) | ||
|
r8814 | else: | ||
util.makedirs(root) | ||||
|
r9092 | self._repo = hg.repository(r.ui, root, create=True) | ||
|
r10068 | f = file(os.path.join(root, '.hg', 'hgrc'), 'w') | ||
f.write('[paths]\ndefault = %s\n' % state[0]) | ||||
f.close() | ||||
|
r8814 | self._repo._subparent = r | ||
self._repo._subsource = state[0] | ||||
|
r8813 | |||
def dirty(self): | ||||
r = self._state[1] | ||||
if r == '': | ||||
return True | ||||
w = self._repo[None] | ||||
if w.p1() != self._repo[r]: # version checked out changed | ||||
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 | ||||
self._repo.ui.status(_('pulling subrepo %s\n') % self._path) | ||||
srcurl = _abssource(self._repo) | ||||
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]] | ||||
if dst.ancestor(cur) == cur: | ||||
|
r9782 | self._repo.ui.debug("updating subrepo %s\n" % self._path) | ||
|
r9781 | hg.update(self._repo, state[1]) | ||
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)) | ||||
write, read, err = util.popen3(cmd) | ||||
retdata = read.read() | ||||
err = err.read().strip() | ||||
if err: | ||||
raise util.Abort(err) | ||||
return retdata | ||||
def _wcrev(self): | ||||
info = self._svncommand(['info']) | ||||
mat = re.search('Revision: ([\d]+)\n', info) | ||||
if not mat: | ||||
return 0 | ||||
return mat.groups()[0] | ||||
def _url(self): | ||||
info = self._svncommand(['info']) | ||||
mat = re.search('URL: ([^\n]+)\n', info) | ||||
if not mat: | ||||
return 0 | ||||
return mat.groups()[0] | ||||
def _wcclean(self): | ||||
status = self._svncommand(['status']) | ||||
status = '\n'.join([s for s in status.splitlines() if s[0] != '?']) | ||||
if status.strip(): | ||||
return False | ||||
return True | ||||
def dirty(self): | ||||
if self._wcrev() == self._state[1] and self._wcclean(): | ||||
return False | ||||
return True | ||||
def commit(self, text, user, date): | ||||
# user and date are out of our hands since svn is centralized | ||||
if self._wcclean(): | ||||
return self._wcrev() | ||||
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(): | ||||
|
r10183 | self._repo.ui.warn(_('not removing repo %s because' | ||
'it has changes.\n' % self._path)) | ||||
|
r10178 | return | ||
self._repo.ui.note('removing subrepo %s\n' % self._path) | ||||
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 | } | ||