mercurial/httppeer.py
branchstable
changeset 37832 6169d95dce3b
parent 37810 856f381ad74b
child 38032 f9dc1d5b676b
child 38164 aac4be30e250
--- a/mercurial/httppeer.py	Thu May 03 14:43:25 2018 +0900
+++ b/mercurial/httppeer.py	Mon Apr 30 15:32:11 2018 -0700
@@ -328,13 +328,24 @@
 
     return res
 
+class RedirectedRepoError(error.RepoError):
+    def __init__(self, msg, respurl):
+        super(RedirectedRepoError, self).__init__(msg)
+        self.respurl = respurl
+
 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
                            allowcbor=False):
     # record the url we got redirected to
+    redirected = False
     respurl = pycompat.bytesurl(resp.geturl())
     if respurl.endswith(qs):
         respurl = respurl[:-len(qs)]
+        qsdropped = False
+    else:
+        qsdropped = True
+
     if baseurl.rstrip('/') != respurl.rstrip('/'):
+        redirected = True
         if not ui.quiet:
             ui.warn(_('real URL is %s\n') % respurl)
 
@@ -351,10 +362,16 @@
     # application/hg-changegroup. We don't support such old servers.
     if not proto.startswith('application/mercurial-'):
         ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
-        raise error.RepoError(
-            _("'%s' does not appear to be an hg repository:\n"
-              "---%%<--- (%s)\n%s\n---%%<---\n")
-            % (safeurl, proto or 'no content-type', resp.read(1024)))
+        msg = _("'%s' does not appear to be an hg repository:\n"
+                "---%%<--- (%s)\n%s\n---%%<---\n") % (
+            safeurl, proto or 'no content-type', resp.read(1024))
+
+        # Some servers may strip the query string from the redirect. We
+        # raise a special error type so callers can react to this specially.
+        if redirected and qsdropped:
+            raise RedirectedRepoError(msg, respurl)
+        else:
+            raise error.RepoError(msg)
 
     try:
         subtype = proto.split('-', 1)[1]
@@ -434,8 +451,6 @@
 
     # End of ipeercommands interface.
 
-    # look up capabilities only when needed
-
     def _callstream(self, cmd, _compressible=False, **args):
         args = pycompat.byteskwargs(args)
 
@@ -853,12 +868,32 @@
     req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
                                            capable, url, 'capabilities',
                                            args)
-
     resp = sendrequest(ui, opener, req)
 
-    respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
-                                               compressible=False,
-                                               allowcbor=advertisev2)
+    # The server may redirect us to the repo root, stripping the
+    # ?cmd=capabilities query string from the URL. The server would likely
+    # return HTML in this case and ``parsev1commandresponse()`` would raise.
+    # We catch this special case and re-issue the capabilities request against
+    # the new URL.
+    #
+    # We should ideally not do this, as a redirect that drops the query
+    # string from the URL is arguably a server bug. (Garbage in, garbage out).
+    # However,  Mercurial clients for several years appeared to handle this
+    # issue without behavior degradation. And according to issue 5860, it may
+    # be a longstanding bug in some server implementations. So we allow a
+    # redirect that drops the query string to "just work."
+    try:
+        respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
+                                                   compressible=False,
+                                                   allowcbor=advertisev2)
+    except RedirectedRepoError as e:
+        req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
+                                               capable, e.respurl,
+                                               'capabilities', args)
+        resp = sendrequest(ui, opener, req)
+        respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
+                                                   compressible=False,
+                                                   allowcbor=advertisev2)
 
     try:
         rawdata = resp.read()