merge with stable
authorGregory Szorc <gregory.szorc@gmail.com>
Tue, 06 Mar 2018 14:32:14 -0800
changeset 36760 7bf80d9d9543
parent 36747 4c71a26a4009 (current diff)
parent 36759 9639c433be54 (diff)
child 36761 09f320067591
merge with stable There were a handful of merge conflicts in the wire protocol code due to significant refactoring in default. When resolving the conflicts, I tried to produce the minimal number of changes to make the incoming security patches work with the new code. I will send some follow-up commits to get the security patches better integrated into default.
hgext/largefiles/uisetup.py
mercurial/changegroup.py
mercurial/hgweb/hgweb_mod.py
mercurial/revlog.py
mercurial/wireproto.py
mercurial/wireprotoserver.py
tests/test-annotate.t
tests/test-http-bundle1.t
tests/test-largefiles-wireproto.t
tests/test-lfs.t
--- a/.hgsigs	Sun Mar 04 21:16:36 2018 -0500
+++ b/.hgsigs	Tue Mar 06 14:32:14 2018 -0800
@@ -158,3 +158,5 @@
 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
+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
--- a/.hgtags	Sun Mar 04 21:16:36 2018 -0500
+++ b/.hgtags	Tue Mar 06 14:32:14 2018 -0800
@@ -171,3 +171,5 @@
 a92b9f8e11ba330614cdfd6af0e03b15c1ff3797 4.4.2
 27b6df1b5adbdf647cf5c6675b40575e1b197c60 4.5-rc
 d334afc585e29577f271c5eda03378736a16ca6b 4.5
+369aadf7a3264b03c8b09efce715bc41e6ab4a9b 4.5.1
+8bba684efde7f45add05f737952093bb2aa07155 4.5.2
--- a/hgext/largefiles/uisetup.py	Sun Mar 04 21:16:36 2018 -0500
+++ b/hgext/largefiles/uisetup.py	Tue Mar 06 14:32:14 2018 -0800
@@ -12,7 +12,6 @@
 from mercurial.i18n import _
 
 from mercurial.hgweb import (
-    hgweb_mod,
     webcommands,
 )
 
@@ -175,9 +174,10 @@
 
     # make putlfile behave the same as push and {get,stat}lfile behave
     # the same as pull w.r.t. permissions checks
-    hgweb_mod.perms['putlfile'] = 'push'
-    hgweb_mod.perms['getlfile'] = 'pull'
-    hgweb_mod.perms['statlfile'] = 'pull'
+    wireproto.permissions['putlfile'] = 'push'
+    wireproto.permissions['getlfile'] = 'pull'
+    wireproto.permissions['statlfile'] = 'pull'
+    wireproto.permissions['lheads'] = 'pull'
 
     extensions.wrapfunction(webcommands, 'decodepath', overrides.decodepath)
 
--- a/mercurial/changegroup.py	Sun Mar 04 21:16:36 2018 -0500
+++ b/mercurial/changegroup.py	Tue Mar 06 14:32:14 2018 -0800
@@ -774,6 +774,8 @@
         progress(msgbundling, None)
 
     def deltaparent(self, revlog, rev, p1, p2, prev):
+        if not revlog.candelta(prev, rev):
+            raise error.ProgrammingError('cg1 should not be used in this case')
         return prev
 
     def revchunk(self, revlog, rev, prev, linknode):
@@ -833,16 +835,19 @@
             # expensive. The revlog caches should have prev cached, meaning
             # less CPU for changegroup generation. There is likely room to add
             # a flag and/or config option to control this behavior.
-            return prev
+            base = prev
         elif dp == nullrev:
             # revlog is configured to use full snapshot for a reason,
             # stick to full snapshot.
-            return nullrev
+            base = nullrev
         elif dp not in (p1, p2, prev):
             # Pick prev when we can't be sure remote has the base revision.
             return prev
         else:
-            return dp
+            base = dp
+        if base != nullrev and not revlog.candelta(base, rev):
+            base = nullrev
+        return base
 
     def builddeltaheader(self, node, p1n, p2n, basenode, linknode, flags):
         # Do nothing with flags, it is implicitly 0 in cg1 and cg2
--- a/mercurial/hgweb/hgweb_mod.py	Sun Mar 04 21:16:36 2018 -0500
+++ b/mercurial/hgweb/hgweb_mod.py	Tue Mar 06 14:32:14 2018 -0800
@@ -37,6 +37,7 @@
     templater,
     ui as uimod,
     util,
+    wireproto,
     wireprotoserver,
 )
 
@@ -46,15 +47,8 @@
     wsgicgi,
 )
 
-perms = {
-    'changegroup': 'pull',
-    'changegroupsubset': 'pull',
-    'getbundle': 'pull',
-    'stream_out': 'pull',
-    'listkeys': 'pull',
-    'unbundle': 'push',
-    'pushkey': 'push',
-}
+# Aliased for API compatibility.
+perms = wireproto.permissions
 
 archivespecs = util.sortdict((
     ('zip', ('application/zip', 'zip', '.zip', None)),
@@ -367,13 +361,21 @@
             try:
                 if query:
                     raise ErrorResponse(HTTP_NOT_FOUND)
-                if cmd in perms:
-                    self.check_perm(rctx, req, perms[cmd])
+
+                # TODO fold this into parsehttprequest
+                req.checkperm = lambda op: self.check_perm(rctx, req, op)
+                protohandler['proto'].checkperm = req.checkperm
+
+                # Assume commands with no defined permissions are writes /
+                # for pushes. This is the safest from a security perspective
+                # because it doesn't allow commands with undefined semantics
+                # from bypassing permissions checks.
+                req.checkperm(perms.get(cmd, 'push'))
+
+                return protohandler['dispatch']()
             except ErrorResponse as inst:
                 return protohandler['handleerror'](inst)
 
-            return protohandler['dispatch']()
-
         # translate user-visible url structure to internal structure
 
         args = query.split('/', 2)
--- a/mercurial/revlog.py	Sun Mar 04 21:16:36 2018 -0500
+++ b/mercurial/revlog.py	Tue Mar 06 14:32:14 2018 -0800
@@ -77,6 +77,8 @@
     REVIDX_EXTSTORED,
 ]
 REVIDX_KNOWN_FLAGS = util.bitsfrom(REVIDX_FLAGS_ORDER)
+# bitmark for flags that could cause rawdata content change
+REVIDX_RAWTEXT_CHANGING_FLAGS = REVIDX_ISCENSORED | REVIDX_EXTSTORED
 
 # max size of revlog with inline data
 _maxinline = 131072
@@ -96,7 +98,8 @@
     """Register a flag processor on a revision data flag.
 
     Invariant:
-    - Flags need to be defined in REVIDX_KNOWN_FLAGS and REVIDX_FLAGS_ORDER.
+    - Flags need to be defined in REVIDX_KNOWN_FLAGS and REVIDX_FLAGS_ORDER,
+      and REVIDX_RAWTEXT_CHANGING_FLAGS if they can alter rawtext.
     - Only one flag processor can be registered on a specific flag.
     - flagprocessors must be 3-tuples of functions (read, write, raw) with the
       following signatures:
@@ -333,7 +336,9 @@
                                                    len(delta) - hlen):
             btext[0] = delta[hlen:]
         else:
-            basetext = revlog.revision(baserev, _df=fh, raw=True)
+            # deltabase is rawtext before changed by flag processors, which is
+            # equivalent to non-raw text
+            basetext = revlog.revision(baserev, _df=fh, raw=False)
             btext[0] = mdiff.patch(basetext, delta)
 
         try:
@@ -404,6 +409,9 @@
         for candidaterevs in self._getcandidaterevs(p1, p2, cachedelta):
             nominateddeltas = []
             for candidaterev in candidaterevs:
+                # no delta for rawtext-changing revs (see "candelta" for why)
+                if revlog.flags(candidaterev) & REVIDX_RAWTEXT_CHANGING_FLAGS:
+                    continue
                 candidatedelta = self._builddeltainfo(revinfo, candidaterev, fh)
                 if revlog._isgooddeltainfo(candidatedelta, revinfo.textlen):
                     nominateddeltas.append(candidatedelta)
@@ -738,6 +746,18 @@
         except KeyError:
             return False
 
+    def candelta(self, baserev, rev):
+        """whether two revisions (baserev, rev) can be delta-ed or not"""
+        # Disable delta if either rev requires a content-changing flag
+        # processor (ex. LFS). This is because such flag processor can alter
+        # the rawtext content that the delta will be based on, and two clients
+        # could have a same revlog node with different flags (i.e. different
+        # rawtext contents) and the delta could be incompatible.
+        if ((self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS)
+            or (self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS)):
+            return False
+        return True
+
     def clearcaches(self):
         self._cache = None
         self._chainbasecache.clear()
@@ -2078,7 +2098,10 @@
         # full versions are inserted when the needed deltas
         # become comparable to the uncompressed text
         if rawtext is None:
-            textlen = mdiff.patchedsize(self.rawsize(cachedelta[0]),
+            # need rawtext size, before changed by flag processors, which is
+            # the non-raw size. use revlog explicitly to avoid filelog's extra
+            # logic that might remove metadata size.
+            textlen = mdiff.patchedsize(revlog.size(self, cachedelta[0]),
                                         cachedelta[1])
         else:
             textlen = len(rawtext)
@@ -2087,7 +2110,14 @@
             deltacomputer = _deltacomputer(self)
 
         revinfo = _revisioninfo(node, p1, p2, btext, textlen, cachedelta, flags)
-        deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
+
+        # no delta for flag processor revision (see "candelta" for why)
+        # not calling candelta since only one revision needs test, also to
+        # avoid overhead fetching flags again.
+        if flags & REVIDX_RAWTEXT_CHANGING_FLAGS:
+            deltainfo = None
+        else:
+            deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
 
         if deltainfo is not None:
             base = deltainfo.base
--- a/mercurial/wireproto.py	Sun Mar 04 21:16:36 2018 -0500
+++ b/mercurial/wireproto.py	Tue Mar 06 14:32:14 2018 -0800
@@ -672,6 +672,11 @@
 
 commands = commanddict()
 
+# Maps wire protocol name to operation type. This is used for permissions
+# checking. All defined @wireiprotocommand should have an entry in this
+# dict.
+permissions = {}
+
 def wireprotocommand(name, args='', transportpolicy=POLICY_ALL):
     """Decorator to declare a wire protocol command.
 
@@ -701,6 +706,8 @@
         return func
     return register
 
+# TODO define a more appropriate permissions type to use for this.
+permissions['batch'] = 'pull'
 @wireprotocommand('batch', 'cmds *')
 def batch(repo, proto, cmds, others):
     repo = repo.filtered("served")
@@ -713,6 +720,17 @@
                 n, v = a.split('=')
                 vals[unescapearg(n)] = unescapearg(v)
         func, spec = commands[op]
+
+        # If the protocol supports permissions checking, perform that
+        # checking on each batched command.
+        # TODO formalize permission checking as part of protocol interface.
+        if util.safehasattr(proto, 'checkperm'):
+            # Assume commands with no defined permissions are writes / for
+            # pushes. This is the safest from a security perspective because
+            # it doesn't allow commands with undefined semantics from
+            # bypassing permissions checks.
+            proto.checkperm(permissions.get(op, 'push'))
+
         if spec:
             keys = spec.split()
             data = {}
@@ -740,6 +758,7 @@
 
     return bytesresponse(';'.join(res))
 
+permissions['between'] = 'pull'
 @wireprotocommand('between', 'pairs', transportpolicy=POLICY_V1_ONLY)
 def between(repo, proto, pairs):
     pairs = [decodelist(p, '-') for p in pairs.split(" ")]
@@ -749,6 +768,7 @@
 
     return bytesresponse(''.join(r))
 
+permissions['branchmap'] = 'pull'
 @wireprotocommand('branchmap')
 def branchmap(repo, proto):
     branchmap = repo.branchmap()
@@ -760,6 +780,7 @@
 
     return bytesresponse('\n'.join(heads))
 
+permissions['branches'] = 'pull'
 @wireprotocommand('branches', 'nodes', transportpolicy=POLICY_V1_ONLY)
 def branches(repo, proto, nodes):
     nodes = decodelist(nodes)
@@ -769,6 +790,7 @@
 
     return bytesresponse(''.join(r))
 
+permissions['clonebundles'] = 'pull'
 @wireprotocommand('clonebundles', '')
 def clonebundles(repo, proto):
     """Server command for returning info for available bundles to seed clones.
@@ -821,10 +843,12 @@
 
 # If you are writing an extension and consider wrapping this function. Wrap
 # `_capabilities` instead.
+permissions['capabilities'] = 'pull'
 @wireprotocommand('capabilities')
 def capabilities(repo, proto):
     return bytesresponse(' '.join(_capabilities(repo, proto)))
 
+permissions['changegroup'] = 'pull'
 @wireprotocommand('changegroup', 'roots', transportpolicy=POLICY_V1_ONLY)
 def changegroup(repo, proto, roots):
     nodes = decodelist(roots)
@@ -834,6 +858,7 @@
     gen = iter(lambda: cg.read(32768), '')
     return streamres(gen=gen)
 
+permissions['changegroupsubset'] = 'pull'
 @wireprotocommand('changegroupsubset', 'bases heads',
                   transportpolicy=POLICY_V1_ONLY)
 def changegroupsubset(repo, proto, bases, heads):
@@ -845,6 +870,7 @@
     gen = iter(lambda: cg.read(32768), '')
     return streamres(gen=gen)
 
+permissions['debugwireargs'] = 'pull'
 @wireprotocommand('debugwireargs', 'one two *')
 def debugwireargs(repo, proto, one, two, others):
     # only accept optional args from the known set
@@ -852,6 +878,7 @@
     return bytesresponse(repo.debugwireargs(one, two,
                                             **pycompat.strkwargs(opts)))
 
+permissions['getbundle'] = 'pull'
 @wireprotocommand('getbundle', '*')
 def getbundle(repo, proto, others):
     opts = options('getbundle', gboptsmap.keys(), others)
@@ -918,11 +945,13 @@
 
     return streamres(gen=chunks, prefer_uncompressed=not prefercompressed)
 
+permissions['heads'] = 'pull'
 @wireprotocommand('heads')
 def heads(repo, proto):
     h = repo.heads()
     return bytesresponse(encodelist(h) + '\n')
 
+permissions['hello'] = 'pull'
 @wireprotocommand('hello')
 def hello(repo, proto):
     """Called as part of SSH handshake to obtain server info.
@@ -938,11 +967,13 @@
     caps = capabilities(repo, proto).data
     return bytesresponse('capabilities: %s\n' % caps)
 
+permissions['listkeys'] = 'pull'
 @wireprotocommand('listkeys', 'namespace')
 def listkeys(repo, proto, namespace):
     d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
     return bytesresponse(pushkeymod.encodekeys(d))
 
+permissions['lookup'] = 'pull'
 @wireprotocommand('lookup', 'key')
 def lookup(repo, proto, key):
     try:
@@ -955,11 +986,13 @@
         success = 0
     return bytesresponse('%d %s\n' % (success, r))
 
+permissions['known'] = 'pull'
 @wireprotocommand('known', 'nodes *')
 def known(repo, proto, nodes, others):
     v = ''.join(b and '1' or '0' for b in repo.known(decodelist(nodes)))
     return bytesresponse(v)
 
+permissions['pushkey'] = 'push'
 @wireprotocommand('pushkey', 'namespace key old new')
 def pushkey(repo, proto, namespace, key, old, new):
     # compatibility with pre-1.8 clients which were accidentally
@@ -981,6 +1014,7 @@
     output = output.getvalue() if output else ''
     return bytesresponse('%d\n%s' % (int(r), output))
 
+permissions['stream_out'] = 'pull'
 @wireprotocommand('stream_out')
 def stream(repo, proto):
     '''If the server supports streaming clone, it advertises the "stream"
@@ -989,6 +1023,7 @@
     '''
     return streamres_legacy(streamclone.generatev1wireproto(repo))
 
+permissions['unbundle'] = 'push'
 @wireprotocommand('unbundle', 'heads')
 def unbundle(repo, proto, heads):
     their_heads = decodelist(heads)
--- a/tests/drawdag.py	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/drawdag.py	Tue Mar 06 14:32:14 2018 -0800
@@ -371,7 +371,8 @@
     comments = list(_getcomments(text))
     filere = re.compile(br'^(\w+)/([\w/]+)\s*=\s*(.*)$', re.M)
     for name, path, content in filere.findall(b'\n'.join(comments)):
-        files[name][path] = content.replace(br'\n', b'\n')
+        content = content.replace(br'\n', b'\n').replace(br'\1', b'\1')
+        files[name][path] = content
 
     committed = {None: node.nullid}  # {name: node}
 
--- a/tests/test-annotate.t	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/test-annotate.t	Tue Mar 06 14:32:14 2018 -0800
@@ -903,9 +903,15 @@
   $ hg init repo-cr
   $ cd repo-cr
 
-  $ substcr() {
-  > sed 's/\r/[CR]/g'
-  > }
+  $ cat <<'EOF' >> "$TESTTMP/substcr.py"
+  > import sys
+  > from mercurial import util
+  > util.setbinary(sys.stdin)
+  > util.setbinary(sys.stdout)
+  > stdin = getattr(sys.stdin, 'buffer', sys.stdin)
+  > stdout = getattr(sys.stdout, 'buffer', sys.stdout)
+  > stdout.write(stdin.read().replace(b'\r', b'[CR]'))
+  > EOF
 
   >>> with open('a', 'wb') as f:
   ...     f.write(b'0a\r0b\r\n0c\r0d\r\n0e\n0f\n0g')
@@ -914,13 +920,13 @@
   ...     f.write(b'0a\r0b\r\n1c\r1d\r\n0e\n1f\n0g')
   $ hg ci -m1
 
-  $ hg annotate -r0 a | substcr
+  $ hg annotate -r0 a | $PYTHON "$TESTTMP/substcr.py"
   0: 0a[CR]0b[CR]
   0: 0c[CR]0d[CR]
   0: 0e
   0: 0f
   0: 0g
-  $ hg annotate -r1 a | substcr
+  $ hg annotate -r1 a | $PYTHON "$TESTTMP/substcr.py"
   0: 0a[CR]0b[CR]
   1: 1c[CR]1d[CR]
   0: 0e
--- a/tests/test-http-bundle1.t	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/test-http-bundle1.t	Tue Mar 06 14:32:14 2018 -0800
@@ -260,60 +260,52 @@
   $ hg rollback -q
 
   $ sed 's/.*] "/"/' < ../access.log
-  "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=branchmap HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=stream_out HTTP/1.1" 401 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=stream_out HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D5fed3813f7f5e1824344fdc9cf8f63bb662c292d x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:common=0000000000000000000000000000000000000000&heads=5fed3813f7f5e1824344fdc9cf8f63bb662c292d x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 403 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
+  "GET /?cmd=capabilities HTTP/1.1" 403 -
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D7f4e523d01f2cc3765ac8934da3d14db775ff872 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=branchmap HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-http-permissions.t	Tue Mar 06 14:32:14 2018 -0800
@@ -0,0 +1,1502 @@
+#require killdaemons
+
+  $ cat > fakeremoteuser.py << EOF
+  > import os
+  > from mercurial.hgweb import hgweb_mod
+  > from mercurial import wireproto
+  > class testenvhgweb(hgweb_mod.hgweb):
+  >     def __call__(self, env, respond):
+  >         # Allow REMOTE_USER to define authenticated user.
+  >         if r'REMOTE_USER' in os.environ:
+  >             env[r'REMOTE_USER'] = os.environ[r'REMOTE_USER']
+  >         # Allow REQUEST_METHOD to override HTTP method
+  >         if r'REQUEST_METHOD' in os.environ:
+  >             env[r'REQUEST_METHOD'] = os.environ[r'REQUEST_METHOD']
+  >         return super(testenvhgweb, self).__call__(env, respond)
+  > hgweb_mod.hgweb = testenvhgweb
+  > 
+  > @wireproto.wireprotocommand('customreadnoperm')
+  > def customread(repo, proto):
+  >     return b'read-only command no defined permissions\n'
+  > @wireproto.wireprotocommand('customwritenoperm')
+  > def customwritenoperm(repo, proto):
+  >     return b'write command no defined permissions\n'
+  > wireproto.permissions['customreadwithperm'] = 'pull'
+  > @wireproto.wireprotocommand('customreadwithperm')
+  > def customreadwithperm(repo, proto):
+  >     return b'read-only command w/ defined permissions\n'
+  > wireproto.permissions['customwritewithperm'] = 'push'
+  > @wireproto.wireprotocommand('customwritewithperm')
+  > def customwritewithperm(repo, proto):
+  >     return b'write command w/ defined permissions\n'
+  > EOF
+
+  $ cat >> $HGRCPATH << EOF
+  > [extensions]
+  > fakeremoteuser = $TESTTMP/fakeremoteuser.py
+  > strip =
+  > EOF
+
+  $ hg init test
+  $ cd test
+  $ echo a > a
+  $ hg ci -Ama
+  adding a
+  $ cd ..
+  $ hg clone test test2
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd test2
+  $ echo a >> a
+  $ hg ci -mb
+  $ hg book bm -r 0
+  $ cd ../test
+
+web.deny_read=* prevents access to wire protocol for all users
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > deny_read = *
+  > EOF
+
+  $ hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=stream_out'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.deny_read=* with REMOTE_USER set still locks out clients
+
+  $ REMOTE_USER=authed_user hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=stream_out'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.deny_read=<user> denies access to unauthenticated user
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > deny_read = baduser1,baduser2
+  > EOF
+
+  $ hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.deny_read=<user> denies access to users in deny list
+
+  $ REMOTE_USER=baduser2 hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.deny_read=<user> allows access to authenticated users not in list
+
+  $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  200 Script output follows
+  
+  cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b	1
+  publishing	True (no-eol)
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  200 Script output follows
+  
+  cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b	1
+  publishing	True (no-eol)
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  200 Script output follows
+  
+  read-only command w/ defined permissions
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+
+  $ killdaemons.py
+
+web.allow_read=* allows reads for unauthenticated users
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > allow_read = *
+  > EOF
+
+  $ hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  200 Script output follows
+  
+  cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b	1
+  publishing	True (no-eol)
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  200 Script output follows
+  
+  cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b	1
+  publishing	True (no-eol)
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  200 Script output follows
+  
+  read-only command w/ defined permissions
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+
+  $ killdaemons.py
+
+web.allow_read=* allows read for authenticated user
+
+  $ REMOTE_USER=authed_user hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  200 Script output follows
+  
+  cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b	1
+  publishing	True (no-eol)
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  200 Script output follows
+  
+  cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b	1
+  publishing	True (no-eol)
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  200 Script output follows
+  
+  read-only command w/ defined permissions
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+
+  $ killdaemons.py
+
+web.allow_read=<user> does not allow unauthenticated users to read
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > allow_read = gooduser
+  > EOF
+
+  $ hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.allow_read=<user> does not allow user not in list to read
+
+  $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.allow_read=<user> allows read from user in list
+
+  $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  200 Script output follows
+  
+  cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b	1
+  publishing	True (no-eol)
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  200 Script output follows
+  
+  cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b	1
+  publishing	True (no-eol)
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  200 Script output follows
+  
+  read-only command w/ defined permissions
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+
+  $ killdaemons.py
+
+web.deny_read takes precedence over web.allow_read
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > allow_read = baduser
+  > deny_read = baduser
+  > EOF
+
+  $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.allow-pull=false denies read access to repo
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > allow-pull = false
+  > EOF
+
+  $ hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=capabilities'
+  401 pull not authorized
+  
+  0
+  pull not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases'
+  401 pull not authorized
+  
+  0
+  pull not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases'
+  401 pull not authorized
+  
+  0
+  pull not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 pull not authorized
+  
+  0
+  pull not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ hg --cwd ../test2 pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+Attempting a write command with HTTP GET fails
+
+  $ cat > .hg/hgrc <<EOF
+  > EOF
+
+  $ REQUEST_METHOD=GET hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+  $ hg bookmark -d bm
+  abort: bookmark 'bm' does not exist
+  [255]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ killdaemons.py
+
+Attempting a write command with an unknown HTTP verb fails
+
+  $ REQUEST_METHOD=someverb hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+  $ hg bookmark -d bm
+  abort: bookmark 'bm' does not exist
+  [255]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  405 push requires POST request
+  
+  0
+  push requires POST request
+  [1]
+
+  $ killdaemons.py
+
+Pushing on a plaintext channel is disabled by default
+
+  $ cat > .hg/hgrc <<EOF
+  > EOF
+
+  $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  403 ssl required
+  
+  0
+  ssl required
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  403 ssl required
+  
+  0
+  ssl required
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  403 ssl required
+  
+  0
+  ssl required
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  403 ssl required
+  
+  0
+  ssl required
+  [1]
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  abort: HTTP Error 403: ssl required
+  [255]
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: HTTP Error 403: ssl required
+  [255]
+
+  $ killdaemons.py
+
+web.deny_push=* denies pushing to unauthenticated users
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > push_ssl = false
+  > deny_push = *
+  > EOF
+
+  $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  abort: authorization failed
+  [255]
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.deny_push=* denies pushing to authenticated users
+
+  $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  abort: authorization failed
+  [255]
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.deny_push=<user> denies pushing to user in list
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > push_ssl = false
+  > deny_push = baduser
+  > EOF
+
+  $ REMOTE_USER=baduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  abort: authorization failed
+  [255]
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.deny_push=<user> denies pushing to user not in list because allow-push isn't set
+
+  $ REMOTE_USER=gooduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  abort: authorization failed
+  [255]
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.allow-push=* allows pushes from unauthenticated users
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > push_ssl = false
+  > allow-push = *
+  > EOF
+
+  $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  200 Script output follows
+  
+  1
+
+  $ hg bookmarks
+     bm                        0:cb9a9f314b8b
+  $ hg book -d bm
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  200 Script output follows
+  
+  write command no defined permissions
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  200 Script output follows
+  
+  write command w/ defined permissions
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  exporting bookmark bm
+  [1]
+
+  $ hg book -d bm
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+
+  $ hg strip -r 1:
+  saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-eea704d7-backup.hg
+
+  $ killdaemons.py
+
+web.allow-push=* allows pushes from authenticated users
+
+  $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  200 Script output follows
+  
+  1
+
+  $ hg bookmarks
+     bm                        0:cb9a9f314b8b
+  $ hg book -d bm
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  200 Script output follows
+  
+  write command no defined permissions
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  200 Script output follows
+  
+  write command w/ defined permissions
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  exporting bookmark bm
+  [1]
+
+  $ hg book -d bm
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+
+  $ hg strip -r 1:
+  saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-eea704d7-backup.hg
+
+  $ killdaemons.py
+
+web.allow-push=<user> denies push to user not in list
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > push_ssl = false
+  > allow-push = gooduser
+  > EOF
+
+  $ REMOTE_USER=baduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  abort: authorization failed
+  [255]
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.allow-push=<user> allows push from user in list
+
+  $ REMOTE_USER=gooduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  200 Script output follows
+  
+  1
+
+  $ hg bookmarks
+     bm                        0:cb9a9f314b8b
+  $ hg book -d bm
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  200 Script output follows
+  
+  1
+
+  $ hg bookmarks
+     bm                        0:cb9a9f314b8b
+  $ hg book -d bm
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  200 Script output follows
+  
+  write command no defined permissions
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  200 Script output follows
+  
+  write command w/ defined permissions
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  exporting bookmark bm
+  [1]
+
+  $ hg book -d bm
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+
+  $ hg strip -r 1:
+  saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-eea704d7-backup.hg
+
+  $ killdaemons.py
+
+web.deny_push takes precedence over web.allow_push
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > push_ssl = false
+  > allow-push = someuser
+  > deny_push = someuser
+  > EOF
+
+  $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 push not authorized
+  
+  0
+  push not authorized
+  [1]
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  abort: authorization failed
+  [255]
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
+
+web.allow-push has no effect if web.deny_read is set
+
+  $ cat > .hg/hgrc <<EOF
+  > [web]
+  > push_ssl = false
+  > allow-push = *
+  > deny_read = *
+  > EOF
+
+  $ REQUEST_METHOD=POST REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ hg bookmarks
+  no bookmarks set
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadnoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customreadwithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritenoperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+  $ get-with-headers.py $LOCALIP:$HGPORT '?cmd=customwritewithperm'
+  401 read not authorized
+  
+  0
+  read not authorized
+  [1]
+
+Reset server to remove REQUEST_METHOD hack to test hg client
+
+  $ killdaemons.py
+  $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid
+  $ cat hg.pid > $DAEMON_PIDS
+
+  $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ hg --cwd ../test2 push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  abort: authorization failed
+  [255]
+
+  $ killdaemons.py
--- a/tests/test-http.t	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/test-http.t	Tue Mar 06 14:32:14 2018 -0800
@@ -254,6 +254,7 @@
   http auth: user user, password ****
   sending capabilities command
   devel-peer-request: GET http://localhost:$HGPORT2/?cmd=capabilities
+  http auth: user user, password ****
   devel-peer-request:   finished in *.???? seconds (200) (glob)
   query 1; heads
   sending batch command
@@ -270,7 +271,6 @@
   devel-peer-request:   Vary X-HgArg-1,X-HgProto-1
   devel-peer-request:   X-hgproto-1 0.1 0.2 comp=$USUAL_COMPRESSIONS$
   devel-peer-request:   16 bytes of commands arguments in headers
-  http auth: user user, password ****
   devel-peer-request:   finished in *.???? seconds (200) (glob)
   received listkey for "phases": 58 bytes
   checking for updated bookmarks
@@ -340,57 +340,49 @@
   $ hg rollback -q
 
   $ sed 's/.*] "/"/' < ../access.log
-  "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=branchmap HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=stream_out HTTP/1.1" 401 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=stream_out HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D5fed3813f7f5e1824344fdc9cf8f63bb662c292d x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=0&common=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&heads=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=getbundle HTTP/1.1" 401 - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=1&common=0000000000000000000000000000000000000000&heads=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=getbundle HTTP/1.1" 200 - x-hgarg-1:bookmarks=1&$USUAL_BUNDLE_CAPS$&cg=1&common=0000000000000000000000000000000000000000&heads=5fed3813f7f5e1824344fdc9cf8f63bb662c292d&listkeys=bookmarks&phases=1 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=capabilities HTTP/1.1" 200 -
-  "GET /?cmd=lookup HTTP/1.1" 200 - x-hgarg-1:key=tip x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 403 - x-hgarg-1:namespace=namespaces x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
+  "GET /?cmd=capabilities HTTP/1.1" 403 -
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D7f4e523d01f2cc3765ac8934da3d14db775ff872 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=branchmap HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
@@ -398,9 +390,9 @@
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "POST /?cmd=unbundle HTTP/1.1" 200 - x-hgarg-1:heads=666f726365* (glob)
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
+  "GET /?cmd=capabilities HTTP/1.1" 401 -
   "GET /?cmd=capabilities HTTP/1.1" 200 -
   "GET /?cmd=batch HTTP/1.1" 200 - x-hgarg-1:cmds=heads+%3Bknown+nodes%3D7f4e523d01f2cc3765ac8934da3d14db775ff872 x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
-  "GET /?cmd=listkeys HTTP/1.1" 401 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=phases x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=listkeys HTTP/1.1" 200 - x-hgarg-1:namespace=bookmarks x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
   "GET /?cmd=branchmap HTTP/1.1" 200 - x-hgproto-1:0.1 0.2 comp=$USUAL_COMPRESSIONS$
--- a/tests/test-largefiles-wireproto.t	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/test-largefiles-wireproto.t	Tue Mar 06 14:32:14 2018 -0800
@@ -444,11 +444,11 @@
   > EOF
   $ hg clone --config ui.interactive=true --config extensions.getpass=get_pass.py \
   >          http://user@localhost:$HGPORT credentialclone
-  requesting all changes
   http authorization required for http://localhost:$HGPORT/
   realm: mercurial
   user: user
-  password: adding changesets
+  password: requesting all changes
+  adding changesets
   adding manifests
   adding file changes
   added 1 changesets with 1 changes to 1 files
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-lfs-bundle.t	Tue Mar 06 14:32:14 2018 -0800
@@ -0,0 +1,97 @@
+In this test, we want to test LFS bundle application on both LFS and non-LFS
+repos.
+
+To make it more interesting, the file revisions will contain hg filelog
+metadata ('\1\n'). The bundle will have 1 file revision overlapping with the
+destination repo.
+
+#  rev      1          2         3
+#  repo:    yes        yes       no
+#  bundle:  no (base)  yes       yes (deltabase: 2 if possible)
+
+It is interesting because rev 2 could have been stored as LFS in the repo, and
+non-LFS in the bundle; or vice-versa.
+
+Init
+
+  $ cat >> $HGRCPATH << EOF
+  > [extensions]
+  > lfs=
+  > drawdag=$TESTDIR/drawdag.py
+  > [lfs]
+  > url=file:$TESTTMP/lfs-remote
+  > EOF
+
+Helper functions
+
+  $ commitxy() {
+  > hg debugdrawdag "$@" <<'EOS'
+  >  Y  # Y/X=\1\nAAAA\nE\nF
+  >  |  # Y/Y=\1\nAAAA\nG\nH
+  >  X  # X/X=\1\nAAAA\nC\n
+  >     # X/Y=\1\nAAAA\nD\n
+  > EOS
+  > }
+
+  $ commitz() {
+  > hg debugdrawdag "$@" <<'EOS'
+  >  Z  # Z/X=\1\nAAAA\nI\n
+  >  |  # Z/Y=\1\nAAAA\nJ\n
+  >  |  # Z/Z=\1\nZ
+  >  Y
+  > EOS
+  > }
+
+  $ enablelfs() {
+  >   cat >> .hg/hgrc <<EOF
+  > [lfs]
+  > track=all()
+  > EOF
+  > }
+
+Generate bundles
+
+  $ for i in normal lfs; do
+  >   NAME=src-$i
+  >   hg init $TESTTMP/$NAME
+  >   cd $TESTTMP/$NAME
+  >   [ $i = lfs ] && enablelfs
+  >   commitxy
+  >   commitz
+  >   hg bundle -q --base X -r Y+Z $TESTTMP/$NAME.bundle
+  >   SRCNAMES="$SRCNAMES $NAME"
+  > done
+
+Prepare destination repos
+
+  $ for i in normal lfs; do
+  >   NAME=dst-$i
+  >   hg init $TESTTMP/$NAME
+  >   cd $TESTTMP/$NAME
+  >   [ $i = lfs ] && enablelfs
+  >   commitxy
+  >   DSTNAMES="$DSTNAMES $NAME"
+  > done
+
+Apply bundles
+
+  $ for i in $SRCNAMES; do
+  >   for j in $DSTNAMES; do
+  >     echo ---- Applying $i.bundle to $j ----
+  >     cp -R $TESTTMP/$j $TESTTMP/tmp-$i-$j
+  >     cd $TESTTMP/tmp-$i-$j
+  >     if hg unbundle $TESTTMP/$i.bundle -q 2>/dev/null; then
+  >       hg verify -q && echo OK
+  >     else
+  >       echo CRASHED
+  >     fi
+  >   done
+  > done
+  ---- Applying src-normal.bundle to dst-normal ----
+  OK
+  ---- Applying src-normal.bundle to dst-lfs ----
+  OK
+  ---- Applying src-lfs.bundle to dst-normal ----
+  OK
+  ---- Applying src-lfs.bundle to dst-lfs ----
+  OK
--- a/tests/test-lfs.t	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/test-lfs.t	Tue Mar 06 14:32:14 2018 -0800
@@ -371,7 +371,7 @@
   uncompressed size of bundle content:
        * (changelog) (glob)
        * (manifests) (glob)
-       *  a (glob)
+      * a (glob)
   $ hg --config extensions.strip= strip -r 2 --no-backup --force -q
   $ hg -R bundle.hg log -p -T '{rev} {desc}\n' a
   5 branching
--- a/tests/test-pull-http.t	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/test-pull-http.t	Tue Mar 06 14:32:14 2018 -0800
@@ -50,7 +50,6 @@
   $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
   $ cat hg.pid >> $DAEMON_PIDS
   $ hg clone http://localhost:$HGPORT/ test4 # bundle2+
-  requesting all changes
   abort: authorization failed
   [255]
   $ hg clone http://localhost:$HGPORT/ test4 --config devel.legacy.exchange=bundle1
@@ -74,7 +73,6 @@
 
   $ req
   pulling from http://localhost:$HGPORT/
-  searching for changes
   abort: authorization failed
   % serve errors
 
--- a/tests/test-push-http.t	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/test-push-http.t	Tue Mar 06 14:32:14 2018 -0800
@@ -307,28 +307,6 @@
   $ hg --config extensions.strip= strip -r 1:
   saved backup bundle to $TESTTMP/test/.hg/strip-backup/ba677d0156c1-eea704d7-backup.hg
 
-expect authorization error: all users denied
-
-  $ echo '[web]' > .hg/hgrc
-  $ echo 'push_ssl = false' >> .hg/hgrc
-  $ echo 'deny_push = *' >> .hg/hgrc
-  $ req
-  pushing to http://localhost:$HGPORT/
-  searching for changes
-  abort: authorization failed
-  % serve errors
-  [255]
-
-expect authorization error: some users denied, users must be authenticated
-
-  $ echo 'deny_push = unperson' >> .hg/hgrc
-  $ req
-  pushing to http://localhost:$HGPORT/
-  searching for changes
-  abort: authorization failed
-  % serve errors
-  [255]
-
 #if bundle2
 
   $ cat > .hg/hgrc <<EOF
--- a/tests/test-revlog-raw.py	Sun Mar 04 21:16:36 2018 -0500
+++ b/tests/test-revlog-raw.py	Tue Mar 06 14:32:14 2018 -0800
@@ -114,6 +114,8 @@
             else:
                 # suboptimal deltaparent
                 deltaparent = min(0, parentrev)
+            if not rlog.candelta(deltaparent, r):
+                deltaparent = -1
             return {'node': rlog.node(r), 'p1': pnode, 'p2': node.nullid,
                     'cs': rlog.node(rlog.linkrev(r)), 'flags': rlog.flags(r),
                     'deltabase': rlog.node(deltaparent),
@@ -151,12 +153,14 @@
     for r in rlog:
         p1 = rlog.node(r - 1)
         p2 = node.nullid
-        if r == 0:
+        if r == 0 or (rlog.flags(r) & revlog.REVIDX_EXTSTORED):
             text = rlog.revision(r, raw=True)
             cachedelta = None
         else:
-            # deltaparent is more interesting if it has the EXTSTORED flag.
-            deltaparent = max([0] + [p for p in range(r - 2) if rlog.flags(p)])
+            # deltaparent cannot have EXTSTORED flag.
+            deltaparent = max([-1] +
+                              [p for p in range(r)
+                               if rlog.flags(p) & revlog.REVIDX_EXTSTORED == 0])
             text = None
             cachedelta = (deltaparent, rlog.revdiff(deltaparent, r))
         flags = rlog.flags(r)
@@ -262,8 +266,9 @@
         result.append((text, rawtext))
 
         # Verify flags like isdelta, isext work as expected
-        if bool(rlog.deltaparent(rev) > -1) != isdelta:
-            abort('rev %d: isdelta is ineffective' % rev)
+        # isdelta can be overridden to False if this or p1 has isext set
+        if bool(rlog.deltaparent(rev) > -1) and not isdelta:
+            abort('rev %d: isdelta is unexpected' % rev)
         if bool(rlog.flags(rev)) != isext:
             abort('rev %d: isext is ineffective' % rev)
     return result