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