[MERGE] [Bug #129307] Better redirection handling

Christophe TROESTLER Christophe.Troestler+bzr at umh.ac.be
Fri Jul 18 14:21:35 BST 2008


Hi,

The following patch addresses bug #129307.  In summary, the bug was
that, when doing "bzr branch http://server/projet", the non-existing
URL "http://server/projet/.bzr/smart" was redirected to an error page,
say "http://server/error/" (and then returning 404) and this new
(bogus) URL was used to check for ".bzr/branch-format".  This resulted
in requesting "http://server/error/.bzr/branch-format" instead of the
correct "http://server/projet/.bzr/branch-format".

The solution discussed with Vincent is to only memorize a redirected
URL if the target is eventually found.

The patch implements this and also treats subsequent redirections as
missing targets (see the diff of workingtree.py)

With these modifications, (as an example)

  bzr branch http://users.skynet.be/Pierre.Brukier/anum/projet
      merge
      co

work as they should.

Regards,
ChriS
-------------- next part --------------
# Bazaar merge directive format 2 (Bazaar 0.90)
# revision_id: christophe.troestler at umh.ac.be-20080718130800-\
#   zphk68vzsoqarj1k
# target_branch: http://bazaar-vcs.org/bzr/bzr.dev/
# testament_sha1: f2fb9801837a39d69fdb2cafe33d15d0069fb2f1
# timestamp: 2008-07-18 15:09:13 +0200
# base_revision_id: pqm at pqm.ubuntu.com-20080718100017-segv2csk7ux2xs9p
# 
# Begin patch
=== modified file 'bzrlib/bzrdir.py'
--- bzrlib/bzrdir.py	2008-07-17 08:00:25 +0000
+++ bzrlib/bzrdir.py	2008-07-17 12:17:28 +0000
@@ -68,7 +68,7 @@
 from bzrlib.store.versioned import WeaveStore
 from bzrlib.transactions import WriteTransaction
 from bzrlib.transport import (
-    do_catching_redirections,
+    _do_following_redirections,
     get_transport,
     )
 from bzrlib.weave import Weave
@@ -770,37 +770,10 @@
         :param transport: Transport containing the bzrdir.
         :param _unsupported: private.
         """
-        base = transport.base
-
-        def find_format(transport):
-            return transport, BzrDirFormat.find_format(
-                transport, _server_formats=_server_formats)
-
-        def redirected(transport, e, redirection_notice):
-            qualified_source = e.get_source_url()
-            relpath = transport.relpath(qualified_source)
-            if not e.target.endswith(relpath):
-                # Not redirected to a branch-format, not a branch
-                raise errors.NotBranchError(path=e.target)
-            target = e.target[:-len(relpath)]
-            note('%s is%s redirected to %s',
-                 transport.base, e.permanently, target)
-            # Let's try with a new transport
-            # FIXME: If 'transport' has a qualifier, this should
-            # be applied again to the new transport *iff* the
-            # schemes used are the same. Uncomment this code
-            # once the function (and tests) exist.
-            # -- vila20070212
-            #target = urlutils.copy_url_qualifiers(original, target)
-            return get_transport(target)
-
-        try:
-            transport, format = do_catching_redirections(find_format,
-                                                         transport,
-                                                         redirected)
-        except errors.TooManyRedirections:
-            raise errors.NotBranchError(base)
-
+        # The transport may be updated if a redirection is followed
+        # successfully.
+        format, transport = BzrDirFormat._find_format_and_transport(
+            transport, _server_formats=_server_formats)
         BzrDir._check_supported(format, _unsupported)
         return format.open(transport, _found=True)
 
@@ -1558,21 +1531,31 @@
     # TransportLock or LockDir
 
     @classmethod
-    def find_format(klass, transport, _server_formats=True):
-        """Return the format present at transport."""
+    def _find_format_and_transport(klass, transport, _server_formats=True):
+        """Return the format present at transport and the possibly
+        redirected transport."""
         if _server_formats:
             formats = klass._control_server_formats + klass._control_formats
         else:
             formats = klass._control_formats
+
         for format in formats:
             try:
-                return format.probe_transport(transport)
-            except errors.NotBranchError:
+                # Follow redirections independently for each format.
+                return _do_following_redirections(
+                    format.probe_transport, transport)
+            except (errors.NotBranchError, errors.TooManyRedirections):
                 # this format does not find a control dir here.
                 pass
         raise errors.NotBranchError(path=transport.base)
 
     @classmethod
+    def find_format(klass, transport, _server_formats=True):
+        """Return the format present at transport."""
+        return (klass._find_format_and_transport(
+                transport, _server_formats=_server_formats))[0]
+
+    @classmethod
     def probe_transport(klass, transport):
         """Return the .bzrdir style format present in a directory."""
         try:

=== modified file 'bzrlib/tests/test_http.py'
--- bzrlib/tests/test_http.py	2008-05-19 07:50:49 +0000
+++ bzrlib/tests/test_http.py	2008-07-18 13:08:00 +0000
@@ -42,6 +42,8 @@
     ui,
     urlutils,
     )
+from bzrlib.bzrdir import BzrDir
+from bzrlib.workingtree import WorkingTreeFormat
 from bzrlib.tests import (
     http_server,
     http_utils,
@@ -125,6 +127,9 @@
     tp_adapter = TransportProtocolAdapter()
     tp_classes= (SmartHTTPTunnellingTest,
                  TestDoCatchRedirections,
+                 TestDoFollowRedirections,
+                 TestOpenFormatRedirections,
+                 TestFindFormatRedirections,
                  TestHTTPConnections,
                  TestHTTPRedirections,
                  TestHTTPSilentRedirections,
@@ -1423,6 +1428,108 @@
                           transport.do_catching_redirections,
                           self.get_a, self.old_transport, redirected)
 
+class TestDoFollowRedirections(http_utils.TestCaseWithRedirectedWebserver):
+    """Test transport._do_following_redirections."""
+
+    def setUp(self):
+        super(TestDoFollowRedirections, self).setUp()
+        self.build_tree_contents([('a', '0123456789')])
+
+        self.old_transport = self._transport(self.old_server.get_url())
+
+    def get_a(self, transport):
+        return transport.get('a')
+
+    def test_no_redirection(self):
+        t = self._transport(self.new_server.get_url())
+        (a, t1) = transport._do_following_redirections(self.get_a, t)
+        self.assertEquals('0123456789', a.read())
+        # The transport should be the very same object.
+        self.assertEquals(t, t1)
+
+    def test_one_redirection(self):
+        (a, t1) = transport._do_following_redirections(
+            self.get_a, self.old_transport)
+        self.assertEquals('0123456789', a.read())
+        self.assertEquals(self.new_server.get_url(), t1.base)
+
+
+    def __create_redirecting_server(self, server):
+        redirecting = http_utils.HTTPServerRedirecting()
+        redirecting.redirect_to(server.host, server.port)
+        return redirecting
+
+    def test_redirection_loop(self):
+        # "Long" sequence of servers redirecting to the previous one
+        s = self.get_readonly_server()
+        for i in range(10):
+            s = self.__create_redirecting_server(s)
+            s.setUp()
+        t = self._transport(s.get_url())
+        self.assertRaises(errors.TooManyRedirections,
+                          transport._do_following_redirections,
+                          self.get_a, t)
+
+
+class TestOpenFormatRedirections(http_utils.TestCaseWithRedirectedWebserver):
+    """Test BzrDir.open_from_transport in presence of redirections."""
+
+    def setUp(self):
+        super(TestOpenFormatRedirections, self).setUp()
+        self.build_tree_contents(
+            [('.bzr/',),
+             ('.bzr/smart', 'redirected'),
+             ('.bzr/branch-format', 'Bazaar-NG meta directory, format 1\n'),
+             ],)
+        self.old_transport = self._transport(self.old_server.get_url())
+
+    def test_good_redirection(self):
+        # The .bzr/smart is redirected but then is found; the
+        # redirected URL should be used by subsequent requests.
+        a_bzrdir = BzrDir.open_from_transport(self.old_transport)
+        self.assertEquals(a_bzrdir.get_repository_transport(None).base,
+                          self.new_server.get_url() + '.bzr/repository/')
+
+    def test_404_redirection(self):
+        new_prefix = 'http://%s:%s' % (self.new_server.host,
+                                       self.new_server.port)
+        # Redirect to an error page, which then returns Not-Found (404).
+        self.old_server.redirections = [
+            ('.bzr/smart', r'%s/error' % (new_prefix), 301),
+            ]
+        # The .bzr/smart is redirected and then fails, thus the
+        # redirection should not be passed to the next format looking
+        # for .bzr/branch-format
+        a_bzrdir = BzrDir.open_from_transport(self.old_transport)
+        self.assertEquals(a_bzrdir.get_repository_transport(None).base,
+                          self.old_server.get_url() + '.bzr/repository/')
+
+class TestFindFormatRedirections(http_utils.TestCaseWithRedirectedWebserver):
+    """Test WorkingTreeFormat.find_format in presence of redirections."""
+
+    def setUp(self):
+        super(TestFindFormatRedirections, self).setUp()
+        self.build_tree_contents(
+            [('.bzr/',),
+             ('.bzr/branch-format', 'Bazaar-NG meta directory, format 1\n'),
+             ],)
+        new_prefix = 'http://%s:%s' % (self.new_server.host,
+                                       self.new_server.port)
+        # Redirect to an error page, which then returns Not-Found (404).
+        self.old_server.redirections = [
+            ('.bzr/checkout(.*)', r'%s/error' % (new_prefix), 301),
+            ]
+        self.old_transport = self._transport(self.old_server.get_url())
+
+    def test_workingtree_find_format(self):
+        a_bzrdir = BzrDir.open_from_transport(self.old_transport)
+        # A redirection for .bzr/checkout/format is considered as an
+        # absence of file because the redirection must happen when
+        # 'a_bzrdir' is created (and is memorized) and the whole .bzr/
+        # must be present at that redirected address.
+        self.assertRaises(errors.NoWorkingTree,
+                          WorkingTreeFormat.find_format, a_bzrdir)
+
 
 class TestAuth(http_utils.TestCaseWithWebserver):
     """Test authentication scheme"""

=== modified file 'bzrlib/transport/__init__.py'
--- bzrlib/transport/__init__.py	2008-06-24 22:53:43 +0000
+++ bzrlib/transport/__init__.py	2008-07-17 07:03:56 +0000
@@ -53,6 +53,7 @@
         )
 from bzrlib.trace import (
     mutter,
+    note,
     )
 from bzrlib import registry
 
@@ -1631,6 +1632,37 @@
         # occurred).
         raise errors.TooManyRedirections
 
+def _do_following_redirections(action, transport):
+    """Execute an action with given transport following redirections.
+
+    This is a common use of do_catching_redirections which is why it
+    is provided here.
+
+    :return: a couple which first component is what 'action' returns
+    and second component is the possibly updated transport."""
+
+    def do_action(transport):
+        return action(transport), transport
+
+    def redirected(transport, e, redirection_notice):
+        qualified_source = e.get_source_url()
+        relpath = transport.relpath(qualified_source)
+        if not e.target.endswith(relpath):
+            # Not redirected to a branch-format, not a branch
+            raise errors.NotBranchError(path=e.target)
+        target = e.target[:-len(relpath)]
+        note('%s is%s redirected to %s', transport.base, e.permanently, target)
+        # Let's try with a new transport
+        # FIXME: If 'transport' has a qualifier, this should
+        # be applied again to the new transport *iff* the
+        # schemes used are the same. Uncomment this code
+        # once the function (and tests) exist.
+        # -- vila20070212
+        #target = urlutils.copy_url_qualifiers(original, target)
+        return get_transport(target)
+
+    return do_catching_redirections(do_action, transport, redirected)
+
 
 class Server(object):
     """A Transport Server.

=== modified file 'bzrlib/workingtree.py'
--- bzrlib/workingtree.py	2008-07-08 14:55:19 +0000
+++ bzrlib/workingtree.py	2008-07-18 12:45:02 +0000
@@ -77,7 +77,9 @@
     xml7,
     )
 import bzrlib.branch
-from bzrlib.transport import get_transport
+from bzrlib.transport import (
+    get_transport,
+    )
 import bzrlib.ui
 from bzrlib.workingtree_4 import WorkingTreeFormat4
 """)
@@ -2656,7 +2658,10 @@
             transport = a_bzrdir.get_workingtree_transport(None)
             format_string = transport.get("format").read()
             return klass._formats[format_string]
-        except errors.NoSuchFile:
+        except (errors.NoSuchFile, errors.RedirectRequested):
+            # We treat a redirection like an absence of file since a
+            # sucessful redirection was going to be remembered when
+            # a_bzrdir was created.
             raise errors.NoWorkingTree(base=transport.base)
         except KeyError:
             raise errors.UnknownFormatError(format=format_string,

# Begin bundle
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


More information about the bazaar mailing list