# HG changeset patch # User Yuya Nishihara # Date 1517924198 -32400 # Node ID 55e8efa2451a0999b56978e471dc31dc8066a0fb # Parent 006ff7268c5c306a5a8c4a748737ab91d57e57b8 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(). diff -r 006ff7268c5c -r 55e8efa2451a hgext/mq.py --- 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 []) diff -r 006ff7268c5c -r 55e8efa2451a mercurial/cmdutil.py --- 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)) diff -r 006ff7268c5c -r 55e8efa2451a mercurial/context.py --- 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] diff -r 006ff7268c5c -r 55e8efa2451a mercurial/localrepo.py --- 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 diff -r 006ff7268c5c -r 55e8efa2451a mercurial/merge.py --- 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] diff -r 006ff7268c5c -r 55e8efa2451a mercurial/subrepo.py --- 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 # @@ -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): diff -r 006ff7268c5c -r 55e8efa2451a mercurial/subrepoutil.py --- /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 +# +# 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