Patchwork hgweb: support WebDAV multi-status directory listings in raw mode

login
register
mail settings
Submitter Paul Boddie
Date April 27, 2013, 10:45 p.m.
Message ID <b64f615c8d6217151aff.1367102751@localhost.localdomain>
Download mbox | patch
Permalink /patch/1491/
State Deferred, archived
Headers show

Comments

Paul Boddie - April 27, 2013, 10:45 p.m.
# HG changeset patch
# User Paul Boddie <paul@boddie.org.uk>
# Date 1367102678 -7200
# Branch stable
# Node ID b64f615c8d6217151affa59d36586381fd55081e
# Parent  70675d77fd4a78d3e57723550d9f3031345d38e4
hgweb: support WebDAV multi-status directory listings in raw mode

The raw-file command, along with the file command when the style is set to
raw, provide directory listings in a textual format resembling the output of
the Unix "ls -l" command. These listings are arguably awkward to parse, but an
alternative exists in the form of the multi-status response defined for the
PROPFIND method extension to the HTTP protocol, as described in the WebDAV
standard (RFC 4918).

This patch provides multi-status responses for PROPFIND requests for directory
information (listing the details of files and subdirectories), file
information (listing the properties of each file), and the repository root
(either providing a listing for the top-level directory or redirecting to a
changeset-specific resource that does so). Only requests with a depth of 0 or
1 are supported, since the raw-file command does not attempt to support
recursive listings.

In order to test the PROPFIND method, the get-with-headers.py program in the
test suite has been modified to support explicit method and depth arguments,
but still supports the common case where a GET request is to be issued.

Only the raw-file command supports PROPFIND in this patch. Moreover, PROPFIND
request bodies are not interpreted, since this would require the parsing of an
XML payload and the selective production of resource properties for files and
directories. A future patch could add Mercurial-specific properties and their
selection via PROPFIND, but the behaviour provided by this patch is restricted
to supporting the retrieval of standardised properties as if "allprop" were
specified (see RFC 4918, section 9.1).

In order to support a consistent resource hierarchy, redirects are employed to
direct clients to changeset-specific resources describing directories and
files. Thus, references to symbolic changeset identifiers (tags, branches,
revision numbers, and so on) are replaced by the short, 12-character form of
the concrete changeset identifier, and clients are directed to navigate
resources employing the latter in all responses.

This patch does not attempt to support other methods specified in the WebDAV
documentation, nor does it alter the result of the OPTIONS method to indicate
WebDAV compliance, since the functionality provided is more of a convenience
than a rigid adherence to the WebDAV specification.

Patch

diff -r 70675d77fd4a -r b64f615c8d62 mercurial/hgweb/common.py
--- a/mercurial/hgweb/common.py	Tue Apr 23 17:26:00 2013 -0500
+++ b/mercurial/hgweb/common.py	Sun Apr 28 00:44:38 2013 +0200
@@ -9,6 +9,8 @@ 
 import errno, mimetypes, os
 
 HTTP_OK = 200
+HTTP_MULTI_STATUS = 207
+HTTP_FOUND = 302
 HTTP_NOT_MODIFIED = 304
 HTTP_BAD_REQUEST = 400
 HTTP_UNAUTHORIZED = 401
@@ -107,6 +109,8 @@ 
 def _statusmessage(code):
     from BaseHTTPServer import BaseHTTPRequestHandler
     responses = BaseHTTPRequestHandler.responses
+    responses[HTTP_MULTI_STATUS] = ('Multi-Status',
+                                    'WebDAV multi-status response')
     return responses.get(code, ('Error', 'Unknown error'))[0]
 
 def statusmessage(code, message=None):
@@ -190,3 +194,15 @@ 
     if req.env.get('HTTP_IF_NONE_MATCH') == tag:
         raise ErrorResponse(HTTP_NOT_MODIFIED)
     req.headers.append(('ETag', tag))
+
+def redirect(url, req):
+    """redirect to the given location"""
+    req.headers.append(('Location', url))
+    req.respond(HTTP_FOUND, 'text/plain')
+    return 'Redirected to %s' % url
+
+def propfind(req):
+    return req.env['REQUEST_METHOD'] == 'PROPFIND'
+
+def propfinddepth(req):
+    return req.env.get('HTTP_DEPTH')
diff -r 70675d77fd4a -r b64f615c8d62 mercurial/hgweb/hgweb_mod.py
--- a/mercurial/hgweb/hgweb_mod.py	Tue Apr 23 17:26:00 2013 -0500
+++ b/mercurial/hgweb/hgweb_mod.py	Sun Apr 28 00:44:38 2013 +0200
@@ -10,9 +10,9 @@ 
 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
 from mercurial.templatefilters import websub
 from mercurial.i18n import _
-from common import get_stat, ErrorResponse, permhooks, caching
+from common import get_stat, ErrorResponse, permhooks, caching, propfind
 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
-from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
+from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR, HTTP_FORBIDDEN
 from request import wsgirequest
 import webcommands, protocol, webutil, re
 
@@ -241,6 +241,9 @@ 
             elif cmd == 'file' and 'raw' in req.form.get('style', []):
                 self.ctype = ctype
                 content = webcommands.rawfile(self, req, tmpl)
+            elif cmd != 'raw-file' and propfind(req):
+                msg = 'PROPFIND not supported for command: %s' % cmd
+                raise ErrorResponse(HTTP_FORBIDDEN, msg)
             else:
                 content = getattr(webcommands, cmd)(self, req, tmpl)
                 req.respond(HTTP_OK, ctype)
diff -r 70675d77fd4a -r b64f615c8d62 mercurial/hgweb/server.py
--- a/mercurial/hgweb/server.py	Tue Apr 23 17:26:00 2013 -0500
+++ b/mercurial/hgweb/server.py	Sun Apr 28 00:44:38 2013 +0200
@@ -83,6 +83,9 @@ 
                            "request '%s':\n%s", self.path, tb)
 
     def do_GET(self):
+        self.do_POST()
+
+    def do_PROPFIND(self):
         self.do_POST()
 
     def do_hgweb(self):
diff -r 70675d77fd4a -r b64f615c8d62 mercurial/hgweb/webcommands.py
--- a/mercurial/hgweb/webcommands.py	Tue Apr 23 17:26:00 2013 -0500
+++ b/mercurial/hgweb/webcommands.py	Sun Apr 28 00:44:38 2013 +0200
@@ -11,7 +11,8 @@ 
 from mercurial.node import short, hex, nullid
 from mercurial.util import binary
 from common import paritygen, staticfile, get_contact, ErrorResponse
-from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
+from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_MULTI_STATUS
+from common import redirect, propfind, propfinddepth
 from mercurial import graphmod, patch
 from mercurial import help as helpmod
 from mercurial import scmutil
@@ -33,12 +34,23 @@ 
         return changelog(web, req, tmpl)
 
 def rawfile(web, req, tmpl):
-    guessmime = web.configbool('web', 'guessmime', False)
+    path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
+    ctx = webutil.changectx(web.repo, req)
+    nodeid = templatefilters.short(hex(ctx.node()))
 
-    path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
+    # redirect to changeset-specific URLs for a consistent WebDAV hierarchy
+    if propfind(req) and (not 'node' in req.form or
+                          req.form['node'] != [nodeid]):
+        return redirect("%s%s/raw-file/%s/%s" %
+                        (tmpl.defaults['urlbase'], req.url.rstrip("/"),
+                         nodeid, path), req)
+
     if not path:
         content = manifest(web, req, tmpl)
-        req.respond(HTTP_OK, web.ctype)
+        if propfind(req):
+            req.respond(HTTP_MULTI_STATUS, 'application/xml')
+        else:
+            req.respond(HTTP_OK, web.ctype)
         return content
 
     try:
@@ -46,23 +58,59 @@ 
     except error.LookupError, inst:
         try:
             content = manifest(web, req, tmpl)
-            req.respond(HTTP_OK, web.ctype)
+            if propfind(req):
+                req.respond(HTTP_MULTI_STATUS, 'application/xml')
+            else:
+                req.respond(HTTP_OK, web.ctype)
             return content
         except ErrorResponse:
             raise inst
 
+    if propfind(req):
+        content = _davfilerevision(web, tmpl, fctx)
+        req.respond(HTTP_MULTI_STATUS, 'application/xml')
+        return content
+
     path = fctx.path()
     text = fctx.data()
+    req.respond(HTTP_OK, rawfiletype(web, path, text), path, body=text)
+    return []
+
+def rawfiletype(web, path, text, withencoding=True):
+    guessmime = web.configbool('web', 'guessmime', False)
+
     mt = 'application/binary'
     if guessmime:
         mt = mimetypes.guess_type(path)[0]
         if mt is None:
             mt = binary(text) and 'application/binary' or 'text/plain'
-    if mt.startswith('text/'):
+    if mt.startswith('text/') and withencoding:
         mt += '; charset="%s"' % encoding.encoding
 
-    req.respond(HTTP_OK, mt, path, body=text)
-    return []
+    return mt
+
+def _davfilerevision(web, tmpl, fctx):
+    f = fctx.path()
+    text = fctx.data()
+    abspath = "/" + f
+    dirname = webutil.up(f)
+    return tmpl("davfilerevision",
+                file=f,
+                path=abspath,
+                basename=abspath[len(dirname):],
+                rev=fctx.rev(),
+                node=fctx.hex(),
+                author=fctx.user(),
+                date=fctx.date(),
+                size=fctx.size(),
+                type=rawfiletype(web, f, text, False),
+                desc=fctx.description(),
+                extra=fctx.extra(),
+                branch=webutil.nodebranchnodefault(fctx),
+                parent=webutil.parents(fctx),
+                child=webutil.children(fctx),
+                rename=webutil.renamelink(fctx),
+                permissions=fctx.manifest().flags(f))
 
 def _filerevision(web, tmpl, fctx):
     f = fctx.path()
@@ -339,41 +387,49 @@ 
     l = len(path)
     abspath = "/" + path
 
-    for full, n in mf.iteritems():
-        # the virtual path (working copy path) used for the full
-        # (repository) path
-        f = decodepath(full)
+    if propfind(req) and propfinddepth(req) not in ('0', '1'):
+        raise ErrorResponse(HTTP_FORBIDDEN, 'depth not supported: %s' %
+                                            propfinddepth(req))
 
-        if f[:l] != path:
-            continue
-        remain = f[l:]
-        elements = remain.split('/')
-        if len(elements) == 1:
-            files[remain] = full
-        else:
-            h = dirs # need to retain ref to dirs (root)
-            for elem in elements[0:-1]:
-                if elem not in h:
-                    h[elem] = {}
-                h = h[elem]
-                if len(h) > 1:
-                    break
-            h[None] = None # denotes files present
+    if not propfind(req) or propfinddepth(req) == '1':
+        for full, n in mf.iteritems():
+            # the virtual path (working copy path) used for the full
+            # (repository) path
+            f = decodepath(full)
 
-    if mf and not files and not dirs:
-        raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
+            if f[:l] != path:
+                continue
+            remain = f[l:]
+            elements = remain.split('/')
+            if len(elements) == 1:
+                files[remain] = full
+            else:
+                h = dirs # need to retain ref to dirs (root)
+                for elem in elements[0:-1]:
+                    if elem not in h:
+                        h[elem] = {}
+                    h = h[elem]
+                    if len(h) > 1:
+                        break
+                h[None] = None # denotes files present
+
+        if mf and not files and not dirs:
+            raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
 
     def filelist(**map):
         for f in sorted(files):
             full = files[f]
 
             fctx = ctx.filectx(full)
-            yield {"file": full,
+            res = {"file": full,
                    "parity": parity.next(),
                    "basename": f,
                    "date": fctx.date(),
                    "size": fctx.size(),
                    "permissions": mf.flags(full)}
+            if propfind(req):
+                res["type"] = rawfiletype(web, fctx.path(), fctx.data(), False)
+            yield res
 
     def dirlist(**map):
         for d in sorted(dirs):
@@ -392,7 +448,7 @@ 
                    "emptydirs": "/".join(emptydirs),
                    "basename": d}
 
-    return tmpl("manifest",
+    return tmpl(propfind(req) and "davmanifest" or "manifest",
                 rev=ctx.rev(),
                 node=hex(node),
                 path=abspath,
diff -r 70675d77fd4a -r b64f615c8d62 mercurial/templates/raw/davdirentry.tmpl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/templates/raw/davdirentry.tmpl	Sun Apr 28 00:44:38 2013 +0200
@@ -0,0 +1,12 @@ 
+  <D:response>
+    <D:href>{url}raw-file/{node|short}{path|urlescape}</D:href>
+    <D:propstat>
+      <D:prop>
+        <D:displayname>{basename|escape}</D:displayname>
+        <D:resourcetype>
+          <D:collection/>
+        </D:resourcetype>
+      </D:prop>
+      <D:status>HTTP/1.1 200 OK</D:status>
+    </D:propstat>
+  </D:response>
diff -r 70675d77fd4a -r b64f615c8d62 mercurial/templates/raw/davfileentry.tmpl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/templates/raw/davfileentry.tmpl	Sun Apr 28 00:44:38 2013 +0200
@@ -0,0 +1,15 @@ 
+  <D:response>
+    <D:href>{url}raw-file/{node|short}/{file|urlescape}</D:href>
+    <D:propstat>
+      <D:prop>
+        <D:displayname>{basename|escape}</D:displayname>
+        <D:resourcetype>
+        </D:resourcetype>
+        <D:getcontentlength>{size}</D:getcontentlength>
+        <D:getcontenttype>{type}</D:getcontenttype>
+        <D:getlastmodified>{date|rfc822date}</D:getlastmodified>
+      </D:prop>
+      <D:status>HTTP/1.1 200 OK</D:status>
+    </D:propstat>
+  </D:response>
+
diff -r 70675d77fd4a -r b64f615c8d62 mercurial/templates/raw/davfilerevision.tmpl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/templates/raw/davfilerevision.tmpl	Sun Apr 28 00:44:38 2013 +0200
@@ -0,0 +1,17 @@ 
+<?xml version="1.0" encoding="utf-8" ?>
+<D:multistatus xmlns:D="DAV:">
+  <D:response>
+    <D:href>{url}raw-file/{node|short}{path|escape}</D:href>
+    <D:propstat>
+      <D:prop>
+        <D:displayname>{basename|escape}</D:displayname>
+        <D:resourcetype>
+        </D:resourcetype>
+        <D:getcontentlength>{size}</D:getcontentlength>
+        <D:getcontenttype>{type}</D:getcontenttype>
+        <D:getlastmodified>{date|rfc822date}</D:getlastmodified>
+      </D:prop>
+      <D:status>HTTP/1.1 200 OK</D:status>
+    </D:propstat>
+  </D:response>
+</D:multistatus>
diff -r 70675d77fd4a -r b64f615c8d62 mercurial/templates/raw/davmanifest.tmpl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/templates/raw/davmanifest.tmpl	Sun Apr 28 00:44:38 2013 +0200
@@ -0,0 +1,17 @@ 
+<?xml version="1.0" encoding="utf-8" ?>
+<D:multistatus xmlns:D="DAV:">
+  <D:response>
+    <D:href>{url}raw-file/{node|short}{path|escape}</D:href>
+    <D:propstat>
+      <D:prop>
+        <D:displayname>{path|escape}</D:displayname>
+        <D:resourcetype>
+          <D:collection/>
+        </D:resourcetype>
+      </D:prop>
+      <D:status>HTTP/1.1 200 OK</D:status>
+    </D:propstat>
+  </D:response>
+{dentries%davdirentry}
+{fentries%davfileentry}
+</D:multistatus>
diff -r 70675d77fd4a -r b64f615c8d62 mercurial/templates/raw/map
--- a/mercurial/templates/raw/map	Tue Apr 23 17:26:00 2013 -0500
+++ b/mercurial/templates/raw/map	Sun Apr 28 00:44:38 2013 +0200
@@ -20,6 +20,10 @@ 
 manifest = manifest.tmpl
 direntry = 'drwxr-xr-x {basename}\n'
 fileentry = '{permissions|permissions} {size} {basename}\n'
+davmanifest = davmanifest.tmpl
+davdirentry = davdirentry.tmpl
+davfileentry = davfileentry.tmpl
+davfilerevision = davfilerevision.tmpl
 index = index.tmpl
 notfound = notfound.tmpl
 error = error.tmpl
diff -r 70675d77fd4a -r b64f615c8d62 tests/get-with-headers.py
--- a/tests/get-with-headers.py	Tue Apr 23 17:26:00 2013 -0500
+++ b/tests/get-with-headers.py	Sun Apr 28 00:44:38 2013 +0200
@@ -1,7 +1,7 @@ 
 #!/usr/bin/env python
 
-"""This does HTTP GET requests given a host:port and path and returns
-a subset of the headers plus the body of the result."""
+"""This does HTTP GET or PROPFIND requests given a host:port and path and
+returns a subset of the headers plus the body of the result."""
 
 import httplib, sys
 
@@ -24,15 +24,14 @@ 
 reasons = {'Not modified': 'Not Modified'} # python 2.4
 
 tag = None
-def request(host, path, show):
+def request(host, path, show, method, headers):
     assert not path.startswith('/'), path
     global tag
-    headers = {}
     if tag:
         headers['If-None-Match'] = tag
 
     conn = httplib.HTTPConnection(host)
-    conn.request("GET", '/' + path, None, headers)
+    conn.request(method, '/' + path, None, headers)
     response = conn.getresponse()
     print response.status, reasons.get(response.reason, response.reason)
     if show[:1] == ['-']:
@@ -51,9 +50,23 @@ 
 
     return response.status
 
-status = request(sys.argv[1], sys.argv[2], sys.argv[3:])
+host, path = sys.argv[1:3]
+headers = {}
+
+if sys.argv[3:] and sys.argv[3] in ('PROPFIND', 'GET'):
+    method = sys.argv[3]
+    if method == 'PROPFIND':
+        headers['Depth'] = sys.argv[4]
+        show = sys.argv[5:]
+    else:
+        show = sys.argv[4:]
+else:
+    method = 'GET'
+    show = sys.argv[3:]
+
+status = request(host, path, show, method, headers)
 if twice:
-    status = request(sys.argv[1], sys.argv[2], sys.argv[3:])
+    status = request(host, path, show, method, headers)
 
 if 200 <= status <= 305:
     sys.exit(0)
diff -r 70675d77fd4a -r b64f615c8d62 tests/test-hgweb-commands.t
--- a/tests/test-hgweb-commands.t	Tue Apr 23 17:26:00 2013 -0500
+++ b/tests/test-hgweb-commands.t	Sun Apr 28 00:44:38 2013 +0200
@@ -579,6 +579,32 @@ 
   
   
   
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'file/1/foo?style=raw' PROPFIND 0
+  302 Found
+  
+  Redirected to http://localhost.localdomain:$HGPORT/raw-file/a4f92ed23982/foo (no-eol)
+
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'raw-file/a4f92ed23982/foo' PROPFIND 0
+  207 Multi-Status
+  
+  <?xml version="1.0" encoding="utf-8" ?>
+  <D:multistatus xmlns:D="DAV:">
+    <D:response>
+      <D:href>/raw-file/a4f92ed23982/foo</D:href>
+      <D:propstat>
+        <D:prop>
+          <D:displayname>foo</D:displayname>
+          <D:resourcetype>
+          </D:resourcetype>
+          <D:getcontentlength>4</D:getcontentlength>
+          <D:getcontenttype>application/binary</D:getcontenttype>
+          <D:getlastmodified>Thu, 01 Jan 1970 00:00:00 +0000</D:getlastmodified>
+        </D:prop>
+        <D:status>HTTP/1.1 200 OK</D:status>
+      </D:propstat>
+    </D:response>
+  </D:multistatus>
+
   $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'file/1/?style=raw'
   200 Script output follows
   
@@ -588,6 +614,112 @@ 
   -rw-r--r-- 4 foo
   
   
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'file/1/?style=raw' PROPFIND 1
+  302 Found
+  
+  Redirected to http://localhost.localdomain:$HGPORT/raw-file/a4f92ed23982/ (no-eol)
+
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'raw-file/a4f92ed23982' PROPFIND 1
+  207 Multi-Status
+  
+  <?xml version="1.0" encoding="utf-8" ?>
+  <D:multistatus xmlns:D="DAV:">
+    <D:response>
+      <D:href>/raw-file/a4f92ed23982/</D:href>
+      <D:propstat>
+        <D:prop>
+          <D:displayname>/</D:displayname>
+          <D:resourcetype>
+            <D:collection/>
+          </D:resourcetype>
+        </D:prop>
+        <D:status>HTTP/1.1 200 OK</D:status>
+      </D:propstat>
+    </D:response>
+    <D:response>
+      <D:href>/raw-file/a4f92ed23982/da</D:href>
+      <D:propstat>
+        <D:prop>
+          <D:displayname>da</D:displayname>
+          <D:resourcetype>
+            <D:collection/>
+          </D:resourcetype>
+        </D:prop>
+        <D:status>HTTP/1.1 200 OK</D:status>
+      </D:propstat>
+    </D:response>
+  
+    <D:response>
+      <D:href>/raw-file/a4f92ed23982/.hgtags</D:href>
+      <D:propstat>
+        <D:prop>
+          <D:displayname>.hgtags</D:displayname>
+          <D:resourcetype>
+          </D:resourcetype>
+          <D:getcontentlength>45</D:getcontentlength>
+          <D:getcontenttype>application/binary</D:getcontenttype>
+          <D:getlastmodified>Thu, 01 Jan 1970 00:00:00 +0000</D:getlastmodified>
+        </D:prop>
+        <D:status>HTTP/1.1 200 OK</D:status>
+      </D:propstat>
+    </D:response>
+  
+    <D:response>
+      <D:href>/raw-file/a4f92ed23982/foo</D:href>
+      <D:propstat>
+        <D:prop>
+          <D:displayname>foo</D:displayname>
+          <D:resourcetype>
+          </D:resourcetype>
+          <D:getcontentlength>4</D:getcontentlength>
+          <D:getcontenttype>application/binary</D:getcontenttype>
+          <D:getlastmodified>Thu, 01 Jan 1970 00:00:00 +0000</D:getlastmodified>
+        </D:prop>
+        <D:status>HTTP/1.1 200 OK</D:status>
+      </D:propstat>
+    </D:response>
+  
+  
+  </D:multistatus>
+
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'file/1/?style=raw' PROPFIND 0
+  302 Found
+  
+  Redirected to http://localhost.localdomain:$HGPORT/raw-file/a4f92ed23982/ (no-eol)
+
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'raw-file/a4f92ed23982' PROPFIND 0
+  207 Multi-Status
+  
+  <?xml version="1.0" encoding="utf-8" ?>
+  <D:multistatus xmlns:D="DAV:">
+    <D:response>
+      <D:href>/raw-file/a4f92ed23982/</D:href>
+      <D:propstat>
+        <D:prop>
+          <D:displayname>/</D:displayname>
+          <D:resourcetype>
+            <D:collection/>
+          </D:resourcetype>
+        </D:prop>
+        <D:status>HTTP/1.1 200 OK</D:status>
+      </D:propstat>
+    </D:response>
+  
+  
+  </D:multistatus>
+
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'file/1/?style=raw' PROPFIND 2
+  302 Found
+  
+  Redirected to http://localhost.localdomain:$HGPORT/raw-file/a4f92ed23982/ (no-eol)
+
+  $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'raw-file/a4f92ed23982' PROPFIND 2
+  403 depth not supported: 2
+  
+  
+  error: depth not supported: 2
+  [1]
+
   $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT 'file/1/foo'
   200 Script output follows