Merge lp:~mbp/bzr/687653-notbrancherror into lp:bzr/2.2

Proposed by Martin Pool on 2010-12-10
Status: Rejected
Rejected by: Martin Pool on 2010-12-16
Proposed branch: lp:~mbp/bzr/687653-notbrancherror
Merge into: lp:bzr/2.2
Diff against target: 259 lines (+44/-79)
4 files modified
NEWS (+7/-0)
bzrlib/branch.py (+19/-5)
bzrlib/errors.py (+16/-40)
bzrlib/tests/test_errors.py (+2/-34)
To merge this branch: bzr merge lp:~mbp/bzr/687653-notbrancherror
Reviewer Review Type Date Requested Status
bzr-core 2010-12-10 Pending
Review via email: mp+43314@code.launchpad.net

Description of the Change

As discussed in <https://bugs.launchpad.net/bzr/+bug/687653> I think doing (possibly network) IO from an error while it's formatted is a pretty bad idea, and likely to lead to cascading errors. Instead this checks for a repository at the time we fail to open the branch. It's possible this sometimes does too much work, but I think this shouldn't be a very common case, since it will only be hit when we already have found a bzrdir.

I'm targeting 2.2 so this can be rolled out on Launchpad.

This is a slight change in API behaviour because the bzrdir parameter is now ignored, but I think not really a break.

To post a comment you must log in.
lp:~mbp/bzr/687653-notbrancherror updated on 2010-12-10
5119. By Martin Pool on 2010-12-10

Import urlutils just once into errors.py

Andrew Bennetts (spiv) wrote :

I'm a bit worried about the performance impact. grepping for places that catch NotBranchError, and then eyeballing them to ignore places that are actually catching NotBranchError from opening a BzrDir rather than a branch, I see a fair few places that are likely to be incurring extra round-trips. The most serious is probably cmd_switch. Other places are less common and/or performance sensitive situations, like cmd_pack, cmd_whoami, BzrDir.sprout where the bzrdir has a repo but no branch.

I think all the HPSS effort tests still pass, so that's a good sign. But it definitely smells a bit, so we're trading one smell for another... probably an ok tradeoff, but it does make me hesitate.

On the plus side you can remove the str(e) call from SmartServerRequestOpenBranchV3.

If the test suite passes it's definitely safe for Launchpad to cherrypick.

Andrew Bennetts (spiv) wrote :

An alternative fix... just add:

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self.__dict__)

The problem is that repr ought to be safe (and thus simple), and currently it can trigger arbitrary work, which allows this bug to happen.

Code that calls str(e) will still get a string suitable for displaying to users — including the "is a repository" hint.

Martin Pool (mbp) wrote :

I'm going to reject this in favor of spiv's fix in 2.2. In 2.3, I'm inclined to think we should just rip it out; the ui benefit is small and doing things from an exception class is bad.

Unmerged revisions

5119. By Martin Pool on 2010-12-10

Import urlutils just once into errors.py

5118. By Martin Pool on 2010-12-10

Smooth off handling of details and bzrdir in NotBranchError and update tests

5117. By Martin Pool on 2010-12-10

Remove misleading unused variables

5116. By Martin Pool on 2010-12-10

Trim unused import

5115. By Martin Pool on 2010-12-10

Don't probe for a repository from within formatting of a `NotBranchError`,
because this can cause knock-on errors. Instead, in cases where it would be
useful to tell the users that a directory contains a repository but not a
branch, check for it when raising the error.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-12-02 09:29:41 +0000
3+++ NEWS 2010-12-10 03:22:11 +0000
4@@ -25,6 +25,13 @@
5 Bug Fixes
6 *********
7
8+* Don't probe for a repository from within formatting of a
9+ `NotBranchError`, because this can cause knock-on errors. Instead, in
10+ cases where it would be useful to tell the users that a directory
11+ contains a repository but not a branch, check for it when raising the
12+ error.
13+ (Martin Pool, 687653)
14+
15 * RevisionTree.is_executable no longer returns None for directories and
16 symlinks. Instead, it returns False, like other Trees and methods.
17 (Aaron Bentley, #681885)
18
19=== modified file 'bzrlib/branch.py'
20--- bzrlib/branch.py 2010-08-13 07:32:06 +0000
21+++ bzrlib/branch.py 2010-12-10 03:22:11 +0000
22@@ -35,7 +35,6 @@
23 rio,
24 symbol_versioning,
25 transport,
26- tsort,
27 ui,
28 urlutils,
29 )
30@@ -1555,7 +1554,15 @@
31 return format()
32 return format
33 except errors.NoSuchFile:
34- raise errors.NotBranchError(path=transport.base, bzrdir=a_bzrdir)
35+ try:
36+ # Give a clearer message if the bzrdir has a repository but no
37+ # branch: see bug 440952.
38+ a_bzrdir.open_repository()
39+ except errors.NoRepositoryPresent:
40+ detail = None
41+ else:
42+ detail = 'location is a repository'
43+ raise errors.NotBranchError(path=transport.base, detail=detail)
44 except KeyError:
45 raise errors.UnknownFormatError(format=format_string, kind='branch')
46
47@@ -2069,7 +2076,15 @@
48 _repository=a_bzrdir.find_repository(),
49 ignore_fallbacks=ignore_fallbacks)
50 except errors.NoSuchFile:
51- raise errors.NotBranchError(path=transport.base, bzrdir=a_bzrdir)
52+ try:
53+ # Give a clearer message if the bzrdir has a repository but no
54+ # branch: see bug 440952.
55+ a_bzrdir.open_repository()
56+ except errors.NoRepositoryPresent:
57+ detail = None
58+ else:
59+ detail = 'location is a repository'
60+ raise errors.NotBranchError(path=transport.base, detail=detail)
61
62 def __init__(self):
63 super(BranchFormatMetadir, self).__init__()
64@@ -2255,7 +2270,7 @@
65 def set_reference(self, a_bzrdir, name, to_branch):
66 """See BranchFormat.set_reference()."""
67 transport = a_bzrdir.get_branch_transport(None, name=name)
68- location = transport.put_bytes('location', to_branch.base)
69+ transport.put_bytes('location', to_branch.base)
70
71 def initialize(self, a_bzrdir, name=None, target_branch=None):
72 """Create a branch of this format in a_bzrdir."""
73@@ -2959,7 +2974,6 @@
74
75 def set_bound_location(self, location):
76 """See Branch.set_push_location."""
77- result = None
78 config = self.get_config()
79 if location is None:
80 if config.get_user_option('bound') != 'True':
81
82=== modified file 'bzrlib/errors.py'
83--- bzrlib/errors.py 2010-10-11 05:06:26 +0000
84+++ bzrlib/errors.py 2010-12-10 03:22:11 +0000
85@@ -20,6 +20,7 @@
86 from bzrlib import (
87 osutils,
88 symbol_versioning,
89+ urlutils,
90 )
91 from bzrlib.patches import (
92 MalformedHunkHeader,
93@@ -312,7 +313,6 @@
94 _fmt = 'There is no public branch set for "%(branch_url)s".'
95
96 def __init__(self, branch):
97- import bzrlib.urlutils as urlutils
98 public_location = urlutils.unescape_for_display(branch.base, 'ascii')
99 BzrError.__init__(self, branch_url=public_location)
100
101@@ -698,45 +698,27 @@
102
103
104 # TODO: This is given a URL; we try to unescape it but doing that from inside
105-# the exception object is a bit undesirable.
106-# TODO: Probably this behavior of should be a common superclass
107+# the exception object is a bit undesirable. We should instead either tell
108+# the exception the encoding we want it formatted into, or let the UIFactory
109+# perform the encoding (if any.)
110 class NotBranchError(PathError):
111
112 _fmt = 'Not a branch: "%(path)s"%(detail)s.'
113
114 def __init__(self, path, detail=None, bzrdir=None):
115- import bzrlib.urlutils as urlutils
116- path = urlutils.unescape_for_display(path, 'ascii')
117- if detail is not None:
118- detail = ': ' + detail
119- self.detail = detail
120- self.bzrdir = bzrdir
121- PathError.__init__(self, path=path)
122+ """Construct an error.
123
124- def _format(self):
125- # XXX: Ideally self.detail would be a property, but Exceptions in
126- # Python 2.4 have to be old-style classes so properties don't work.
127- # Instead we override _format.
128- if self.detail is None:
129- if self.bzrdir is not None:
130- try:
131- self.bzrdir.open_repository()
132- except NoRepositoryPresent:
133- self.detail = ''
134- except Exception:
135- # Just ignore unexpected errors. Raising arbitrary errors
136- # during str(err) can provoke strange bugs. Concretely
137- # Launchpad's codehosting managed to raise NotBranchError
138- # here, and then get stuck in an infinite loop/recursion
139- # trying to str() that error. All this error really cares
140- # about that there's no working repository there, and if
141- # open_repository() fails, there probably isn't.
142- self.detail = ''
143- else:
144- self.detail = ': location is a repository'
145- else:
146- self.detail = ''
147- return PathError._format(self)
148+ :param detail: Extra string to append to the message, explaining
149+ why it was not a branch.
150+ :param bzrdir: Deprecated/ignored.
151+ """
152+ path = urlutils.unescape_for_display(path, 'ascii')
153+ if detail is not None:
154+ self.detail = ': ' + detail
155+ else:
156+ self.detail = ''
157+ self.bzrdir = bzrdir
158+ PathError.__init__(self, path=path)
159
160
161 class NoSubmitBranch(PathError):
162@@ -744,7 +726,6 @@
163 _fmt = 'No submit branch available for branch "%(path)s"'
164
165 def __init__(self, branch):
166- import bzrlib.urlutils as urlutils
167 self.path = urlutils.unescape_for_display(branch.base, 'ascii')
168
169
170@@ -1218,7 +1199,6 @@
171 ' branch "%(location)s".')
172
173 def __init__(self, location):
174- import bzrlib.urlutils as urlutils
175 location = urlutils.unescape_for_display(location, 'ascii')
176 BzrError.__init__(self, location=location)
177
178@@ -2168,7 +2148,6 @@
179 '"%(revstring)s".'
180
181 def __init__(self, public_location, revstring):
182- import bzrlib.urlutils as urlutils
183 public_location = urlutils.unescape_for_display(public_location,
184 'ascii')
185 BzrError.__init__(self, public_location=public_location,
186@@ -2780,7 +2759,6 @@
187 class BzrDirError(BzrError):
188
189 def __init__(self, bzrdir):
190- import bzrlib.urlutils as urlutils
191 display_url = urlutils.unescape_for_display(bzrdir.user_url,
192 'ascii')
193 BzrError.__init__(self, bzrdir=bzrdir, display_url=display_url)
194@@ -2793,7 +2771,6 @@
195
196 def __init__(self, bzrdir, target_branch):
197 BzrDirError.__init__(self, bzrdir)
198- import bzrlib.urlutils as urlutils
199 self.target_url = urlutils.unescape_for_display(target_branch.base,
200 'ascii')
201
202@@ -2860,7 +2837,6 @@
203 more = ''
204 else:
205 more = ' ' + more
206- import bzrlib.urlutils as urlutils
207 display_url = urlutils.unescape_for_display(
208 tree.user_url, 'ascii')
209 BzrError.__init__(self, tree=tree, display_url=display_url, more=more)
210
211=== modified file 'bzrlib/tests/test_errors.py'
212--- bzrlib/tests/test_errors.py 2010-12-02 09:23:10 +0000
213+++ bzrlib/tests/test_errors.py 2010-12-10 03:22:11 +0000
214@@ -661,43 +661,11 @@
215 err = errors.NotBranchError('path')
216 self.assertEqual('Not a branch: "path".', str(err))
217
218- def test_not_branch_bzrdir_with_repo(self):
219- bzrdir = self.make_repository('repo').bzrdir
220- err = errors.NotBranchError('path', bzrdir=bzrdir)
221+ def test_not_branch_bzrdir_with_details(self):
222+ err = errors.NotBranchError('path', detail='location is a repository')
223 self.assertEqual(
224 'Not a branch: "path": location is a repository.', str(err))
225
226- def test_not_branch_bzrdir_without_repo(self):
227- bzrdir = self.make_bzrdir('bzrdir')
228- err = errors.NotBranchError('path', bzrdir=bzrdir)
229- self.assertEqual('Not a branch: "path".', str(err))
230-
231- def test_not_branch_bzrdir_with_recursive_not_branch_error(self):
232- class FakeBzrDir(object):
233- def open_repository(self):
234- # str() on the NotBranchError will trigger a call to this,
235- # which in turn will another, identical NotBranchError.
236- raise errors.NotBranchError('path', bzrdir=FakeBzrDir())
237- err = errors.NotBranchError('path', bzrdir=FakeBzrDir())
238- self.assertEqual('Not a branch: "path".', str(err))
239-
240- def test_not_branch_laziness(self):
241- real_bzrdir = self.make_bzrdir('path')
242- class FakeBzrDir(object):
243- def __init__(self):
244- self.calls = []
245- def open_repository(self):
246- self.calls.append('open_repository')
247- raise errors.NoRepositoryPresent(real_bzrdir)
248- fake_bzrdir = FakeBzrDir()
249- err = errors.NotBranchError('path', bzrdir=fake_bzrdir)
250- self.assertEqual([], fake_bzrdir.calls)
251- str(err)
252- self.assertEqual(['open_repository'], fake_bzrdir.calls)
253- # Stringifying twice doesn't try to open a repository twice.
254- str(err)
255- self.assertEqual(['open_repository'], fake_bzrdir.calls)
256-
257 def test_invalid_pattern(self):
258 error = errors.InvalidPattern('Bad pattern msg.')
259 self.assertEqualDiff("Invalid pattern(s) found. Bad pattern msg.",

Subscribers

People subscribed via source and target branches