/mercurial/subrepo.py
Python | 1580 lines | 1434 code | 63 blank | 83 comment | 123 complexity | fd3c75b75b6257d20700ce92bfb8fed8 MD5 | raw file
Possible License(s): GPL-2.0
Large files files are truncated, but you can click here to view the full file
- # subrepo.py - sub-repository handling for Mercurial
- #
- # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
- #
- # This software may be used and distributed according to the terms of the
- # GNU General Public License version 2 or any later version.
- import errno, os, re, shutil, posixpath, sys
- import xml.dom.minidom
- import stat, subprocess, tarfile
- from i18n import _
- import config, util, node, error, cmdutil, bookmarks, match as matchmod
- import phases
- import pathutil
- hg = None
- propertycache = util.propertycache
- nullstate = ('', '', 'empty')
- def _expandedabspath(path):
- '''
- get a path or url and if it is a path expand it and return an absolute path
- '''
- expandedpath = util.urllocalpath(util.expandpath(path))
- u = util.url(expandedpath)
- if not u.scheme:
- path = util.normpath(os.path.abspath(u.path))
- return path
- def _getstorehashcachename(remotepath):
- '''get a unique filename for the store hash cache of a remote repository'''
- return util.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
- def _calcfilehash(filename):
- data = ''
- if os.path.exists(filename):
- fd = open(filename, 'rb')
- data = fd.read()
- fd.close()
- return util.sha1(data).hexdigest()
- class SubrepoAbort(error.Abort):
- """Exception class used to avoid handling a subrepo error more than once"""
- def __init__(self, *args, **kw):
- error.Abort.__init__(self, *args, **kw)
- self.subrepo = kw.get('subrepo')
- self.cause = kw.get('cause')
- def annotatesubrepoerror(func):
- def decoratedmethod(self, *args, **kargs):
- try:
- res = func(self, *args, **kargs)
- except SubrepoAbort, ex:
- # This exception has already been handled
- raise ex
- except error.Abort, ex:
- subrepo = subrelpath(self)
- errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
- # avoid handling this exception by raising a SubrepoAbort exception
- raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
- cause=sys.exc_info())
- return res
- return decoratedmethod
- def state(ctx, ui):
- """return a state dict, mapping subrepo paths configured in .hgsub
- to tuple: (source from .hgsub, revision from .hgsubstate, kind
- (key in types dict))
- """
- p = config.config()
- def read(f, sections=None, remap=None):
- if f in ctx:
- try:
- data = ctx[f].data()
- except IOError, err:
- if err.errno != errno.ENOENT:
- raise
- # handle missing subrepo spec files as removed
- ui.warn(_("warning: subrepo spec file %s not found\n") % f)
- return
- p.parse(f, data, sections, remap, read)
- else:
- raise util.Abort(_("subrepo spec file %s not found") % f)
- if '.hgsub' in ctx:
- read('.hgsub')
- for path, src in ui.configitems('subpaths'):
- p.set('subpaths', path, src, ui.configsource('subpaths', path))
- rev = {}
- if '.hgsubstate' in ctx:
- try:
- for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
- l = l.lstrip()
- if not l:
- continue
- try:
- revision, path = l.split(" ", 1)
- except ValueError:
- raise util.Abort(_("invalid subrepository revision "
- "specifier in .hgsubstate line %d")
- % (i + 1))
- rev[path] = revision
- except IOError, err:
- if err.errno != errno.ENOENT:
- raise
- def remap(src):
- for pattern, repl in p.items('subpaths'):
- # 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)
- 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))
- return src
- state = {}
- for path, src in p[''].items():
- kind = 'hg'
- if src.startswith('['):
- if ']' not in src:
- raise util.Abort(_('missing ] in subrepo source'))
- kind, src = src.split(']', 1)
- kind = kind[1:]
- src = src.lstrip() # strip any extra whitespace after ']'
- if not util.url(src).isabs():
- parent = _abssource(ctx._repo, abort=False)
- if parent:
- parent = util.url(parent)
- parent.path = posixpath.join(parent.path or '', src)
- parent.path = posixpath.normpath(parent.path)
- joined = str(parent)
- # Remap the full joined path and use it if it changes,
- # else remap the original source.
- remapped = remap(joined)
- if remapped == joined:
- src = remap(src)
- else:
- src = remapped
- src = remap(src)
- state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
- return state
- def writestate(repo, state):
- """rewrite .hgsubstate in (outer) repo with these subrepo states"""
- lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
- repo.wwrite('.hgsubstate', ''.join(lines), '')
- def submerge(repo, wctx, mctx, actx, overwrite):
- """delegated from merge.applyupdates: merging of .hgsubstate file
- in working context, merging context and ancestor context"""
- if mctx == actx: # backwards?
- actx = wctx.p1()
- s1 = wctx.substate
- s2 = mctx.substate
- sa = actx.substate
- sm = {}
- repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
- def debug(s, msg, r=""):
- if r:
- r = "%s:%s:%s" % r
- repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
- for s, l in sorted(s1.iteritems()):
- a = sa.get(s, nullstate)
- ld = l # local state with possible dirty flag for compares
- if wctx.sub(s).dirty():
- ld = (l[0], l[1] + "+")
- if wctx == actx: # overwrite
- a = ld
- if s in s2:
- r = s2[s]
- if ld == r or r == a: # no change or local is newer
- sm[s] = l
- continue
- elif ld == a: # other side changed
- debug(s, "other changed, get", r)
- wctx.sub(s).get(r, overwrite)
- sm[s] = r
- elif ld[0] != r[0]: # sources differ
- if repo.ui.promptchoice(
- _(' subrepository sources for %s differ\n'
- 'use (l)ocal source (%s) or (r)emote source (%s)?'
- '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
- debug(s, "prompt changed, get", r)
- wctx.sub(s).get(r, overwrite)
- sm[s] = r
- elif ld[1] == a[1]: # local side is unchanged
- debug(s, "other side changed, get", r)
- wctx.sub(s).get(r, overwrite)
- sm[s] = r
- else:
- debug(s, "both sides changed")
- srepo = wctx.sub(s)
- option = repo.ui.promptchoice(
- _(' subrepository %s diverged (local revision: %s, '
- 'remote revision: %s)\n'
- '(M)erge, keep (l)ocal or keep (r)emote?'
- '$$ &Merge $$ &Local $$ &Remote')
- % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
- if option == 0:
- wctx.sub(s).merge(r)
- sm[s] = l
- debug(s, "merge with", r)
- elif option == 1:
- sm[s] = l
- debug(s, "keep local subrepo revision", l)
- else:
- wctx.sub(s).get(r, overwrite)
- sm[s] = r
- debug(s, "get remote subrepo revision", r)
- elif ld == a: # remote removed, local unchanged
- debug(s, "remote removed, remove")
- wctx.sub(s).remove()
- elif a == nullstate: # not present in remote or ancestor
- debug(s, "local added, keep")
- sm[s] = l
- continue
- else:
- if repo.ui.promptchoice(
- _(' local changed subrepository %s which remote removed\n'
- 'use (c)hanged version or (d)elete?'
- '$$ &Changed $$ &Delete') % s, 0):
- debug(s, "prompt remove")
- wctx.sub(s).remove()
- for s, r in sorted(s2.items()):
- if s in s1:
- continue
- elif s not in sa:
- debug(s, "remote added, get", r)
- mctx.sub(s).get(r)
- sm[s] = r
- elif r != sa[s]:
- if repo.ui.promptchoice(
- _(' remote changed subrepository %s which local removed\n'
- 'use (c)hanged version or (d)elete?'
- '$$ &Changed $$ &Delete') % s, 0) == 0:
- debug(s, "prompt recreate", r)
- wctx.sub(s).get(r)
- sm[s] = r
- # record merged .hgsubstate
- writestate(repo, sm)
- return sm
- def _updateprompt(ui, sub, dirty, local, remote):
- if dirty:
- msg = (_(' subrepository sources for %s differ\n'
- 'use (l)ocal source (%s) or (r)emote source (%s)?\n'
- '$$ &Local $$ &Remote')
- % (subrelpath(sub), local, remote))
- else:
- msg = (_(' subrepository sources for %s differ (in checked out '
- 'version)\n'
- 'use (l)ocal source (%s) or (r)emote source (%s)?\n'
- '$$ &Local $$ &Remote')
- % (subrelpath(sub), local, remote))
- return ui.promptchoice(msg, 0)
- def reporelpath(repo):
- """return path to this (sub)repo as seen from outermost repo"""
- parent = repo
- while util.safehasattr(parent, '_subparent'):
- parent = parent._subparent
- return repo.root[len(pathutil.normasprefix(parent.root)):]
- def subrelpath(sub):
- """return path to this subrepo as seen from outermost repo"""
- if util.safehasattr(sub, '_relpath'):
- return sub._relpath
- if not util.safehasattr(sub, '_repo'):
- return sub._path
- return reporelpath(sub._repo)
- def _abssource(repo, push=False, abort=True):
- """return pull/push path of repo - either based on parent repo .hgsub info
- or on the top repo config. Abort or return None if no source found."""
- if util.safehasattr(repo, '_subparent'):
- source = util.url(repo._subsource)
- if source.isabs():
- return str(source)
- source.path = posixpath.normpath(source.path)
- parent = _abssource(repo._subparent, push, abort=False)
- if parent:
- parent = util.url(util.pconvert(parent))
- parent.path = posixpath.join(parent.path or '', source.path)
- parent.path = posixpath.normpath(parent.path)
- return str(parent)
- else: # recursion reached top repo
- if util.safehasattr(repo, '_subtoppath'):
- return repo._subtoppath
- if push and repo.ui.config('paths', 'default-push'):
- return repo.ui.config('paths', 'default-push')
- if repo.ui.config('paths', 'default'):
- return repo.ui.config('paths', 'default')
- if repo.sharedpath != repo.path:
- # chop off the .hg component to get the default path form
- return os.path.dirname(repo.sharedpath)
- if abort:
- raise util.Abort(_("default path for subrepository not found"))
- def _sanitize(ui, path, ignore):
- for dirname, dirs, names in os.walk(path):
- for i, d in enumerate(dirs):
- if d.lower() == ignore:
- del dirs[i]
- break
- if os.path.basename(dirname).lower() != '.hg':
- continue
- for f in names:
- if f.lower() == 'hgrc':
- ui.warn(_("warning: removing potentially hostile 'hgrc' "
- "in '%s'\n") % dirname)
- os.unlink(os.path.join(dirname, f))
- def subrepo(ctx, path):
- """return instance of the right subrepo class for subrepo in 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
- global hg
- import hg as h
- hg = h
- pathutil.pathauditor(ctx._repo.root)(path)
- state = ctx.substate[path]
- if state[2] not in types:
- raise util.Abort(_('unknown subrepo type %s') % state[2])
- return types[state[2]](ctx, path, state[:2])
- def newcommitphase(ui, ctx):
- commitphase = phases.newcommitphase(ui)
- substate = getattr(ctx, "substate", None)
- if not substate:
- return commitphase
- check = ui.config('phases', 'checksubrepos', 'follow')
- if check not in ('ignore', 'follow', 'abort'):
- raise util.Abort(_('invalid phases.checksubrepos configuration: %s')
- % (check))
- if check == 'ignore':
- return commitphase
- maxphase = phases.public
- maxsub = None
- for s in sorted(substate):
- sub = ctx.sub(s)
- subphase = sub.phase(substate[s][1])
- if maxphase < subphase:
- maxphase = subphase
- maxsub = s
- if commitphase < maxphase:
- if check == 'abort':
- raise util.Abort(_("can't commit in %s phase"
- " conflicting %s from subrepository %s") %
- (phases.phasenames[commitphase],
- phases.phasenames[maxphase], maxsub))
- ui.warn(_("warning: changes are committed in"
- " %s phase from subrepository %s\n") %
- (phases.phasenames[maxphase], maxsub))
- return maxphase
- return commitphase
- # subrepo classes need to implement the following abstract class:
- class abstractsubrepo(object):
- def storeclean(self, path):
- """
- returns true if the repository has not changed since it was last
- cloned from or pushed to a given repository.
- """
- return False
- def dirty(self, ignoreupdate=False):
- """returns true if the dirstate of the subrepo is dirty or does not
- match current stored state. If ignoreupdate is true, only check
- whether the subrepo has uncommitted changes in its dirstate.
- """
- raise NotImplementedError
- def basestate(self):
- """current working directory base state, disregarding .hgsubstate
- state and working directory modifications"""
- raise NotImplementedError
- def checknested(self, path):
- """check if path is a subrepository within this repository"""
- return False
- 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 phase(self, state):
- """returns phase of specified state in the subrepository.
- """
- return phases.public
- def remove(self):
- """remove the subrepo
- (should verify the dirstate is not dirty first)
- """
- raise NotImplementedError
- def get(self, state, overwrite=False):
- """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, opts):
- """perform whatever action is analogous to 'hg push'
- This may be a no-op on some systems.
- """
- raise NotImplementedError
- def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
- return []
- def cat(self, ui, match, prefix, **opts):
- return 1
- def status(self, rev2, **opts):
- return [], [], [], [], [], [], []
- def diff(self, ui, diffopts, node2, match, prefix, **opts):
- pass
- def outgoing(self, ui, dest, opts):
- return 1
- def incoming(self, ui, source, opts):
- return 1
- def files(self):
- """return filename iterator"""
- raise NotImplementedError
- def filedata(self, name):
- """return file data"""
- raise NotImplementedError
- def fileflags(self, name):
- """return file flags"""
- return ''
- def archive(self, ui, archiver, prefix, match=None):
- if match is not None:
- files = [f for f in self.files() if match(f)]
- else:
- files = self.files()
- total = len(files)
- relpath = subrelpath(self)
- ui.progress(_('archiving (%s)') % relpath, 0,
- unit=_('files'), total=total)
- for i, name in enumerate(files):
- flags = self.fileflags(name)
- mode = 'x' in flags and 0755 or 0644
- symlink = 'l' in flags
- archiver.addfile(os.path.join(prefix, self._path, name),
- mode, symlink, self.filedata(name))
- ui.progress(_('archiving (%s)') % relpath, i + 1,
- unit=_('files'), total=total)
- ui.progress(_('archiving (%s)') % relpath, None)
- return total
- def walk(self, match):
- '''
- walk recursively through the directory tree, finding all files
- matched by the match function
- '''
- pass
- def forget(self, ui, match, prefix):
- return ([], [])
- def revert(self, ui, substate, *pats, **opts):
- ui.warn('%s: reverting %s subrepos is unsupported\n' \
- % (substate[0], substate[2]))
- return []
- def shortid(self, revid):
- return revid
- class hgsubrepo(abstractsubrepo):
- def __init__(self, ctx, path, state):
- self._path = path
- self._state = state
- r = ctx._repo
- root = r.wjoin(path)
- create = False
- if not os.path.exists(os.path.join(root, '.hg')):
- create = True
- util.makedirs(root)
- self._repo = hg.repository(r.baseui, root, create=create)
- for s, k in [('ui', 'commitsubrepos')]:
- v = r.ui.config(s, k)
- if v:
- self._repo.ui.setconfig(s, k, v, 'subrepo')
- self._repo.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
- self._initrepo(r, state[0], create)
- def storeclean(self, path):
- clean = True
- lock = self._repo.lock()
- itercache = self._calcstorehash(path)
- try:
- for filehash in self._readstorehashcache(path):
- if filehash != itercache.next():
- clean = False
- break
- except StopIteration:
- # the cached and current pull states have a different size
- clean = False
- if clean:
- try:
- itercache.next()
- # the cached and current pull states have a different size
- clean = False
- except StopIteration:
- pass
- lock.release()
- return clean
- def _calcstorehash(self, remotepath):
- '''calculate a unique "store hash"
- This method is used to to detect when there are changes that may
- require a push to a given remote path.'''
- # sort the files that will be hashed in increasing (likely) file size
- filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
- yield '# %s\n' % _expandedabspath(remotepath)
- for relname in filelist:
- absname = os.path.normpath(self._repo.join(relname))
- yield '%s = %s\n' % (relname, _calcfilehash(absname))
- def _getstorehashcachepath(self, remotepath):
- '''get a unique path for the store hash cache'''
- return self._repo.join(os.path.join(
- 'cache', 'storehash', _getstorehashcachename(remotepath)))
- def _readstorehashcache(self, remotepath):
- '''read the store hash cache for a given remote repository'''
- cachefile = self._getstorehashcachepath(remotepath)
- if not os.path.exists(cachefile):
- return ''
- fd = open(cachefile, 'r')
- pullstate = fd.readlines()
- fd.close()
- return pullstate
- def _cachestorehash(self, remotepath):
- '''cache the current store hash
- Each remote repo requires its own store hash cache, because a subrepo
- store may be "clean" versus a given remote repo, but not versus another
- '''
- cachefile = self._getstorehashcachepath(remotepath)
- lock = self._repo.lock()
- storehash = list(self._calcstorehash(remotepath))
- cachedir = os.path.dirname(cachefile)
- if not os.path.exists(cachedir):
- util.makedirs(cachedir, notindexed=True)
- fd = open(cachefile, 'w')
- fd.writelines(storehash)
- fd.close()
- lock.release()
- @annotatesubrepoerror
- def _initrepo(self, parentrepo, source, create):
- self._repo._subparent = parentrepo
- self._repo._subsource = source
- if create:
- fp = self._repo.opener("hgrc", "w", text=True)
- fp.write('[paths]\n')
- def addpathconfig(key, value):
- if value:
- fp.write('%s = %s\n' % (key, value))
- self._repo.ui.setconfig('paths', key, value, 'subrepo')
- defpath = _abssource(self._repo, abort=False)
- defpushpath = _abssource(self._repo, True, abort=False)
- addpathconfig('default', defpath)
- if defpath != defpushpath:
- addpathconfig('default-push', defpushpath)
- fp.close()
- @annotatesubrepoerror
- def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
- return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
- os.path.join(prefix, self._path), explicitonly)
- @annotatesubrepoerror
- def cat(self, ui, match, prefix, **opts):
- rev = self._state[1]
- ctx = self._repo[rev]
- return cmdutil.cat(ui, self._repo, ctx, match, prefix, **opts)
- @annotatesubrepoerror
- 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: error "%s" in subrepository "%s"\n')
- % (inst, subrelpath(self)))
- return [], [], [], [], [], [], []
- @annotatesubrepoerror
- def diff(self, ui, diffopts, node2, match, prefix, **opts):
- try:
- node1 = node.bin(self._state[1])
- # We currently expect node2 to come from substate and be
- # in hex format
- if node2 is not None:
- node2 = node.bin(node2)
- cmdutil.diffordiffstat(ui, self._repo, diffopts,
- node1, node2, match,
- prefix=posixpath.join(prefix, self._path),
- listsubrepos=True, **opts)
- except error.RepoLookupError, inst:
- self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
- % (inst, subrelpath(self)))
- @annotatesubrepoerror
- def archive(self, ui, archiver, prefix, match=None):
- self._get(self._state + ('hg',))
- total = abstractsubrepo.archive(self, ui, archiver, prefix, match)
- rev = self._state[1]
- ctx = self._repo[rev]
- for subpath in ctx.substate:
- s = subrepo(ctx, subpath)
- submatch = matchmod.narrowmatcher(subpath, match)
- total += s.archive(
- ui, archiver, os.path.join(prefix, self._path), submatch)
- return total
- @annotatesubrepoerror
- def dirty(self, ignoreupdate=False):
- r = self._state[1]
- if r == '' and not ignoreupdate: # no state recorded
- return True
- w = self._repo[None]
- if r != w.p1().hex() and not ignoreupdate:
- # different version checked out
- return True
- return w.dirty() # working directory changed
- def basestate(self):
- return self._repo['.'].hex()
- def checknested(self, path):
- return self._repo._checknested(self._repo.wjoin(path))
- @annotatesubrepoerror
- def commit(self, text, user, date):
- # don't bother committing in the subrepo if it's only been
- # updated
- if not self.dirty(True):
- return self._repo['.'].hex()
- self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
- n = self._repo.commit(text, user, date)
- if not n:
- return self._repo['.'].hex() # different version checked out
- return node.hex(n)
- @annotatesubrepoerror
- def phase(self, state):
- return self._repo[state].phase()
- @annotatesubrepoerror
- 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') % subrelpath(self))
- hg.clean(self._repo, node.nullid, False)
- def _get(self, state):
- source, revision, kind = state
- if revision in self._repo.unfiltered():
- return True
- self._repo._subsource = source
- srcurl = _abssource(self._repo)
- other = hg.peer(self._repo, {}, srcurl)
- if len(self._repo) == 0:
- self._repo.ui.status(_('cloning subrepo %s from %s\n')
- % (subrelpath(self), srcurl))
- parentrepo = self._repo._subparent
- shutil.rmtree(self._repo.path)
- other, cloned = hg.clone(self._repo._subparent.baseui, {},
- other, self._repo.root,
- update=False)
- self._repo = cloned.local()
- self._initrepo(parentrepo, source, create=True)
- self._cachestorehash(srcurl)
- else:
- self._repo.ui.status(_('pulling subrepo %s from %s\n')
- % (subrelpath(self), srcurl))
- cleansub = self.storeclean(srcurl)
- remotebookmarks = other.listkeys('bookmarks')
- self._repo.pull(other)
- bookmarks.updatefromremote(self._repo.ui, self._repo,
- remotebookmarks, srcurl)
- if cleansub:
- # keep the repo clean after pull
- self._cachestorehash(srcurl)
- return False
- @annotatesubrepoerror
- def get(self, state, overwrite=False):
- inrepo = self._get(state)
- source, revision, kind = state
- repo = self._repo
- repo.ui.debug("getting subrepo %s\n" % self._path)
- if inrepo:
- urepo = repo.unfiltered()
- ctx = urepo[revision]
- if ctx.hidden():
- urepo.ui.warn(
- _('revision %s in subrepo %s is hidden\n') \
- % (revision[0:12], self._path))
- repo = urepo
- hg.updaterepo(repo, revision, overwrite)
- @annotatesubrepoerror
- def merge(self, state):
- self._get(state)
- cur = self._repo['.']
- dst = self._repo[state[1]]
- anc = dst.ancestor(cur)
- def mergefunc():
- if anc == cur and dst.branch() == cur.branch():
- self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
- hg.update(self._repo, state[1])
- elif anc == dst:
- self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
- else:
- self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
- hg.merge(self._repo, state[1], remind=False)
- wctx = self._repo[None]
- if self.dirty():
- if anc != dst:
- if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
- mergefunc()
- else:
- mergefunc()
- else:
- mergefunc()
- @annotatesubrepoerror
- def push(self, opts):
- force = opts.get('force')
- newbranch = opts.get('new_branch')
- ssh = opts.get('ssh')
- # push subrepos depth-first for coherent ordering
- c = self._repo['']
- subs = c.substate # only repos that are committed
- for s in sorted(subs):
- if c.sub(s).push(opts) == 0:
- return False
- dsturl = _abssource(self._repo, True)
- if not force:
- if self.storeclean(dsturl):
- self._repo.ui.status(
- _('no changes made to subrepo %s since last push to %s\n')
- % (subrelpath(self), dsturl))
- return None
- self._repo.ui.status(_('pushing subrepo %s to %s\n') %
- (subrelpath(self), dsturl))
- other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
- res = self._repo.push(other, force, newbranch=newbranch)
- # the repo is now clean
- self._cachestorehash(dsturl)
- return res
- @annotatesubrepoerror
- def outgoing(self, ui, dest, opts):
- return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
- @annotatesubrepoerror
- def incoming(self, ui, source, opts):
- return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
- @annotatesubrepoerror
- def files(self):
- rev = self._state[1]
- ctx = self._repo[rev]
- return ctx.manifest()
- def filedata(self, name):
- rev = self._state[1]
- return self._repo[rev][name].data()
- def fileflags(self, name):
- rev = self._state[1]
- ctx = self._repo[rev]
- return ctx.flags(name)
- def walk(self, match):
- ctx = self._repo[None]
- return ctx.walk(match)
- @annotatesubrepoerror
- def forget(self, ui, match, prefix):
- return cmdutil.forget(ui, self._repo, match,
- os.path.join(prefix, self._path), True)
- @annotatesubrepoerror
- def revert(self, ui, substate, *pats, **opts):
- # reverting a subrepo is a 2 step process:
- # 1. if the no_backup is not set, revert all modified
- # files inside the subrepo
- # 2. update the subrepo to the revision specified in
- # the corresponding substate dictionary
- ui.status(_('reverting subrepo %s\n') % substate[0])
- if not opts.get('no_backup'):
- # Revert all files on the subrepo, creating backups
- # Note that this will not recursively revert subrepos
- # We could do it if there was a set:subrepos() predicate
- opts = opts.copy()
- opts['date'] = None
- opts['rev'] = substate[1]
- pats = []
- if not opts.get('all'):
- pats = ['set:modified()']
- self.filerevert(ui, *pats, **opts)
- # Update the repo to the revision specified in the given substate
- self.get(substate, overwrite=True)
- def filerevert(self, ui, *pats, **opts):
- ctx = self._repo[opts['rev']]
- parents = self._repo.dirstate.parents()
- if opts.get('all'):
- pats = ['set:modified()']
- else:
- pats = []
- cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
- def shortid(self, revid):
- return revid[:12]
- class svnsubrepo(abstractsubrepo):
- def __init__(self, ctx, path, state):
- self._path = path
- self._state = state
- self._ctx = ctx
- self._ui = ctx._repo.ui
- self._exe = util.findexe('svn')
- if not self._exe:
- raise util.Abort(_("'svn' executable not found for subrepo '%s'")
- % self._path)
- def _svncommand(self, commands, filename='', failok=False):
- cmd = [self._exe]
- extrakw = {}
- if not self._ui.interactive():
- # Making stdin be a pipe should prevent svn from behaving
- # interactively even if we can't pass --non-interactive.
- extrakw['stdin'] = subprocess.PIPE
- # Starting in svn 1.5 --non-interactive is a global flag
- # instead of being per-command, but we need to support 1.4 so
- # we have to be intelligent about what commands take
- # --non-interactive.
- if commands[0] in ('update', 'checkout', 'commit'):
- cmd.append('--non-interactive')
- cmd.extend(commands)
- if filename is not None:
- path = os.path.join(self._ctx._repo.origroot, self._path, filename)
- cmd.append(path)
- env = dict(os.environ)
- # Avoid localized output, preserve current locale for everything else.
- lc_all = env.get('LC_ALL')
- if lc_all:
- env['LANG'] = lc_all
- del env['LC_ALL']
- env['LC_MESSAGES'] = 'C'
- p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- universal_newlines=True, env=env, **extrakw)
- stdout, stderr = p.communicate()
- stderr = stderr.strip()
- if not failok:
- if p.returncode:
- raise util.Abort(stderr or 'exited with code %d' % p.returncode)
- if stderr:
- self._ui.warn(stderr + '\n')
- return stdout, stderr
- @propertycache
- def _svnversion(self):
- output, err = self._svncommand(['--version', '--quiet'], filename=None)
- m = re.search(r'^(\d+)\.(\d+)', output)
- if not m:
- raise util.Abort(_('cannot retrieve svn tool version'))
- return (int(m.group(1)), int(m.group(2)))
- def _wcrevs(self):
- # Get the working directory revision as well as the last
- # commit revision so we can compare the subrepo state with
- # both. We used to store the working directory one.
- output, err = self._svncommand(['info', '--xml'])
- doc = xml.dom.minidom.parseString(output)
- entries = doc.getElementsByTagName('entry')
- lastrev, rev = '0', '0'
- if entries:
- rev = str(entries[0].getAttribute('revision')) or '0'
- commits = entries[0].getElementsByTagName('commit')
- if commits:
- lastrev = str(commits[0].getAttribute('revision')) or '0'
- return (lastrev, rev)
- def _wcrev(self):
- return self._wcrevs()[0]
- def _wcchanged(self):
- """Return (changes, extchanges, missing) where changes is True
- if the working directory was changed, extchanges is
- True if any of these changes concern an external entry and missing
- is True if any change is a missing entry.
- """
- output, err = self._svncommand(['status', '--xml'])
- externals, changes, missing = [], [], []
- doc = xml.dom.minidom.parseString(output)
- 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)
- elif item == 'missing':
- missing.append(path)
- if (item not in ('', 'normal', 'unversioned', 'external')
- or props not in ('', 'none', 'normal')):
- changes.append(path)
- for path in changes:
- for ext in externals:
- if path == ext or path.startswith(ext + os.sep):
- return True, True, bool(missing)
- return bool(changes), False, bool(missing)
- def dirty(self, ignoreupdate=False):
- if not self._wcchanged()[0]:
- if self._state[1] in self._wcrevs() or ignoreupdate:
- return False
- return True
- def basestate(self):
- lastrev, rev = self._wcrevs()
- if lastrev != rev:
- # Last committed rev is not the same than rev. We would
- # like to take lastrev but we do not know if the subrepo
- # URL exists at lastrev. Test it and fallback to rev it
- # is not there.
- try:
- self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
- return lastrev
- except error.Abort:
- pass
- return rev
- @annotatesubrepoerror
- def commit(self, text, user, date):
- # user and date are out of our hands since svn is centralized
- changed, extchanged, missing = self._wcchanged()
- if not changed:
- return self.basestate()
- if extchanged:
- # Do not try to commit externals
- raise util.Abort(_('cannot commit svn externals'))
- if missing:
- # svn can commit with missing entries but aborting like hg
- # seems a better approach.
- raise util.Abort(_('cannot commit missing svn entries'))
- commitinfo, err = self._svncommand(['commit', '-m', text])
- self._ui.status(commitinfo)
- newrev = re.search('Committed revision ([0-9]+).', commitinfo)
- if not newrev:
- if not commitinfo.strip():
- # Sometimes, our definition of "changed" differs from
- # svn one. For instance, svn ignores missing files
- # when committing. If there are only missing files, no
- # commit is made, no output and no error code.
- raise util.Abort(_('failed to commit svn changes'))
- raise util.Abort(commitinfo.splitlines()[-1])
- newrev = newrev.groups()[0]
- self._ui.status(self._svncommand(['update', '-r', newrev])[0])
- return newrev
- @annotatesubrepoerror
- def remove(self):
- if self.dirty():
- self._ui.warn(_('not removing repo %s because '
- 'it has changes.\n') % self._path)
- return
- self._ui.note(_('removing subrepo %s\n') % self._path)
- def onerror(function, path, excinfo):
- if function is not os.remove:
- raise
- # read-only files cannot be unlinked under Windows
- s = os.stat(path)
- if (s.st_mode & stat.S_IWRITE) != 0:
- raise
- os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
- os.remove(path)
- path = self._ctx._repo.wjoin(self._path)
- shutil.rmtree(path, onerror=onerror)
- try:
- os.removedirs(os.path.dirname(path))
- except OSError:
- pass
- @annotatesubrepoerror
- def get(self, state, overwrite=False):
- if overwrite:
- self._svncommand(['revert', '--recursive'])
- args = ['checkout']
- if self._svnversion >= (1, 5):
- args.append('--force')
- # The revision must be specified at the end of the URL to properly
- # update to a directory which has since been deleted and recreated.
- args.append('%s@%s' % (state[0], state[1]))
- status, err = self._svncommand(args, failok=True)
- _sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn')
- if not re.search('Checked out revision [0-9]+.', status):
- if ('is already a working copy for a different URL' in err
- and (self._wcchanged()[:2] == (False, False))):
- # obstructed but clean working copy, so just blow it away.
- self.remove()
- self.get(state, overwrite=False)
- return
- raise util.Abort((status or err).splitlines()[-1])
- self._ui.status(status)
- @annotatesubrepoerror
- def merge(self, state):
- old = self._state[1]
- new = state[1]
- wcrev = self._wcrev()
- if new != wcrev:
- dirty = old == wcrev or self._wcchanged()[0]
- if _updateprompt(self._ui, self, dirty, wcrev, new):
- self.get(state, False)
- def push(self, opts):
- # push is a no-op for SVN
- return True
- @annotatesubrepoerror
- def files(self):
- output = self._svncommand(['list', '--recursive', '--xml'])[0]
- doc = xml.dom.minidom.parseString(output)
- paths = []
- for e in doc.getElementsByTagName('entry'):
- kind = str(e.getAttribute('kind'))
- if kind != 'file':
- continue
- name = ''.join(c.data for c
- in e.getElementsByTagName('name')[0].childNodes
- if c.nodeType == c.TEXT_NODE)
- paths.append(name.encode('utf-8'))
- return paths
- def filedata(self, name):
- return self._svncommand(['cat'], name)[0]
- class gitsubrepo(abstractsubrepo):
- def __init__(self, ctx, path, state):
- self._state = state
- self._ctx = ctx
- self._path = path
- self._relpath = os.path.join(reporelpath(ctx._repo), path)
- self._abspath = ctx._repo.wjoin(path)
- self._subparent = ctx._repo
- self._ui = ctx._repo.ui
- self._ensuregit()
- def _ensuregit(self):
- try:
- self._gitexecutable = 'git'
- out, err = self._gitnodir(['--version'])
- except OSError, e:
- if e.errno != 2 or os.name != 'nt':
- raise
- self._gitexecutable = 'git.cmd'
- out, err = self._gitnodir(['--version'])
- versionstatus = self._checkversion(out)
- if versionstatus == 'unknown':
- self._ui.warn(_('cannot retrieve git version\n'))
- elif versionstatus == 'abort':
- raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
- elif versionstatus == 'warning':
- self._ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
- @staticmethod
- def _checkversion(out):
- '''ensure git version is new enough
- >>> _checkversion = gitsubrepo._checkversion
- >>> _checkversion('git version 1.6.0')
- 'ok'
- >>> _checkversion('git version 1.8.5')
- 'ok'
- >>> _checkversion('git version 1.4.0')
- 'abort'
- >>> _checkversion('git version 1.5.0')
- 'warning'
- >>> _checkversion('git version 1.9-rc0')
- 'ok'
- >>> _checkversion('git version 1.9.0.265.g81cdec2')
- 'ok'
- >>> _checkversion('git version 1.9.0.GIT')
- 'ok'
- >>> _checkversion('git version 12345')
- 'unknown'
- >>> _checkversion('no')
- 'unknown'
- '''
- m = re.search(r'^git version (\d+)\.(\d+)', out)
- if not m:
- return 'unknown'
- version = (int(m.group(1)), int(m.group(2)))
- # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
- # despite the docstring comment. For now, error on 1.4.0, warn on
- # 1.5.0 but attempt to continue.
- if version < (1, 5):
- return 'abort'
- elif version < (1, 6):
- return 'warning'
- return 'ok'
- def _gitcommand(self, commands, env=None, stream=False):
- return self._gitdir(commands, env=env, stream=stream)[0]
- def _gitdir(self, commands, env=None, stream=False):
- return self._gitnodir(commands, env=env, stream=stream,
- cwd=self._abspath)
- def _gitnodir(self, commands, env=None, stream=False, cwd=None):
- """Calls the git command
- The methods tries to call the git command. versions prior to 1.6.0
- are not supported and very probably fail.
- """
- self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
- # unless ui.quiet is set, print git's stderr,
- # which is mostly progress and useful info
- errpipe = None
- if self._ui.quiet:
- errpipe = open(os.devnull, 'w')
- p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
- cwd=cwd, env=env, close_fds=util.closefds,
- stdout=subprocess.PIPE, stderr=errpipe)
- if stream:
- return p.stdout, None
- retdata = p.stdout.read().strip()
- # wait for the child to exit to avoid race condition.
- p.wait()
- if p.returncode != 0 and p.returncode != 1:
- # there are certain error codes that are ok
- command = commands[0]
- if command in ('cat-file', 'symbolic-ref'):
- return retdata, p.returncode
- # for all others, abort
- raise util.Abort('git %s error %d in %s' %
- (command, p.returncode, self._relpath))
- return retdata, p.returncode
- def _gitmissing(self):
- return not os.path.exists(os.path.join(self._abspath, '.git'))
- def _gitstate(self):
- return self._gitcommand(['rev-parse', 'HEAD'])
- def _gitcurrentbranch(self):
- current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
- if err:
- current = None
- return current
- def _gitremote(self, remote):
- out = self._gitcommand(['remote', 'show', '-n', remote])
- line = out.split('\n')[1]
- i = line.index('URL: ') + len('URL: ')
- return line[i:]
- def _githavelocally(self, revision):
- out, code = self._gitdir(['cat-file', '-e', revision])
- return code == 0
- def _gitisancestor(self, r1, r2):
- base = self._gitcommand(['merge-base', r1, r2])
- return base == r1
- def _gitisbare(self):
- return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
- def _gitupdatestat(self):
- """This must be run before git diff-index.
- diff-index only looks at changes to file stat;
- this command looks at file contents and updates the stat."""
- self._gitcommand(['update-index', '-q', '--refresh'])
- def _gitbranchmap(self):
- '''returns 2 things:
- a map from git branch to revision
- a map from revision to branches'''
- branch2rev = {}
- rev2branch = {}
- out = self._gitcommand(['for-each-ref', '--format',
- '%(objectname) %(refname)'])
- for line in out.split('\n'):
- revision, ref = line.split(' ')
- if (not ref.startswith('refs/heads/') and
- not ref.startswith('refs/remotes/')):
- continue
- if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
- continue # ignore remote/HEAD redirects
- branch2rev[ref] = revision
- rev2branch.setdefault(revision, []).append(ref)
- return branch2rev, rev2branch
- def _gittracking(self, branches):
- 'return map of remote branch to local tracking branch'
- # assumes no more than one local tracking branch for each remote
- tracking = {}
- for b in branches:
- if b.startswith('refs/remotes/'):
- continue
- bname = b.split('/', 2)[2]
- remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
- if remote:
- ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
- tracking['refs/remotes/%s/%s' %
- (remote, ref.split('/', 2)[2])] = b
- return tracking
- def _abssource(self, source):
- if '://' not in source:
- # recognize the scp syntax as an absolute source
- colon = source.find(':')
- if colon != -1 and '/' not in source[:colon]:
- return source
- self._subsource = source
- return _abssource(self)
- def _fetch(self, source, revision):
- if self._gitmissing():
- source = self._abssource(source)
- self._ui.status(_('cloning subrepo %s from %s\n') %
- (self._relpath, source))
- self._gitnodir(['clone', source, self._abspath])
- if self._githavelocally(revision):
- return
- self._ui.status(_('pulling subrepo %s from %s\n') %
- (self._relpath, self._gitremote('origin')))
- # try only origin: the originally cloned repo
- self._gitcommand(['fetch'])
- if not self._githavelocally(revision):
- raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
- (revision, self._relpath))
- @annotatesubrepoerror
- def dirty(self, ignoreupdate=False):
- if self._gitmissing():
- return self._state[1] != ''
- if self._gitisbare():
- return True
- if not ignoreupdate and self._state[1] != self._gitstate():
- # different version checked out
- return True
- # check for staged changes or modified files; ignore untracked files
- self._gitupdatestat()
- out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
- return code == 1
- def basestate(self):
- return self._gitstate()
- @annotatesubrepoerror
- def get(self, state, overwrite=False):
- source, revision, kind = state
- if not revision:
- self.remove()
- return
- self._fetch(source, revision)
- # if the repo was set to be bare, u…
Large files files are truncated, but you can click here to view the full file