mercurial/subrepo.py
changeset 36009 55e8efa2451a
parent 35925 533f04d4cb6d
child 36139 b72c6ff4e4c0
equal deleted inserted replaced
36008:006ff7268c5c 36009:55e8efa2451a
     1 # subrepo.py - sub-repository handling for Mercurial
     1 # subrepo.py - sub-repository classes and factory
     2 #
     2 #
     3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
     3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
     4 #
     4 #
     5 # This software may be used and distributed according to the terms of the
     5 # This software may be used and distributed according to the terms of the
     6 # GNU General Public License version 2 or any later version.
     6 # GNU General Public License version 2 or any later version.
    17 import subprocess
    17 import subprocess
    18 import sys
    18 import sys
    19 import tarfile
    19 import tarfile
    20 import xml.dom.minidom
    20 import xml.dom.minidom
    21 
    21 
    22 
       
    23 from .i18n import _
    22 from .i18n import _
    24 from . import (
    23 from . import (
    25     cmdutil,
    24     cmdutil,
    26     config,
       
    27     encoding,
    25     encoding,
    28     error,
    26     error,
    29     exchange,
    27     exchange,
    30     filemerge,
       
    31     logcmdutil,
    28     logcmdutil,
    32     match as matchmod,
    29     match as matchmod,
    33     node,
    30     node,
    34     pathutil,
    31     pathutil,
    35     phases,
    32     phases,
    36     pycompat,
    33     pycompat,
    37     scmutil,
    34     scmutil,
       
    35     subrepoutil,
    38     util,
    36     util,
    39     vfs as vfsmod,
    37     vfs as vfsmod,
    40 )
    38 )
    41 
    39 
    42 hg = None
    40 hg = None
       
    41 reporelpath = subrepoutil.reporelpath
       
    42 subrelpath = subrepoutil.subrelpath
       
    43 _abssource = subrepoutil._abssource
    43 propertycache = util.propertycache
    44 propertycache = util.propertycache
    44 
       
    45 nullstate = ('', '', 'empty')
       
    46 
    45 
    47 def _expandedabspath(path):
    46 def _expandedabspath(path):
    48     '''
    47     '''
    49     get a path or url and if it is a path expand it and return an absolute path
    48     get a path or url and if it is a path expand it and return an absolute path
    50     '''
    49     '''
    79             raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
    78             raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
    80                                cause=sys.exc_info())
    79                                cause=sys.exc_info())
    81         return res
    80         return res
    82     return decoratedmethod
    81     return decoratedmethod
    83 
    82 
    84 def state(ctx, ui):
       
    85     """return a state dict, mapping subrepo paths configured in .hgsub
       
    86     to tuple: (source from .hgsub, revision from .hgsubstate, kind
       
    87     (key in types dict))
       
    88     """
       
    89     p = config.config()
       
    90     repo = ctx.repo()
       
    91     def read(f, sections=None, remap=None):
       
    92         if f in ctx:
       
    93             try:
       
    94                 data = ctx[f].data()
       
    95             except IOError as err:
       
    96                 if err.errno != errno.ENOENT:
       
    97                     raise
       
    98                 # handle missing subrepo spec files as removed
       
    99                 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
       
   100                         repo.pathto(f))
       
   101                 return
       
   102             p.parse(f, data, sections, remap, read)
       
   103         else:
       
   104             raise error.Abort(_("subrepo spec file \'%s\' not found") %
       
   105                              repo.pathto(f))
       
   106     if '.hgsub' in ctx:
       
   107         read('.hgsub')
       
   108 
       
   109     for path, src in ui.configitems('subpaths'):
       
   110         p.set('subpaths', path, src, ui.configsource('subpaths', path))
       
   111 
       
   112     rev = {}
       
   113     if '.hgsubstate' in ctx:
       
   114         try:
       
   115             for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
       
   116                 l = l.lstrip()
       
   117                 if not l:
       
   118                     continue
       
   119                 try:
       
   120                     revision, path = l.split(" ", 1)
       
   121                 except ValueError:
       
   122                     raise error.Abort(_("invalid subrepository revision "
       
   123                                        "specifier in \'%s\' line %d")
       
   124                                      % (repo.pathto('.hgsubstate'), (i + 1)))
       
   125                 rev[path] = revision
       
   126         except IOError as err:
       
   127             if err.errno != errno.ENOENT:
       
   128                 raise
       
   129 
       
   130     def remap(src):
       
   131         for pattern, repl in p.items('subpaths'):
       
   132             # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
       
   133             # does a string decode.
       
   134             repl = util.escapestr(repl)
       
   135             # However, we still want to allow back references to go
       
   136             # through unharmed, so we turn r'\\1' into r'\1'. Again,
       
   137             # extra escapes are needed because re.sub string decodes.
       
   138             repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl)
       
   139             try:
       
   140                 src = re.sub(pattern, repl, src, 1)
       
   141             except re.error as e:
       
   142                 raise error.Abort(_("bad subrepository pattern in %s: %s")
       
   143                                  % (p.source('subpaths', pattern), e))
       
   144         return src
       
   145 
       
   146     state = {}
       
   147     for path, src in p[''].items():
       
   148         kind = 'hg'
       
   149         if src.startswith('['):
       
   150             if ']' not in src:
       
   151                 raise error.Abort(_('missing ] in subrepository source'))
       
   152             kind, src = src.split(']', 1)
       
   153             kind = kind[1:]
       
   154             src = src.lstrip() # strip any extra whitespace after ']'
       
   155 
       
   156         if not util.url(src).isabs():
       
   157             parent = _abssource(repo, abort=False)
       
   158             if parent:
       
   159                 parent = util.url(parent)
       
   160                 parent.path = posixpath.join(parent.path or '', src)
       
   161                 parent.path = posixpath.normpath(parent.path)
       
   162                 joined = str(parent)
       
   163                 # Remap the full joined path and use it if it changes,
       
   164                 # else remap the original source.
       
   165                 remapped = remap(joined)
       
   166                 if remapped == joined:
       
   167                     src = remap(src)
       
   168                 else:
       
   169                     src = remapped
       
   170 
       
   171         src = remap(src)
       
   172         state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
       
   173 
       
   174     return state
       
   175 
       
   176 def writestate(repo, state):
       
   177     """rewrite .hgsubstate in (outer) repo with these subrepo states"""
       
   178     lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
       
   179                                                 if state[s][1] != nullstate[1]]
       
   180     repo.wwrite('.hgsubstate', ''.join(lines), '')
       
   181 
       
   182 def submerge(repo, wctx, mctx, actx, overwrite, labels=None):
       
   183     """delegated from merge.applyupdates: merging of .hgsubstate file
       
   184     in working context, merging context and ancestor context"""
       
   185     if mctx == actx: # backwards?
       
   186         actx = wctx.p1()
       
   187     s1 = wctx.substate
       
   188     s2 = mctx.substate
       
   189     sa = actx.substate
       
   190     sm = {}
       
   191 
       
   192     repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
       
   193 
       
   194     def debug(s, msg, r=""):
       
   195         if r:
       
   196             r = "%s:%s:%s" % r
       
   197         repo.ui.debug("  subrepo %s: %s %s\n" % (s, msg, r))
       
   198 
       
   199     promptssrc = filemerge.partextras(labels)
       
   200     for s, l in sorted(s1.iteritems()):
       
   201         prompts = None
       
   202         a = sa.get(s, nullstate)
       
   203         ld = l # local state with possible dirty flag for compares
       
   204         if wctx.sub(s).dirty():
       
   205             ld = (l[0], l[1] + "+")
       
   206         if wctx == actx: # overwrite
       
   207             a = ld
       
   208 
       
   209         prompts = promptssrc.copy()
       
   210         prompts['s'] = s
       
   211         if s in s2:
       
   212             r = s2[s]
       
   213             if ld == r or r == a: # no change or local is newer
       
   214                 sm[s] = l
       
   215                 continue
       
   216             elif ld == a: # other side changed
       
   217                 debug(s, "other changed, get", r)
       
   218                 wctx.sub(s).get(r, overwrite)
       
   219                 sm[s] = r
       
   220             elif ld[0] != r[0]: # sources differ
       
   221                 prompts['lo'] = l[0]
       
   222                 prompts['ro'] = r[0]
       
   223                 if repo.ui.promptchoice(
       
   224                     _(' subrepository sources for %(s)s differ\n'
       
   225                       'use (l)ocal%(l)s source (%(lo)s)'
       
   226                       ' or (r)emote%(o)s source (%(ro)s)?'
       
   227                       '$$ &Local $$ &Remote') % prompts, 0):
       
   228                     debug(s, "prompt changed, get", r)
       
   229                     wctx.sub(s).get(r, overwrite)
       
   230                     sm[s] = r
       
   231             elif ld[1] == a[1]: # local side is unchanged
       
   232                 debug(s, "other side changed, get", r)
       
   233                 wctx.sub(s).get(r, overwrite)
       
   234                 sm[s] = r
       
   235             else:
       
   236                 debug(s, "both sides changed")
       
   237                 srepo = wctx.sub(s)
       
   238                 prompts['sl'] = srepo.shortid(l[1])
       
   239                 prompts['sr'] = srepo.shortid(r[1])
       
   240                 option = repo.ui.promptchoice(
       
   241                     _(' subrepository %(s)s diverged (local revision: %(sl)s, '
       
   242                       'remote revision: %(sr)s)\n'
       
   243                       '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?'
       
   244                       '$$ &Merge $$ &Local $$ &Remote')
       
   245                     % prompts, 0)
       
   246                 if option == 0:
       
   247                     wctx.sub(s).merge(r)
       
   248                     sm[s] = l
       
   249                     debug(s, "merge with", r)
       
   250                 elif option == 1:
       
   251                     sm[s] = l
       
   252                     debug(s, "keep local subrepo revision", l)
       
   253                 else:
       
   254                     wctx.sub(s).get(r, overwrite)
       
   255                     sm[s] = r
       
   256                     debug(s, "get remote subrepo revision", r)
       
   257         elif ld == a: # remote removed, local unchanged
       
   258             debug(s, "remote removed, remove")
       
   259             wctx.sub(s).remove()
       
   260         elif a == nullstate: # not present in remote or ancestor
       
   261             debug(s, "local added, keep")
       
   262             sm[s] = l
       
   263             continue
       
   264         else:
       
   265             if repo.ui.promptchoice(
       
   266                 _(' local%(l)s changed subrepository %(s)s'
       
   267                   ' which remote%(o)s removed\n'
       
   268                   'use (c)hanged version or (d)elete?'
       
   269                   '$$ &Changed $$ &Delete') % prompts, 0):
       
   270                 debug(s, "prompt remove")
       
   271                 wctx.sub(s).remove()
       
   272 
       
   273     for s, r in sorted(s2.items()):
       
   274         prompts = None
       
   275         if s in s1:
       
   276             continue
       
   277         elif s not in sa:
       
   278             debug(s, "remote added, get", r)
       
   279             mctx.sub(s).get(r)
       
   280             sm[s] = r
       
   281         elif r != sa[s]:
       
   282             prompts = promptssrc.copy()
       
   283             prompts['s'] = s
       
   284             if repo.ui.promptchoice(
       
   285                 _(' remote%(o)s changed subrepository %(s)s'
       
   286                   ' which local%(l)s removed\n'
       
   287                   'use (c)hanged version or (d)elete?'
       
   288                   '$$ &Changed $$ &Delete') % prompts, 0) == 0:
       
   289                 debug(s, "prompt recreate", r)
       
   290                 mctx.sub(s).get(r)
       
   291                 sm[s] = r
       
   292 
       
   293     # record merged .hgsubstate
       
   294     writestate(repo, sm)
       
   295     return sm
       
   296 
       
   297 def precommit(ui, wctx, status, match, force=False):
       
   298     """Calculate .hgsubstate changes that should be applied before committing
       
   299 
       
   300     Returns (subs, commitsubs, newstate) where
       
   301     - subs: changed subrepos (including dirty ones)
       
   302     - commitsubs: dirty subrepos which the caller needs to commit recursively
       
   303     - newstate: new state dict which the caller must write to .hgsubstate
       
   304 
       
   305     This also updates the given status argument.
       
   306     """
       
   307     subs = []
       
   308     commitsubs = set()
       
   309     newstate = wctx.substate.copy()
       
   310 
       
   311     # only manage subrepos and .hgsubstate if .hgsub is present
       
   312     if '.hgsub' in wctx:
       
   313         # we'll decide whether to track this ourselves, thanks
       
   314         for c in status.modified, status.added, status.removed:
       
   315             if '.hgsubstate' in c:
       
   316                 c.remove('.hgsubstate')
       
   317 
       
   318         # compare current state to last committed state
       
   319         # build new substate based on last committed state
       
   320         oldstate = wctx.p1().substate
       
   321         for s in sorted(newstate.keys()):
       
   322             if not match(s):
       
   323                 # ignore working copy, use old state if present
       
   324                 if s in oldstate:
       
   325                     newstate[s] = oldstate[s]
       
   326                     continue
       
   327                 if not force:
       
   328                     raise error.Abort(
       
   329                         _("commit with new subrepo %s excluded") % s)
       
   330             dirtyreason = wctx.sub(s).dirtyreason(True)
       
   331             if dirtyreason:
       
   332                 if not ui.configbool('ui', 'commitsubrepos'):
       
   333                     raise error.Abort(dirtyreason,
       
   334                         hint=_("use --subrepos for recursive commit"))
       
   335                 subs.append(s)
       
   336                 commitsubs.add(s)
       
   337             else:
       
   338                 bs = wctx.sub(s).basestate()
       
   339                 newstate[s] = (newstate[s][0], bs, newstate[s][2])
       
   340                 if oldstate.get(s, (None, None, None))[1] != bs:
       
   341                     subs.append(s)
       
   342 
       
   343         # check for removed subrepos
       
   344         for p in wctx.parents():
       
   345             r = [s for s in p.substate if s not in newstate]
       
   346             subs += [s for s in r if match(s)]
       
   347         if subs:
       
   348             if (not match('.hgsub') and
       
   349                 '.hgsub' in (wctx.modified() + wctx.added())):
       
   350                 raise error.Abort(_("can't commit subrepos without .hgsub"))
       
   351             status.modified.insert(0, '.hgsubstate')
       
   352 
       
   353     elif '.hgsub' in status.removed:
       
   354         # clean up .hgsubstate when .hgsub is removed
       
   355         if ('.hgsubstate' in wctx and
       
   356             '.hgsubstate' not in (status.modified + status.added +
       
   357                                   status.removed)):
       
   358             status.removed.insert(0, '.hgsubstate')
       
   359 
       
   360     return subs, commitsubs, newstate
       
   361 
       
   362 def _updateprompt(ui, sub, dirty, local, remote):
    83 def _updateprompt(ui, sub, dirty, local, remote):
   363     if dirty:
    84     if dirty:
   364         msg = (_(' subrepository sources for %s differ\n'
    85         msg = (_(' subrepository sources for %s differ\n'
   365                  'use (l)ocal source (%s) or (r)emote source (%s)?'
    86                  'use (l)ocal source (%s) or (r)emote source (%s)?'
   366                  '$$ &Local $$ &Remote')
    87                  '$$ &Local $$ &Remote')
   371                  'use (l)ocal source (%s) or (r)emote source (%s)?'
    92                  'use (l)ocal source (%s) or (r)emote source (%s)?'
   372                  '$$ &Local $$ &Remote')
    93                  '$$ &Local $$ &Remote')
   373                % (subrelpath(sub), local, remote))
    94                % (subrelpath(sub), local, remote))
   374     return ui.promptchoice(msg, 0)
    95     return ui.promptchoice(msg, 0)
   375 
    96 
   376 def reporelpath(repo):
       
   377     """return path to this (sub)repo as seen from outermost repo"""
       
   378     parent = repo
       
   379     while util.safehasattr(parent, '_subparent'):
       
   380         parent = parent._subparent
       
   381     return repo.root[len(pathutil.normasprefix(parent.root)):]
       
   382 
       
   383 def subrelpath(sub):
       
   384     """return path to this subrepo as seen from outermost repo"""
       
   385     return sub._relpath
       
   386 
       
   387 def _abssource(repo, push=False, abort=True):
       
   388     """return pull/push path of repo - either based on parent repo .hgsub info
       
   389     or on the top repo config. Abort or return None if no source found."""
       
   390     if util.safehasattr(repo, '_subparent'):
       
   391         source = util.url(repo._subsource)
       
   392         if source.isabs():
       
   393             return bytes(source)
       
   394         source.path = posixpath.normpath(source.path)
       
   395         parent = _abssource(repo._subparent, push, abort=False)
       
   396         if parent:
       
   397             parent = util.url(util.pconvert(parent))
       
   398             parent.path = posixpath.join(parent.path or '', source.path)
       
   399             parent.path = posixpath.normpath(parent.path)
       
   400             return bytes(parent)
       
   401     else: # recursion reached top repo
       
   402         path = None
       
   403         if util.safehasattr(repo, '_subtoppath'):
       
   404             path = repo._subtoppath
       
   405         elif push and repo.ui.config('paths', 'default-push'):
       
   406             path = repo.ui.config('paths', 'default-push')
       
   407         elif repo.ui.config('paths', 'default'):
       
   408             path = repo.ui.config('paths', 'default')
       
   409         elif repo.shared():
       
   410             # chop off the .hg component to get the default path form.  This has
       
   411             # already run through vfsmod.vfs(..., realpath=True), so it doesn't
       
   412             # have problems with 'C:'
       
   413             return os.path.dirname(repo.sharedpath)
       
   414         if path:
       
   415             # issue5770: 'C:\' and 'C:' are not equivalent paths.  The former is
       
   416             # as expected: an absolute path to the root of the C: drive.  The
       
   417             # latter is a relative path, and works like so:
       
   418             #
       
   419             #   C:\>cd C:\some\path
       
   420             #   C:\>D:
       
   421             #   D:\>python -c "import os; print os.path.abspath('C:')"
       
   422             #   C:\some\path
       
   423             #
       
   424             #   D:\>python -c "import os; print os.path.abspath('C:relative')"
       
   425             #   C:\some\path\relative
       
   426             if util.hasdriveletter(path):
       
   427                 if len(path) == 2 or path[2:3] not in br'\/':
       
   428                     path = os.path.abspath(path)
       
   429             return path
       
   430 
       
   431     if abort:
       
   432         raise error.Abort(_("default path for subrepository not found"))
       
   433 
       
   434 def _sanitize(ui, vfs, ignore):
    97 def _sanitize(ui, vfs, ignore):
   435     for dirname, dirs, names in vfs.walk():
    98     for dirname, dirs, names in vfs.walk():
   436         for i, d in enumerate(dirs):
    99         for i, d in enumerate(dirs):
   437             if d.lower() == ignore:
   100             if d.lower() == ignore:
   438                 del dirs[i]
   101                 del dirs[i]
   506     _checktype(repo.ui, state[2])
   169     _checktype(repo.ui, state[2])
   507     subrev = ''
   170     subrev = ''
   508     if state[2] == 'hg':
   171     if state[2] == 'hg':
   509         subrev = "0" * 40
   172         subrev = "0" * 40
   510     return types[state[2]](pctx, path, (state[0], subrev), True)
   173     return types[state[2]](pctx, path, (state[0], subrev), True)
   511 
       
   512 def newcommitphase(ui, ctx):
       
   513     commitphase = phases.newcommitphase(ui)
       
   514     substate = getattr(ctx, "substate", None)
       
   515     if not substate:
       
   516         return commitphase
       
   517     check = ui.config('phases', 'checksubrepos')
       
   518     if check not in ('ignore', 'follow', 'abort'):
       
   519         raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
       
   520                          % (check))
       
   521     if check == 'ignore':
       
   522         return commitphase
       
   523     maxphase = phases.public
       
   524     maxsub = None
       
   525     for s in sorted(substate):
       
   526         sub = ctx.sub(s)
       
   527         subphase = sub.phase(substate[s][1])
       
   528         if maxphase < subphase:
       
   529             maxphase = subphase
       
   530             maxsub = s
       
   531     if commitphase < maxphase:
       
   532         if check == 'abort':
       
   533             raise error.Abort(_("can't commit in %s phase"
       
   534                                " conflicting %s from subrepository %s") %
       
   535                              (phases.phasenames[commitphase],
       
   536                               phases.phasenames[maxphase], maxsub))
       
   537         ui.warn(_("warning: changes are committed in"
       
   538                   " %s phase from subrepository %s\n") %
       
   539                 (phases.phasenames[maxphase], maxsub))
       
   540         return maxphase
       
   541     return commitphase
       
   542 
   174 
   543 # subrepo classes need to implement the following abstract class:
   175 # subrepo classes need to implement the following abstract class:
   544 
   176 
   545 class abstractsubrepo(object):
   177 class abstractsubrepo(object):
   546 
   178