subrepo: split non-core functions to new module
Resolves import cycle caused by subrepo -> cmdutil. Still we have another
cycle, cmdutil -> context -> subrepo, but where I think importing context
is wrong. Perhaps we'll need repo.makememctx().
--- a/hgext/mq.py Wed Feb 07 23:22:53 2018 +0900
+++ b/hgext/mq.py Tue Feb 06 22:36:38 2018 +0900
@@ -94,7 +94,7 @@
revsetlang,
scmutil,
smartset,
- subrepo,
+ subrepoutil,
util,
vfs as vfsmod,
)
@@ -970,8 +970,8 @@
wctx = repo[None]
pctx = repo['.']
overwrite = False
- mergedsubstate = subrepo.submerge(repo, pctx, wctx, wctx,
- overwrite)
+ mergedsubstate = subrepoutil.submerge(repo, pctx, wctx, wctx,
+ overwrite)
files += mergedsubstate.keys()
match = scmutil.matchfiles(repo, files or [])
--- a/mercurial/cmdutil.py Wed Feb 07 23:22:53 2018 +0900
+++ b/mercurial/cmdutil.py Tue Feb 06 22:36:38 2018 +0900
@@ -40,6 +40,7 @@
rewriteutil,
scmutil,
smartset,
+ subrepoutil,
templater,
util,
vfs as vfsmod,
@@ -2307,13 +2308,12 @@
# subrepo.precommit(). To minimize the risk of this hack, we do
# nothing if .hgsub does not exist.
if '.hgsub' in wctx or '.hgsub' in old:
- from . import subrepo # avoid cycle: cmdutil -> subrepo -> cmdutil
- subs, commitsubs, newsubstate = subrepo.precommit(
+ subs, commitsubs, newsubstate = subrepoutil.precommit(
ui, wctx, wctx._status, matcher)
# amend should abort if commitsubrepos is enabled
assert not commitsubs
if subs:
- subrepo.writestate(repo, newsubstate)
+ subrepoutil.writestate(repo, newsubstate)
filestoamend = set(f for f in wctx.files() if matcher(f))
--- a/mercurial/context.py Wed Feb 07 23:22:53 2018 +0900
+++ b/mercurial/context.py Tue Feb 06 22:36:38 2018 +0900
@@ -46,6 +46,7 @@
scmutil,
sparse,
subrepo,
+ subrepoutil,
util,
)
@@ -173,7 +174,7 @@
@propertycache
def substate(self):
- return subrepo.state(self, self._repo.ui)
+ return subrepoutil.state(self, self._repo.ui)
def subrev(self, subpath):
return self.substate[subpath][1]
--- a/mercurial/localrepo.py Wed Feb 07 23:22:53 2018 +0900
+++ b/mercurial/localrepo.py Tue Feb 06 22:36:38 2018 +0900
@@ -57,7 +57,7 @@
scmutil,
sparse,
store,
- subrepo,
+ subrepoutil,
tags as tagsmod,
transaction,
txnutil,
@@ -1833,7 +1833,7 @@
status.modified.extend(status.clean) # mq may commit clean files
# check subrepos
- subs, commitsubs, newstate = subrepo.precommit(
+ subs, commitsubs, newstate = subrepoutil.precommit(
self.ui, wctx, status, match, force=force)
# make sure all explicit patterns are matched
@@ -1870,10 +1870,10 @@
for s in sorted(commitsubs):
sub = wctx.sub(s)
self.ui.status(_('committing subrepository %s\n') %
- subrepo.subrelpath(sub))
+ subrepoutil.subrelpath(sub))
sr = sub.commit(cctx._text, user, date)
newstate[s] = (newstate[s][0], sr)
- subrepo.writestate(self, newstate)
+ subrepoutil.writestate(self, newstate)
p1, p2 = self.dirstate.parents()
hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
@@ -1983,7 +1983,7 @@
self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
parent2=xp2)
# set the new commit is proper phase
- targetphase = subrepo.newcommitphase(self.ui, ctx)
+ targetphase = subrepoutil.newcommitphase(self.ui, ctx)
if targetphase:
# retract boundary do not alter parent changeset.
# if a parent have higher the resulting phase will
--- a/mercurial/merge.py Wed Feb 07 23:22:53 2018 +0900
+++ b/mercurial/merge.py Tue Feb 06 22:36:38 2018 +0900
@@ -31,7 +31,7 @@
obsutil,
pycompat,
scmutil,
- subrepo,
+ subrepoutil,
util,
worker,
)
@@ -1445,7 +1445,7 @@
z = 0
if [a for a in actions['r'] if a[0] == '.hgsubstate']:
- subrepo.submerge(repo, wctx, mctx, wctx, overwrite, labels)
+ subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
# record path conflicts
for f, args, msg in actions['p']:
@@ -1495,7 +1495,7 @@
updated = len(actions['g'])
if [a for a in actions['g'] if a[0] == '.hgsubstate']:
- subrepo.submerge(repo, wctx, mctx, wctx, overwrite, labels)
+ subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
# forget (manifest only, just log it) (must come first)
for f, args, msg in actions['f']:
@@ -1583,8 +1583,8 @@
z += 1
progress(_updating, z, item=f, total=numupdates, unit=_files)
if f == '.hgsubstate': # subrepo states need updating
- subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
- overwrite, labels)
+ subrepoutil.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
+ overwrite, labels)
continue
wctx[f].audit()
complete, r = ms.preresolve(f, wctx)
@@ -1913,7 +1913,7 @@
# Prompt and create actions. Most of this is in the resolve phase
# already, but we can't handle .hgsubstate in filemerge or
- # subrepo.submerge yet so we have to keep prompting for it.
+ # subrepoutil.submerge yet so we have to keep prompting for it.
if '.hgsubstate' in actionbyfile:
f = '.hgsubstate'
m, args, msg = actionbyfile[f]
--- a/mercurial/subrepo.py Wed Feb 07 23:22:53 2018 +0900
+++ b/mercurial/subrepo.py Tue Feb 06 22:36:38 2018 +0900
@@ -1,4 +1,4 @@
-# subrepo.py - sub-repository handling for Mercurial
+# subrepo.py - sub-repository classes and factory
#
# Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
#
@@ -19,15 +19,12 @@
import tarfile
import xml.dom.minidom
-
from .i18n import _
from . import (
cmdutil,
- config,
encoding,
error,
exchange,
- filemerge,
logcmdutil,
match as matchmod,
node,
@@ -35,15 +32,17 @@
phases,
pycompat,
scmutil,
+ subrepoutil,
util,
vfs as vfsmod,
)
hg = None
+reporelpath = subrepoutil.reporelpath
+subrelpath = subrepoutil.subrelpath
+_abssource = subrepoutil._abssource
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
@@ -81,284 +80,6 @@
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()
- repo = ctx.repo()
- def read(f, sections=None, remap=None):
- if f in ctx:
- try:
- data = ctx[f].data()
- except IOError as err:
- if err.errno != errno.ENOENT:
- raise
- # handle missing subrepo spec files as removed
- ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
- repo.pathto(f))
- return
- p.parse(f, data, sections, remap, read)
- else:
- raise error.Abort(_("subrepo spec file \'%s\' not found") %
- repo.pathto(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 error.Abort(_("invalid subrepository revision "
- "specifier in \'%s\' line %d")
- % (repo.pathto('.hgsubstate'), (i + 1)))
- rev[path] = revision
- except IOError as 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 = util.escapestr(repl)
- # 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(br'\\\\([0-9]+)', br'\\\1', repl)
- try:
- src = re.sub(pattern, repl, src, 1)
- except re.error as e:
- raise error.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 error.Abort(_('missing ] in subrepository 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(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)
- if state[s][1] != nullstate[1]]
- repo.wwrite('.hgsubstate', ''.join(lines), '')
-
-def submerge(repo, wctx, mctx, actx, overwrite, labels=None):
- """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))
-
- promptssrc = filemerge.partextras(labels)
- for s, l in sorted(s1.iteritems()):
- prompts = None
- 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
-
- prompts = promptssrc.copy()
- prompts['s'] = s
- 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
- prompts['lo'] = l[0]
- prompts['ro'] = r[0]
- if repo.ui.promptchoice(
- _(' subrepository sources for %(s)s differ\n'
- 'use (l)ocal%(l)s source (%(lo)s)'
- ' or (r)emote%(o)s source (%(ro)s)?'
- '$$ &Local $$ &Remote') % prompts, 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)
- prompts['sl'] = srepo.shortid(l[1])
- prompts['sr'] = srepo.shortid(r[1])
- option = repo.ui.promptchoice(
- _(' subrepository %(s)s diverged (local revision: %(sl)s, '
- 'remote revision: %(sr)s)\n'
- '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?'
- '$$ &Merge $$ &Local $$ &Remote')
- % prompts, 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%(l)s changed subrepository %(s)s'
- ' which remote%(o)s removed\n'
- 'use (c)hanged version or (d)elete?'
- '$$ &Changed $$ &Delete') % prompts, 0):
- debug(s, "prompt remove")
- wctx.sub(s).remove()
-
- for s, r in sorted(s2.items()):
- prompts = None
- 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]:
- prompts = promptssrc.copy()
- prompts['s'] = s
- if repo.ui.promptchoice(
- _(' remote%(o)s changed subrepository %(s)s'
- ' which local%(l)s removed\n'
- 'use (c)hanged version or (d)elete?'
- '$$ &Changed $$ &Delete') % prompts, 0) == 0:
- debug(s, "prompt recreate", r)
- mctx.sub(s).get(r)
- sm[s] = r
-
- # record merged .hgsubstate
- writestate(repo, sm)
- return sm
-
-def precommit(ui, wctx, status, match, force=False):
- """Calculate .hgsubstate changes that should be applied before committing
-
- Returns (subs, commitsubs, newstate) where
- - subs: changed subrepos (including dirty ones)
- - commitsubs: dirty subrepos which the caller needs to commit recursively
- - newstate: new state dict which the caller must write to .hgsubstate
-
- This also updates the given status argument.
- """
- subs = []
- commitsubs = set()
- newstate = wctx.substate.copy()
-
- # only manage subrepos and .hgsubstate if .hgsub is present
- if '.hgsub' in wctx:
- # we'll decide whether to track this ourselves, thanks
- for c in status.modified, status.added, status.removed:
- if '.hgsubstate' in c:
- c.remove('.hgsubstate')
-
- # compare current state to last committed state
- # build new substate based on last committed state
- oldstate = wctx.p1().substate
- for s in sorted(newstate.keys()):
- if not match(s):
- # ignore working copy, use old state if present
- if s in oldstate:
- newstate[s] = oldstate[s]
- continue
- if not force:
- raise error.Abort(
- _("commit with new subrepo %s excluded") % s)
- dirtyreason = wctx.sub(s).dirtyreason(True)
- if dirtyreason:
- if not ui.configbool('ui', 'commitsubrepos'):
- raise error.Abort(dirtyreason,
- hint=_("use --subrepos for recursive commit"))
- subs.append(s)
- commitsubs.add(s)
- else:
- bs = wctx.sub(s).basestate()
- newstate[s] = (newstate[s][0], bs, newstate[s][2])
- if oldstate.get(s, (None, None, None))[1] != bs:
- subs.append(s)
-
- # check for removed subrepos
- for p in wctx.parents():
- r = [s for s in p.substate if s not in newstate]
- subs += [s for s in r if match(s)]
- if subs:
- if (not match('.hgsub') and
- '.hgsub' in (wctx.modified() + wctx.added())):
- raise error.Abort(_("can't commit subrepos without .hgsub"))
- status.modified.insert(0, '.hgsubstate')
-
- elif '.hgsub' in status.removed:
- # clean up .hgsubstate when .hgsub is removed
- if ('.hgsubstate' in wctx and
- '.hgsubstate' not in (status.modified + status.added +
- status.removed)):
- status.removed.insert(0, '.hgsubstate')
-
- return subs, commitsubs, newstate
-
def _updateprompt(ui, sub, dirty, local, remote):
if dirty:
msg = (_(' subrepository sources for %s differ\n'
@@ -373,64 +94,6 @@
% (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"""
- return sub._relpath
-
-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 bytes(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 bytes(parent)
- else: # recursion reached top repo
- path = None
- if util.safehasattr(repo, '_subtoppath'):
- path = repo._subtoppath
- elif push and repo.ui.config('paths', 'default-push'):
- path = repo.ui.config('paths', 'default-push')
- elif repo.ui.config('paths', 'default'):
- path = repo.ui.config('paths', 'default')
- elif repo.shared():
- # chop off the .hg component to get the default path form. This has
- # already run through vfsmod.vfs(..., realpath=True), so it doesn't
- # have problems with 'C:'
- return os.path.dirname(repo.sharedpath)
- if path:
- # issue5770: 'C:\' and 'C:' are not equivalent paths. The former is
- # as expected: an absolute path to the root of the C: drive. The
- # latter is a relative path, and works like so:
- #
- # C:\>cd C:\some\path
- # C:\>D:
- # D:\>python -c "import os; print os.path.abspath('C:')"
- # C:\some\path
- #
- # D:\>python -c "import os; print os.path.abspath('C:relative')"
- # C:\some\path\relative
- if util.hasdriveletter(path):
- if len(path) == 2 or path[2:3] not in br'\/':
- path = os.path.abspath(path)
- return path
-
- if abort:
- raise error.Abort(_("default path for subrepository not found"))
-
def _sanitize(ui, vfs, ignore):
for dirname, dirs, names in vfs.walk():
for i, d in enumerate(dirs):
@@ -509,37 +172,6 @@
subrev = "0" * 40
return types[state[2]](pctx, path, (state[0], subrev), True)
-def newcommitphase(ui, ctx):
- commitphase = phases.newcommitphase(ui)
- substate = getattr(ctx, "substate", None)
- if not substate:
- return commitphase
- check = ui.config('phases', 'checksubrepos')
- if check not in ('ignore', 'follow', 'abort'):
- raise error.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 error.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):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/subrepoutil.py Tue Feb 06 22:36:38 2018 +0900
@@ -0,0 +1,392 @@
+# subrepoutil.py - sub-repository operations and substate handling
+#
+# 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.
+
+from __future__ import absolute_import
+
+import errno
+import os
+import posixpath
+import re
+
+from .i18n import _
+from . import (
+ config,
+ error,
+ filemerge,
+ pathutil,
+ phases,
+ util,
+)
+
+nullstate = ('', '', 'empty')
+
+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()
+ repo = ctx.repo()
+ def read(f, sections=None, remap=None):
+ if f in ctx:
+ try:
+ data = ctx[f].data()
+ except IOError as err:
+ if err.errno != errno.ENOENT:
+ raise
+ # handle missing subrepo spec files as removed
+ ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
+ repo.pathto(f))
+ return
+ p.parse(f, data, sections, remap, read)
+ else:
+ raise error.Abort(_("subrepo spec file \'%s\' not found") %
+ repo.pathto(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 error.Abort(_("invalid subrepository revision "
+ "specifier in \'%s\' line %d")
+ % (repo.pathto('.hgsubstate'), (i + 1)))
+ rev[path] = revision
+ except IOError as 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 = util.escapestr(repl)
+ # 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(br'\\\\([0-9]+)', br'\\\1', repl)
+ try:
+ src = re.sub(pattern, repl, src, 1)
+ except re.error as e:
+ raise error.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 error.Abort(_('missing ] in subrepository 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(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)
+ if state[s][1] != nullstate[1]]
+ repo.wwrite('.hgsubstate', ''.join(lines), '')
+
+def submerge(repo, wctx, mctx, actx, overwrite, labels=None):
+ """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))
+
+ promptssrc = filemerge.partextras(labels)
+ for s, l in sorted(s1.iteritems()):
+ prompts = None
+ 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
+
+ prompts = promptssrc.copy()
+ prompts['s'] = s
+ 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
+ prompts['lo'] = l[0]
+ prompts['ro'] = r[0]
+ if repo.ui.promptchoice(
+ _(' subrepository sources for %(s)s differ\n'
+ 'use (l)ocal%(l)s source (%(lo)s)'
+ ' or (r)emote%(o)s source (%(ro)s)?'
+ '$$ &Local $$ &Remote') % prompts, 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)
+ prompts['sl'] = srepo.shortid(l[1])
+ prompts['sr'] = srepo.shortid(r[1])
+ option = repo.ui.promptchoice(
+ _(' subrepository %(s)s diverged (local revision: %(sl)s, '
+ 'remote revision: %(sr)s)\n'
+ '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?'
+ '$$ &Merge $$ &Local $$ &Remote')
+ % prompts, 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%(l)s changed subrepository %(s)s'
+ ' which remote%(o)s removed\n'
+ 'use (c)hanged version or (d)elete?'
+ '$$ &Changed $$ &Delete') % prompts, 0):
+ debug(s, "prompt remove")
+ wctx.sub(s).remove()
+
+ for s, r in sorted(s2.items()):
+ prompts = None
+ 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]:
+ prompts = promptssrc.copy()
+ prompts['s'] = s
+ if repo.ui.promptchoice(
+ _(' remote%(o)s changed subrepository %(s)s'
+ ' which local%(l)s removed\n'
+ 'use (c)hanged version or (d)elete?'
+ '$$ &Changed $$ &Delete') % prompts, 0) == 0:
+ debug(s, "prompt recreate", r)
+ mctx.sub(s).get(r)
+ sm[s] = r
+
+ # record merged .hgsubstate
+ writestate(repo, sm)
+ return sm
+
+def precommit(ui, wctx, status, match, force=False):
+ """Calculate .hgsubstate changes that should be applied before committing
+
+ Returns (subs, commitsubs, newstate) where
+ - subs: changed subrepos (including dirty ones)
+ - commitsubs: dirty subrepos which the caller needs to commit recursively
+ - newstate: new state dict which the caller must write to .hgsubstate
+
+ This also updates the given status argument.
+ """
+ subs = []
+ commitsubs = set()
+ newstate = wctx.substate.copy()
+
+ # only manage subrepos and .hgsubstate if .hgsub is present
+ if '.hgsub' in wctx:
+ # we'll decide whether to track this ourselves, thanks
+ for c in status.modified, status.added, status.removed:
+ if '.hgsubstate' in c:
+ c.remove('.hgsubstate')
+
+ # compare current state to last committed state
+ # build new substate based on last committed state
+ oldstate = wctx.p1().substate
+ for s in sorted(newstate.keys()):
+ if not match(s):
+ # ignore working copy, use old state if present
+ if s in oldstate:
+ newstate[s] = oldstate[s]
+ continue
+ if not force:
+ raise error.Abort(
+ _("commit with new subrepo %s excluded") % s)
+ dirtyreason = wctx.sub(s).dirtyreason(True)
+ if dirtyreason:
+ if not ui.configbool('ui', 'commitsubrepos'):
+ raise error.Abort(dirtyreason,
+ hint=_("use --subrepos for recursive commit"))
+ subs.append(s)
+ commitsubs.add(s)
+ else:
+ bs = wctx.sub(s).basestate()
+ newstate[s] = (newstate[s][0], bs, newstate[s][2])
+ if oldstate.get(s, (None, None, None))[1] != bs:
+ subs.append(s)
+
+ # check for removed subrepos
+ for p in wctx.parents():
+ r = [s for s in p.substate if s not in newstate]
+ subs += [s for s in r if match(s)]
+ if subs:
+ if (not match('.hgsub') and
+ '.hgsub' in (wctx.modified() + wctx.added())):
+ raise error.Abort(_("can't commit subrepos without .hgsub"))
+ status.modified.insert(0, '.hgsubstate')
+
+ elif '.hgsub' in status.removed:
+ # clean up .hgsubstate when .hgsub is removed
+ if ('.hgsubstate' in wctx and
+ '.hgsubstate' not in (status.modified + status.added +
+ status.removed)):
+ status.removed.insert(0, '.hgsubstate')
+
+ return subs, commitsubs, newstate
+
+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"""
+ return sub._relpath
+
+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 bytes(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 bytes(parent)
+ else: # recursion reached top repo
+ path = None
+ if util.safehasattr(repo, '_subtoppath'):
+ path = repo._subtoppath
+ elif push and repo.ui.config('paths', 'default-push'):
+ path = repo.ui.config('paths', 'default-push')
+ elif repo.ui.config('paths', 'default'):
+ path = repo.ui.config('paths', 'default')
+ elif repo.shared():
+ # chop off the .hg component to get the default path form. This has
+ # already run through vfsmod.vfs(..., realpath=True), so it doesn't
+ # have problems with 'C:'
+ return os.path.dirname(repo.sharedpath)
+ if path:
+ # issue5770: 'C:\' and 'C:' are not equivalent paths. The former is
+ # as expected: an absolute path to the root of the C: drive. The
+ # latter is a relative path, and works like so:
+ #
+ # C:\>cd C:\some\path
+ # C:\>D:
+ # D:\>python -c "import os; print os.path.abspath('C:')"
+ # C:\some\path
+ #
+ # D:\>python -c "import os; print os.path.abspath('C:relative')"
+ # C:\some\path\relative
+ if util.hasdriveletter(path):
+ if len(path) == 2 or path[2:3] not in br'\/':
+ path = os.path.abspath(path)
+ return path
+
+ if abort:
+ raise error.Abort(_("default path for subrepository not found"))
+
+def newcommitphase(ui, ctx):
+ commitphase = phases.newcommitphase(ui)
+ substate = getattr(ctx, "substate", None)
+ if not substate:
+ return commitphase
+ check = ui.config('phases', 'checksubrepos')
+ if check not in ('ignore', 'follow', 'abort'):
+ raise error.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 error.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