changeset 35186:aef2b98d9352

merge with stable
author Kevin Bullock <kbullock+mercurial@ringworld.org>
date Fri, 01 Dec 2017 15:21:05 -0600
parents 66ecde8a704d (current diff) fff9ffa2ea05 (diff)
children b4b0aed7bfaf
files hgext/largefiles/lfcommands.py mercurial/dispatch.py mercurial/help/environment.txt mercurial/ui.py tests/test-import.t
diffstat 19 files changed, 558 insertions(+), 51 deletions(-) [+]
line wrap: on
line diff
--- a/.hgsigs	Thu Nov 16 03:52:42 2017 +0100
+++ b/.hgsigs	Fri Dec 01 15:21:05 2017 -0600
@@ -155,3 +155,4 @@
 1e2454b60e5936f5e77498cab2648db469504487 0 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
+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
--- a/.hgtags	Thu Nov 16 03:52:42 2017 +0100
+++ b/.hgtags	Fri Dec 01 15:21:05 2017 -0600
@@ -168,3 +168,4 @@
 1e2454b60e5936f5e77498cab2648db469504487 4.4-rc
 0ccb43d4cf01d013ae05917ec4f305509f851b2d 4.4
 cabc840ffdee8a72f3689fb77dd74d04fdc2bc04 4.4.1
+a92b9f8e11ba330614cdfd6af0e03b15c1ff3797 4.4.2
--- a/hgext/largefiles/lfcommands.py	Thu Nov 16 03:52:42 2017 +0100
+++ b/hgext/largefiles/lfcommands.py	Fri Dec 01 15:21:05 2017 -0600
@@ -455,6 +455,7 @@
             lfiles = [f for f in lfiles if f in filelist]
 
         update = {}
+        dropped = set()
         updated, removed = 0, 0
         wvfs = repo.wvfs
         wctx = repo[None]
@@ -476,7 +477,11 @@
                 expecthash = lfutil.readasstandin(wctx[relstandin])
                 if expecthash != '':
                     if lfile not in wctx: # not switched to normal file
-                        wvfs.unlinkpath(rellfile, ignoremissing=True)
+                        if repo.dirstate[relstandin] != '?':
+                            wvfs.unlinkpath(rellfile, ignoremissing=True)
+                        else:
+                            dropped.add(rellfile)
+
                     # use normallookup() to allocate an entry in largefiles
                     # dirstate to prevent lfilesrepo.status() from reporting
                     # missing files as removed.
@@ -496,6 +501,15 @@
         lfdirstate.write()
 
         if lfiles:
+            lfiles = [f for f in lfiles if f not in dropped]
+
+            for f in dropped:
+                repo.wvfs.unlinkpath(lfutil.standin(f))
+
+                # This needs to happen for dropped files, otherwise they stay in
+                # the M state.
+                lfutil.synclfdirstate(repo, lfdirstate, f, normallookup)
+
             statuswriter(_('getting changed largefiles\n'))
             cachelfiles(ui, repo, None, lfiles)
 
--- a/mercurial/chgserver.py	Thu Nov 16 03:52:42 2017 +0100
+++ b/mercurial/chgserver.py	Fri Dec 01 15:21:05 2017 -0600
@@ -220,8 +220,17 @@
         newui._csystem = srcui._csystem
 
     # command line args
-    args = args[:]
-    dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
+    options = {}
+    if srcui.plain('strictflags'):
+        options.update(dispatch._earlyparseopts(args))
+    else:
+        args = args[:]
+        options['config'] = dispatch._earlygetopt(['--config'], args)
+        cwds = dispatch._earlygetopt(['--cwd'], args)
+        options['cwd'] = cwds and cwds[-1] or ''
+        rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
+        options['repository'] = rpath and rpath[-1] or ''
+    dispatch._parseconfig(newui, options['config'])
 
     # stolen from tortoisehg.util.copydynamicconfig()
     for section, name, value in srcui.walkconfig():
@@ -232,10 +241,9 @@
         newui.setconfig(section, name, value, source)
 
     # load wd and repo config, copied from dispatch.py
-    cwds = dispatch._earlygetopt(['--cwd'], args)
-    cwd = cwds and os.path.realpath(cwds[-1]) or None
-    rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
-    rpath = rpath and rpath[-1] or ''
+    cwd = options['cwd']
+    cwd = cwd and os.path.realpath(cwd) or None
+    rpath = options['repository']
     path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
 
     return (newui, newlui)
--- a/mercurial/dispatch.py	Thu Nov 16 03:52:42 2017 +0100
+++ b/mercurial/dispatch.py	Fri Dec 01 15:21:05 2017 -0600
@@ -150,6 +150,8 @@
     try:
         if not req.ui:
             req.ui = uimod.ui.load()
+        if req.ui.plain('strictflags'):
+            req.earlyoptions.update(_earlyparseopts(req.args))
         if _earlyreqoptbool(req, 'traceback', ['--traceback']):
             req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
 
@@ -644,6 +646,12 @@
 
     return configs
 
+def _earlyparseopts(args):
+    options = {}
+    fancyopts.fancyopts(args, commands.globalopts, options,
+                        gnu=False, early=True)
+    return options
+
 def _earlygetopt(aliases, args, strip=True):
     """Return list of values for an option (or aliases).
 
@@ -732,12 +740,16 @@
 
 def _earlyreqopt(req, name, aliases):
     """Peek a list option without using a full options table"""
+    if req.ui.plain('strictflags'):
+        return req.earlyoptions[name]
     values = _earlygetopt(aliases, req.args, strip=False)
     req.earlyoptions[name] = values
     return values
 
 def _earlyreqoptstr(req, name, aliases):
     """Peek a string option without using a full options table"""
+    if req.ui.plain('strictflags'):
+        return req.earlyoptions[name]
     value = (_earlygetopt(aliases, req.args, strip=False) or [''])[-1]
     req.earlyoptions[name] = value
     return value
@@ -745,13 +757,15 @@
 def _earlyreqoptbool(req, name, aliases):
     """Peek a boolean option without using a full options table
 
-    >>> req = request([b'x', b'--debugger'])
+    >>> req = request([b'x', b'--debugger'], uimod.ui())
     >>> _earlyreqoptbool(req, b'debugger', [b'--debugger'])
     True
 
-    >>> req = request([b'x', b'--', b'--debugger'])
+    >>> req = request([b'x', b'--', b'--debugger'], uimod.ui())
     >>> _earlyreqoptbool(req, b'debugger', [b'--debugger'])
     """
+    if req.ui.plain('strictflags'):
+        return req.earlyoptions[name]
     try:
         argcount = req.args.index("--")
     except ValueError:
--- a/mercurial/fancyopts.py	Thu Nov 16 03:52:42 2017 +0100
+++ b/mercurial/fancyopts.py	Fri Dec 01 15:21:05 2017 -0600
@@ -7,6 +7,8 @@
 
 from __future__ import absolute_import
 
+import functools
+
 from .i18n import _
 from . import (
     error,
@@ -24,6 +26,179 @@
     'version',
 }
 
+def _earlyoptarg(arg, shortlist, namelist):
+    """Check if the given arg is a valid unabbreviated option
+
+    Returns (flag_str, has_embedded_value?, embedded_value, takes_value?)
+
+    >>> def opt(arg):
+    ...     return _earlyoptarg(arg, b'R:q', [b'cwd=', b'debugger'])
+
+    long form:
+
+    >>> opt(b'--cwd')
+    ('--cwd', False, '', True)
+    >>> opt(b'--cwd=')
+    ('--cwd', True, '', True)
+    >>> opt(b'--cwd=foo')
+    ('--cwd', True, 'foo', True)
+    >>> opt(b'--debugger')
+    ('--debugger', False, '', False)
+    >>> opt(b'--debugger=')  # invalid but parsable
+    ('--debugger', True, '', False)
+
+    short form:
+
+    >>> opt(b'-R')
+    ('-R', False, '', True)
+    >>> opt(b'-Rfoo')
+    ('-R', True, 'foo', True)
+    >>> opt(b'-q')
+    ('-q', False, '', False)
+    >>> opt(b'-qfoo')  # invalid but parsable
+    ('-q', True, 'foo', False)
+
+    unknown or invalid:
+
+    >>> opt(b'--unknown')
+    ('', False, '', False)
+    >>> opt(b'-u')
+    ('', False, '', False)
+    >>> opt(b'-ufoo')
+    ('', False, '', False)
+    >>> opt(b'--')
+    ('', False, '', False)
+    >>> opt(b'-')
+    ('', False, '', False)
+    >>> opt(b'-:')
+    ('', False, '', False)
+    >>> opt(b'-:foo')
+    ('', False, '', False)
+    """
+    if arg.startswith('--'):
+        flag, eq, val = arg.partition('=')
+        if flag[2:] in namelist:
+            return flag, bool(eq), val, False
+        if flag[2:] + '=' in namelist:
+            return flag, bool(eq), val, True
+    elif arg.startswith('-') and arg != '-' and not arg.startswith('-:'):
+        flag, val = arg[:2], arg[2:]
+        i = shortlist.find(flag[1:])
+        if i >= 0:
+            return flag, bool(val), val, shortlist.startswith(':', i + 1)
+    return '', False, '', False
+
+def earlygetopt(args, shortlist, namelist, gnu=False, keepsep=False):
+    """Parse options like getopt, but ignores unknown options and abbreviated
+    forms
+
+    If gnu=False, this stops processing options as soon as a non/unknown-option
+    argument is encountered. Otherwise, option and non-option arguments may be
+    intermixed, and unknown-option arguments are taken as non-option.
+
+    If keepsep=True, '--' won't be removed from the list of arguments left.
+    This is useful for stripping early options from a full command arguments.
+
+    >>> def get(args, gnu=False, keepsep=False):
+    ...     return earlygetopt(args, b'R:q', [b'cwd=', b'debugger'],
+    ...                        gnu=gnu, keepsep=keepsep)
+
+    default parsing rules for early options:
+
+    >>> get([b'x', b'--cwd', b'foo', b'-Rbar', b'-q', b'y'], gnu=True)
+    ([('--cwd', 'foo'), ('-R', 'bar'), ('-q', '')], ['x', 'y'])
+    >>> get([b'x', b'--cwd=foo', b'y', b'-R', b'bar', b'--debugger'], gnu=True)
+    ([('--cwd', 'foo'), ('-R', 'bar'), ('--debugger', '')], ['x', 'y'])
+    >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=True)
+    ([('--cwd', 'foo')], ['--unknown', '--debugger'])
+
+    restricted parsing rules (early options must come first):
+
+    >>> get([b'--cwd', b'foo', b'-Rbar', b'x', b'-q', b'y'], gnu=False)
+    ([('--cwd', 'foo'), ('-R', 'bar')], ['x', '-q', 'y'])
+    >>> get([b'--cwd=foo', b'x', b'y', b'-R', b'bar', b'--debugger'], gnu=False)
+    ([('--cwd', 'foo')], ['x', 'y', '-R', 'bar', '--debugger'])
+    >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=False)
+    ([], ['--unknown', '--cwd=foo', '--debugger'])
+
+    stripping early options (without loosing '--'):
+
+    >>> get([b'x', b'-Rbar', b'--', '--debugger'], gnu=True, keepsep=True)[1]
+    ['x', '--', '--debugger']
+
+    last argument:
+
+    >>> get([b'--cwd'])
+    ([], ['--cwd'])
+    >>> get([b'--cwd=foo'])
+    ([('--cwd', 'foo')], [])
+    >>> get([b'-R'])
+    ([], ['-R'])
+    >>> get([b'-Rbar'])
+    ([('-R', 'bar')], [])
+    >>> get([b'-q'])
+    ([('-q', '')], [])
+    >>> get([b'-q', b'--'])
+    ([('-q', '')], [])
+
+    value passed to bool options:
+
+    >>> get([b'--debugger=foo', b'x'])
+    ([], ['--debugger=foo', 'x'])
+    >>> get([b'-qfoo', b'x'])
+    ([], ['-qfoo', 'x'])
+
+    short option isn't separated with '=':
+
+    >>> get([b'-R=bar'])
+    ([('-R', '=bar')], [])
+
+    ':' may be in shortlist, but shouldn't be taken as an option letter:
+
+    >>> get([b'-:', b'y'])
+    ([], ['-:', 'y'])
+
+    '-' is a valid non-option argument:
+
+    >>> get([b'-', b'y'])
+    ([], ['-', 'y'])
+    """
+    # ignoring everything just after '--' isn't correct as '--' may be an
+    # option value (e.g. ['-R', '--']), but we do that consistently.
+    try:
+        argcount = args.index('--')
+    except ValueError:
+        argcount = len(args)
+
+    parsedopts = []
+    parsedargs = []
+    pos = 0
+    while pos < argcount:
+        arg = args[pos]
+        flag, hasval, val, takeval = _earlyoptarg(arg, shortlist, namelist)
+        if not hasval and takeval and pos + 1 >= argcount:
+            # missing last argument
+            break
+        if not flag or hasval and not takeval:
+            # non-option argument or -b/--bool=INVALID_VALUE
+            if gnu:
+                parsedargs.append(arg)
+                pos += 1
+            else:
+                break
+        elif hasval == takeval:
+            # -b/--bool or -s/--str=VALUE
+            parsedopts.append((flag, val))
+            pos += 1
+        else:
+            # -s/--str VALUE
+            parsedopts.append((flag, args[pos + 1]))
+            pos += 2
+
+    parsedargs.extend(args[pos:argcount])
+    parsedargs.extend(args[argcount + (not keepsep):])
+    return parsedopts, parsedargs
+
 def gnugetopt(args, options, longoptions):
     """Parse options mostly like getopt.gnu_getopt.
 
@@ -51,7 +226,7 @@
     return opts, args
 
 
-def fancyopts(args, options, state, gnu=False):
+def fancyopts(args, options, state, gnu=False, early=False):
     """
     read args, parse options, and store options in state
 
@@ -124,7 +299,9 @@
             namelist.append(oname)
 
     # parse arguments
-    if gnu:
+    if early:
+        parse = functools.partial(earlygetopt, gnu=gnu)
+    elif gnu:
         parse = gnugetopt
     else:
         parse = pycompat.getoptb
--- a/mercurial/help/environment.txt	Thu Nov 16 03:52:42 2017 +0100
+++ b/mercurial/help/environment.txt	Fri Dec 01 15:21:05 2017 -0600
@@ -56,9 +56,17 @@
     localization. This can be useful when scripting against Mercurial
     in the face of existing user configuration.
 
+    In addition to the features disabled by ``HGPLAIN=``, the following
+    values can be specified to adjust behavior:
+
+    ``+strictflags``
+        Restrict parsing of command line flags.
+
     Equivalent options set via command line flags or environment
     variables are not overridden.
 
+    See :hg:`help scripting` for details.
+
 HGPLAINEXCEPT
     This is a comma-separated list of features to preserve when
     HGPLAIN is enabled. Currently the following values are supported:
--- a/mercurial/help/scripting.txt	Thu Nov 16 03:52:42 2017 +0100
+++ b/mercurial/help/scripting.txt	Fri Dec 01 15:21:05 2017 -0600
@@ -74,6 +74,32 @@
     like the username and extensions that may be required to interface
     with a repository.
 
+Command-line Flags
+==================
+
+Mercurial's default command-line parser is designed for humans, and is not
+robust against malicious input. For instance, you can start a debugger by
+passing ``--debugger`` as an option value::
+
+    $ REV=--debugger sh -c 'hg log -r "$REV"'
+
+This happens because several command-line flags need to be scanned without
+using a concrete command table, which may be modified while loading repository
+settings and extensions.
+
+Since Mercurial 4.4.2, the parsing of such flags may be restricted by setting
+``HGPLAIN=+strictflags``. When this feature is enabled, all early options
+(e.g. ``-R/--repository``, ``--cwd``, ``--config``) must be specified first
+amongst the other global options, and cannot be injected to an arbitrary
+location::
+
+    $ HGPLAIN=+strictflags hg -R "$REPO" log -r "$REV"
+
+In earlier Mercurial versions where ``+strictflags`` isn't available, you
+can mitigate the issue by concatenating an option value with its flag::
+
+    $ hg log -r"$REV" --keyword="$KEYWORD"
+
 Consuming Command Output
 ========================
 
--- a/mercurial/help/subrepos.txt	Thu Nov 16 03:52:42 2017 +0100
+++ b/mercurial/help/subrepos.txt	Fri Dec 01 15:21:05 2017 -0600
@@ -90,7 +90,7 @@
 :archive: archive does not recurse in subrepositories unless
     -S/--subrepos is specified.
 
-:cat: cat currently only handles exact file matches in subrepos.
+:cat: Git subrepositories only support exact file matches.
     Subversion subrepositories are currently ignored.
 
 :commit: commit creates a consistent snapshot of the state of the
--- a/mercurial/merge.py	Thu Nov 16 03:52:42 2017 +0100
+++ b/mercurial/merge.py	Fri Dec 01 15:21:05 2017 -0600
@@ -653,7 +653,7 @@
         and repo.dirstate.normalize(f) not in repo.dirstate
         and mctx[f2].cmp(wctx[f]))
 
-def _checkunknowndirs(repo, f):
+class _unknowndirschecker(object):
     """
     Look for any unknown files or directories that may have a path conflict
     with a file.  If any path prefix of the file exists as a file or link,
@@ -663,23 +663,42 @@
     Returns the shortest path at which a conflict occurs, or None if there is
     no conflict.
     """
+    def __init__(self):
+        # A set of paths known to be good.  This prevents repeated checking of
+        # dirs.  It will be updated with any new dirs that are checked and found
+        # to be safe.
+        self._unknowndircache = set()
 
-    # Check for path prefixes that exist as unknown files.
-    for p in reversed(list(util.finddirs(f))):
-        if (repo.wvfs.audit.check(p)
-                and repo.wvfs.isfileorlink(p)
-                and repo.dirstate.normalize(p) not in repo.dirstate):
-            return p
+        # A set of paths that are known to be absent.  This prevents repeated
+        # checking of subdirectories that are known not to exist. It will be
+        # updated with any new dirs that are checked and found to be absent.
+        self._missingdircache = set()
 
-    # Check if the file conflicts with a directory containing unknown files.
-    if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
-        # Does the directory contain any files that are not in the dirstate?
-        for p, dirs, files in repo.wvfs.walk(f):
-            for fn in files:
-                relf = repo.dirstate.normalize(repo.wvfs.reljoin(p, fn))
-                if relf not in repo.dirstate:
-                    return f
-    return None
+    def __call__(self, repo, f):
+        # Check for path prefixes that exist as unknown files.
+        for p in reversed(list(util.finddirs(f))):
+            if p in self._missingdircache:
+                return
+            if p in self._unknowndircache:
+                continue
+            if repo.wvfs.audit.check(p):
+                if (repo.wvfs.isfileorlink(p)
+                        and repo.dirstate.normalize(p) not in repo.dirstate):
+                    return p
+                if not repo.wvfs.lexists(p):
+                    self._missingdircache.add(p)
+                    return
+                self._unknowndircache.add(p)
+
+        # Check if the file conflicts with a directory containing unknown files.
+        if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
+            # Does the directory contain any files that are not in the dirstate?
+            for p, dirs, files in repo.wvfs.walk(f):
+                for fn in files:
+                    relf = repo.dirstate.normalize(repo.wvfs.reljoin(p, fn))
+                    if relf not in repo.dirstate:
+                        return f
+        return None
 
 def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
     """
@@ -701,12 +720,13 @@
             elif config == 'warn':
                 warnconflicts.update(conflicts)
 
+        checkunknowndirs = _unknowndirschecker()
         for f, (m, args, msg) in actions.iteritems():
             if m in ('c', 'dc'):
                 if _checkunknownfile(repo, wctx, mctx, f):
                     fileconflicts.add(f)
                 elif pathconfig and f not in wctx:
-                    path = _checkunknowndirs(repo, f)
+                    path = checkunknowndirs(repo, f)
                     if path is not None:
                         pathconflicts.add(path)
             elif m == 'dg':
@@ -895,34 +915,21 @@
     # can't be updated to cleanly.
     invalidconflicts = set()
 
+    # The set of directories that contain files that are being created.
+    createdfiledirs = set()
+
     # The set of files deleted by all the actions.
     deletedfiles = set()
 
     for f, (m, args, msg) in actions.items():
         if m in ('c', 'dc', 'm', 'cm'):
             # This action may create a new local file.
+            createdfiledirs.update(util.finddirs(f))
             if mf.hasdir(f):
                 # The file aliases a local directory.  This might be ok if all
                 # the files in the local directory are being deleted.  This
                 # will be checked once we know what all the deleted files are.
                 remoteconflicts.add(f)
-            for p in util.finddirs(f):
-                if p in mf:
-                    if p in mctx:
-                        # The file is in a directory which aliases both a local
-                        # and a remote file.  This is an internal inconsistency
-                        # within the remote manifest.
-                        invalidconflicts.add(p)
-                    else:
-                        # The file is in a directory which aliases a local file.
-                        # We will need to rename the local file.
-                        localconflicts.add(p)
-                if p in actions and actions[p][0] in ('c', 'dc', 'm', 'cm'):
-                    # The file is in a directory which aliases a remote file.
-                    # This is an internal inconsistency within the remote
-                    # manifest.
-                    invalidconflicts.add(p)
-
         # Track the names of all deleted files.
         if m == 'r':
             deletedfiles.add(f)
@@ -934,6 +941,24 @@
             f2, flags = args
             deletedfiles.add(f2)
 
+    # Check all directories that contain created files for path conflicts.
+    for p in createdfiledirs:
+        if p in mf:
+            if p in mctx:
+                # A file is in a directory which aliases both a local
+                # and a remote file.  This is an internal inconsistency
+                # within the remote manifest.
+                invalidconflicts.add(p)
+            else:
+                # A file is in a directory which aliases a local file.
+                # We will need to rename the local file.
+                localconflicts.add(p)
+        if p in actions and actions[p][0] in ('c', 'dc', 'm', 'cm'):
+            # The file is in a directory which aliases a remote file.
+            # This is an internal inconsistency within the remote
+            # manifest.
+            invalidconflicts.add(p)
+
     # Rename all local conflicting files that have not been deleted.
     for p in localconflicts:
         if p not in deletedfiles:
--- a/mercurial/ui.py	Thu Nov 16 03:52:42 2017 +0100
+++ b/mercurial/ui.py	Fri Dec 01 15:21:05 2017 -0600
@@ -766,6 +766,7 @@
 
         The return value can either be
         - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
+        - False if feature is disabled by default and not included in HGPLAIN
         - True otherwise
         '''
         if ('HGPLAIN' not in encoding.environ and
@@ -773,6 +774,9 @@
             return False
         exceptions = encoding.environ.get('HGPLAINEXCEPT',
                 '').strip().split(',')
+        # TODO: add support for HGPLAIN=+feature,-feature syntax
+        if '+strictflags' not in encoding.environ.get('HGPLAIN', '').split(','):
+            exceptions.append('strictflags')
         if feature and exceptions:
             return feature not in exceptions
         return True
--- a/tests/test-amend-subrepo.t	Thu Nov 16 03:52:42 2017 +0100
+++ b/tests/test-amend-subrepo.t	Fri Dec 01 15:21:05 2017 -0600
@@ -58,7 +58,7 @@
 
   $ echo a >> s/a
   $ hg add -R s
-  adding s/a
+  adding s/a (glob)
   $ hg amend
   abort: uncommitted changes in subrepository "s"
   (use --subrepos for recursive commit)
--- a/tests/test-audit-subrepo.t	Thu Nov 16 03:52:42 2017 +0100
+++ b/tests/test-audit-subrepo.t	Fri Dec 01 15:21:05 2017 -0600
@@ -9,7 +9,7 @@
   $ hg init sub/.hg
   $ echo 'sub/.hg = sub/.hg' >> .hgsub
   $ hg ci -qAm 'add subrepo "sub/.hg"'
-  abort: path 'sub/.hg' is inside nested repo 'sub'
+  abort: path 'sub/.hg' is inside nested repo 'sub' (glob)
   [255]
 
 prepare tampered repo (including the commit above):
@@ -33,7 +33,7 @@
 on clone (and update):
 
   $ hg clone -q hgname hgname2
-  abort: path 'sub/.hg' is inside nested repo 'sub'
+  abort: path 'sub/.hg' is inside nested repo 'sub' (glob)
   [255]
 
 Test direct symlink traversal
--- a/tests/test-commandserver.t	Thu Nov 16 03:52:42 2017 +0100
+++ b/tests/test-commandserver.t	Fri Dec 01 15:21:05 2017 -0600
@@ -137,6 +137,20 @@
   summary:     1
   
 
+check strict parsing of early options:
+
+  >>> import os
+  >>> from hgclient import check, readchannel, runcommand
+  >>> os.environ['HGPLAIN'] = '+strictflags'
+  >>> @check
+  ... def cwd(server):
+  ...     readchannel(server)
+  ...     runcommand(server, ['log', '-b', '--config=alias.log=!echo pwned',
+  ...                         'default'])
+  *** runcommand log -b --config=alias.log=!echo pwned default
+  abort: unknown revision '--config=alias.log=!echo pwned'!
+   [255]
+
 check that "histedit --commands=-" can read rules from the input channel:
 
   >>> import cStringIO
--- a/tests/test-dispatch.t	Thu Nov 16 03:52:42 2017 +0100
+++ b/tests/test-dispatch.t	Fri Dec 01 15:21:05 2017 -0600
@@ -113,6 +113,51 @@
   $ hg log -b '--config=alias.log=!echo howdy'
   howdy
 
+Early options must come first if HGPLAIN=+strictflags is specified:
+(BUG: chg cherry-picks early options to pass them as a server command)
+
+#if no-chg
+  $ HGPLAIN=+strictflags hg log -b --config='hooks.pre-log=false' default
+  abort: unknown revision '--config=hooks.pre-log=false'!
+  [255]
+  $ HGPLAIN=+strictflags hg log -b -R. default
+  abort: unknown revision '-R.'!
+  [255]
+  $ HGPLAIN=+strictflags hg log -b --cwd=. default
+  abort: unknown revision '--cwd=.'!
+  [255]
+#endif
+  $ HGPLAIN=+strictflags hg log -b --debugger default
+  abort: unknown revision '--debugger'!
+  [255]
+  $ HGPLAIN=+strictflags hg log -b --config='alias.log=!echo pwned' default
+  abort: unknown revision '--config=alias.log=!echo pwned'!
+  [255]
+
+  $ HGPLAIN=+strictflags hg log --config='hooks.pre-log=false' -b default
+  abort: option --config may not be abbreviated!
+  [255]
+  $ HGPLAIN=+strictflags hg log -q --cwd=.. -b default
+  abort: option --cwd may not be abbreviated!
+  [255]
+  $ HGPLAIN=+strictflags hg log -q -R . -b default
+  abort: option -R has to be separated from other options (e.g. not -qR) and --repository may only be abbreviated as --repo!
+  [255]
+
+  $ HGPLAIN=+strictflags hg --config='hooks.pre-log=false' log -b default
+  abort: pre-log hook exited with status 1
+  [255]
+  $ HGPLAIN=+strictflags hg --cwd .. -q -Ra log -b default
+  0:cb9a9f314b8b
+
+For compatibility reasons, HGPLAIN=+strictflags is not enabled by plain HGPLAIN:
+
+  $ HGPLAIN= hg log --config='hooks.pre-log=false' -b default
+  abort: pre-log hook exited with status 1
+  [255]
+  $ HGPLAINEXCEPT= hg log --cwd .. -q -Ra -b default
+  0:cb9a9f314b8b
+
 [defaults]
 
   $ hg cat a
--- a/tests/test-doctest.py	Thu Nov 16 03:52:42 2017 +0100
+++ b/tests/test-doctest.py	Fri Dec 01 15:21:05 2017 -0600
@@ -48,6 +48,7 @@
 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
 testmod('mercurial.dispatch')
 testmod('mercurial.encoding')
+testmod('mercurial.fancyopts')
 testmod('mercurial.formatter')
 testmod('mercurial.hg')
 testmod('mercurial.hgweb.hgwebdir_mod')
--- a/tests/test-import.t	Thu Nov 16 03:52:42 2017 +0100
+++ b/tests/test-import.t	Fri Dec 01 15:21:05 2017 -0600
@@ -972,6 +972,7 @@
   adding b
   recording removal of a as rename to b (88% similar)
   applied to working directory
+  $ echo 'mod b' > b
   $ hg st -C
   A b
     a
@@ -979,6 +980,8 @@
   $ hg revert -a
   undeleting a
   forgetting b
+  $ cat b
+  mod b
   $ rm b
   $ hg import --no-commit -v -s 100 ../rename.diff -p2
   applying ../rename.diff
--- a/tests/test-largefiles-misc.t	Thu Nov 16 03:52:42 2017 +0100
+++ b/tests/test-largefiles-misc.t	Fri Dec 01 15:21:05 2017 -0600
@@ -390,8 +390,12 @@
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg status -S
 
+Forget doesn't change the content of the file
+  $ echo 'pre-forget content' > subrepo/large.txt
   $ hg forget -v subrepo/large.txt
   removing subrepo/large.txt (glob)
+  $ cat subrepo/large.txt
+  pre-forget content
 
 Test reverting a forgotten file
   $ hg revert -R subrepo subrepo/large.txt
@@ -1060,7 +1064,9 @@
   > largefiles=
   > EOF
   $ echo large > subrepo-root/large
-  $ hg -R subrepo-root add --large subrepo-root/large
+  $ mkdir -p subrepo-root/dir/subdir
+  $ echo large2 > subrepo-root/dir/subdir/large.bin
+  $ hg -R subrepo-root add --large subrepo-root/large subrepo-root/dir/subdir/large.bin
   $ hg clone -q no-largefiles subrepo-root/no-largefiles
   $ cat > subrepo-root/.hgsub <<EOF
   > no-largefiles = no-largefiles
@@ -1069,6 +1075,7 @@
   $ hg -R subrepo-root commit -m '#0'
   Invoking status precommit hook
   A .hgsub
+  A dir/subdir/large.bin
   A large
   ? .hgsubstate
   $ echo dirty >> subrepo-root/large
@@ -1085,8 +1092,155 @@
   reverting subrepo no-largefiles
   reverting subrepo-root/no-largefiles/normal1 (glob)
 
-  $ cd ..
+Move (and then undo) a directory move with only largefiles.
+
+  $ listtree() {
+  >   find $@ \( -type d -printf "%p/\n" -o -type f -printf "%p\n" \) \
+  >           -a -name .hg -prune | sort
+  > }
+
+  $ cd subrepo-root
+  $ listtree .hglf dir* large*
+  .hglf/
+  .hglf/dir/
+  .hglf/dir/subdir/
+  .hglf/dir/subdir/large.bin
+  .hglf/large
+  dir/
+  dir/subdir/
+  dir/subdir/large.bin
+  large
+  large.orig
+
+  $ hg mv dir/subdir dir/subdir2
+  moving .hglf/dir/subdir/large.bin to .hglf/dir/subdir2/large.bin (glob)
+
+  $ listtree .hglf dir* large*
+  .hglf/
+  .hglf/dir/
+  .hglf/dir/subdir2/
+  .hglf/dir/subdir2/large.bin
+  .hglf/large
+  dir/
+  dir/subdir2/
+  dir/subdir2/large.bin
+  large
+  large.orig
+  $ hg status -C
+  A dir/subdir2/large.bin
+    dir/subdir/large.bin
+  R dir/subdir/large.bin
+  ? large.orig
+
+  $ echo 'modified' > dir/subdir2/large.bin
+  $ hg status -C
+  A dir/subdir2/large.bin
+    dir/subdir/large.bin
+  R dir/subdir/large.bin
+  ? large.orig
+
+  $ hg revert --all
+  undeleting .hglf/dir/subdir/large.bin (glob)
+  forgetting .hglf/dir/subdir2/large.bin (glob)
+  reverting subrepo no-largefiles
+
+  $ hg status -C
+  ? dir/subdir2/large.bin
+  ? large.orig
+
+The content of the forgotten file shouldn't be clobbered
+
+  $ cat dir/subdir2/large.bin
+  modified
+
+The standin for subdir2 should be deleted, not just dropped
 
+  $ listtree .hglf dir* large*
+  .hglf/
+  .hglf/dir/
+  .hglf/dir/subdir/
+  .hglf/dir/subdir/large.bin
+  .hglf/large
+  dir/
+  dir/subdir/
+  dir/subdir/large.bin
+  dir/subdir2/
+  dir/subdir2/large.bin
+  large
+  large.orig
+
+  $ rm -r dir/subdir2
+
+'subdir' should not be in the destination.  It would be if the subdir2 directory
+existed under .hglf/.
+  $ hg mv dir/subdir dir/subdir2
+  moving .hglf/dir/subdir/large.bin to .hglf/dir/subdir2/large.bin (glob)
+
+  $ hg status -C
+  A dir/subdir2/large.bin
+    dir/subdir/large.bin
+  R dir/subdir/large.bin
+  ? large.orig
+
+  $ listtree .hglf dir* large*
+  .hglf/
+  .hglf/dir/
+  .hglf/dir/subdir2/
+  .hglf/dir/subdir2/large.bin
+  .hglf/large
+  dir/
+  dir/subdir2/
+  dir/subdir2/large.bin
+  large
+  large.orig
+
+Start from scratch, and rename something other than the final path component.
+
+  $ hg up -qC .
+  $ hg --config extensions.purge= purge
+
+  $ hg mv dir/subdir dir2/subdir
+  moving .hglf/dir/subdir/large.bin to .hglf/dir2/subdir/large.bin (glob)
+
+  $ hg status -C
+  A dir2/subdir/large.bin
+    dir/subdir/large.bin
+  R dir/subdir/large.bin
+
+  $ listtree .hglf dir* large*
+  .hglf/
+  .hglf/dir2/
+  .hglf/dir2/subdir/
+  .hglf/dir2/subdir/large.bin
+  .hglf/large
+  dir2/
+  dir2/subdir/
+  dir2/subdir/large.bin
+  large
+
+  $ hg revert --all
+  undeleting .hglf/dir/subdir/large.bin (glob)
+  forgetting .hglf/dir2/subdir/large.bin (glob)
+  reverting subrepo no-largefiles
+
+  $ hg status -C
+  ? dir2/subdir/large.bin
+
+  $ listtree .hglf dir* large*
+  .hglf/
+  .hglf/dir/
+  .hglf/dir/subdir/
+  .hglf/dir/subdir/large.bin
+  .hglf/large
+  dir/
+  dir/subdir/
+  dir/subdir/large.bin
+  dir2/
+  dir2/subdir/
+  dir2/subdir/large.bin
+  large
+
+  $ cd ../..
 
 Test "pull --rebase" when rebase is enabled before largefiles (issue3861)
 =========================================================================
--- a/tests/test-subrepo.t	Thu Nov 16 03:52:42 2017 +0100
+++ b/tests/test-subrepo.t	Fri Dec 01 15:21:05 2017 -0600
@@ -1071,6 +1071,18 @@
     "path": "sub/repo/foo"
    }
   ]
+
+ non-exact match:
+
+  $ hg cat -T '{path}\n' 'glob:**'
+  .hgsub
+  .hgsubstate
+  sub/repo/foo (glob)
+  $ hg cat -T '{path}\n' 're:^sub'
+  sub/repo/foo (glob)
+
+ missing subrepos in working directory:
+
   $ mkdir -p tmp/sub/repo
   $ hg cat -r 0 --output tmp/%p_p sub/repo/foo
   $ cat tmp/sub/repo/foo_p