Merge lp:~jcsackett/launchpad/branch-delete-api-702620 into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 12297
Proposed branch: lp:~jcsackett/launchpad/branch-delete-api-702620
Merge into: lp:launchpad
Diff against target: 136 lines (+72/-8)
4 files modified
lib/lp/code/errors.py (+3/-0)
lib/lp/code/model/branch.py (+7/-1)
lib/lp/code/tests/test_branch.py (+1/-7)
lib/lp/code/tests/test_branch_webservice.py (+61/-0)
To merge this branch: bzr merge lp:~jcsackett/launchpad/branch-delete-api-702620
Reviewer Review Type Date Requested Status
Leonard Richardson (community) Approve
Review via email: mp+47449@code.launchpad.net

Commit message

[r=leonardr][bug=702620] Expose CannotDeleteBranch exception for the webservice on DELETE

Description of the change

Summary
=======

Calling a DELETE across the webservice on a branch leads to an OOPS when the branch has linked artifacts (e.g. stacked branches) that cause CannotDeleteBranch to be raised. CannotDeleteBranch should be raised over the webservice instead of the server error.

Preimplementation
=================

Spoke with Curtis Hovey about the reasons for the DELETE failing, and if we should have the DELETE just whack the linked objects. Not having the DELETE hit the linked objects (and providing no way to do so) was a conscious decision, so just exposing the error is correct.

Spoke with Leonard Richardson about various oddities in exposing this particular error, as the exception is raised in such a way that just adding webservice_error was not sufficient.

Proposed Fix
============

Add webservice_error to CannotDeleteBranch.

Implementation
==============

lib/lp/code/errors.py
---------------------

Add webservice_error to CannotDeleteBranch

lib/lp/code/model/branch.py
---------------------------

In deleteSelfBreakReferences, catch the CannotDeleteBranch exception and re-raise with expose.

lib/lp/code/tests/test_branch.py
--------------------------------

Drive by fix; unittest and testsuite boilerplate removed, b/c it's no longer needed.

lib/lp/code/tests/test_branch_webservice.py
-------------------------------------------

Test case added for branch deletion over webservice.

Tests
=====

bin/test -t TestBranchDeletes

Demo & QA
=========

Attempt to delete a branch with branches stacked on it via launchpadlib. You should get a 400, rather than a 500 error.

Lint
====

make lint output:

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/errors.py
  lib/lp/code/model/branch.py
  lib/lp/code/tests/test_branch.py
  lib/lp/code/tests/test_branch_webservice.py

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

So this will solve 1/2 the bug: it won't oops. However it won't let
scripts that want to delete branches do so - some of the artifacts
cannot be deleted from the web API or UI except by deleting the branch
in the web UI.

I realise it was a conscious decision, but what was the rationale? Why
does it still make sense?

-Rob

Revision history for this message
Leonard Richardson (leonardr) :
review: Approve
Revision history for this message
j.c.sackett (jcsackett) wrote :

> So this will solve 1/2 the bug: it won't oops. However it won't let
> scripts that want to delete branches do so - some of the artifacts
> cannot be deleted from the web API or UI except by deleting the branch
> in the web UI.

Are there artifacts that can't be deleted? Which ones? The bug states that it all can be deleted manually, it's just a bit of a pain.

If there are things that can't be deleted, perhaps the appropriate step is to change the webservice so those artifacts can be deleted.

> I realise it was a conscious decision, but what was the rationale? Why
> does it still make sense?

As I understand it, it's because we don't want want someone to delete stacked_on branches &c without being warned, and there's no sane way to provide a method in the service that warns and requires confirmation. We could add a prompt with input() or something, but that strikes me as faintly ridiculous at best.

Revision history for this message
Leonard Richardson (leonardr) wrote :

This is very blue-sky, but one thing I've considered in this case is responding to a DELETE request by sending back a list of the documents that would have to be deleted before this document can be deleted. On the client side you could then prompt the user "deleting this object will delete 100 other objects", or go ahead and recursively delete the other documents, or decide not to delete that document after all.

It's difficult to come up with a general solution beyond adding the equivalent of "rm -rf", and I'm open to just making that possible.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/errors.py'
2--- lib/lp/code/errors.py 2011-01-19 00:10:48 +0000
3+++ lib/lp/code/errors.py 2011-02-01 15:31:14 +0000
4@@ -40,6 +40,8 @@
5 'WrongBranchMergeProposal',
6 ]
7
8+import httplib
9+
10 from lazr.restful.declarations import webservice_error
11
12 from lp.app.errors import NameLookupFailed
13@@ -87,6 +89,7 @@
14
15 class CannotDeleteBranch(Exception):
16 """The branch cannot be deleted at this time."""
17+ webservice_error(httplib.BAD_REQUEST)
18
19
20 class BranchCreationForbidden(BranchCreationException):
21
22=== modified file 'lib/lp/code/model/branch.py'
23--- lib/lp/code/model/branch.py 2011-01-20 19:07:26 +0000
24+++ lib/lp/code/model/branch.py 2011-02-01 15:31:14 +0000
25@@ -15,6 +15,7 @@
26
27 from bzrlib import urlutils
28 from bzrlib.revision import NULL_REVISION
29+from lazr.restful.error import expose
30 import pytz
31 from sqlobject import (
32 BoolCol,
33@@ -1025,7 +1026,12 @@
34
35 def destroySelfBreakReferences(self):
36 """See `IBranch`."""
37- return self.destroySelf(break_references=True)
38+ try:
39+ return self.destroySelf(break_references=True)
40+ except CannotDeleteBranch, e:
41+ # Reraise and expose exception here so that the webservice_error
42+ # is propogated.
43+ raise expose(CannotDeleteBranch(e.message))
44
45 def _deleteBranchSubscriptions(self):
46 """Delete subscriptions for this branch prior to deleting branch."""
47
48=== modified file 'lib/lp/code/tests/test_branch.py'
49--- lib/lp/code/tests/test_branch.py 2010-10-04 19:50:45 +0000
50+++ lib/lp/code/tests/test_branch.py 2011-02-01 15:31:14 +0000
51@@ -1,10 +1,8 @@
52-# Copyright 2009 Canonical Ltd. This software is licensed under the
53+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
54 # GNU Affero General Public License version 3 (see the file LICENSE).
55
56 """Unit tests for methods of Branch and BranchSet."""
57
58-import unittest
59-
60 from zope.component import getUtility
61 from zope.security.proxy import removeSecurityProxy
62
63@@ -377,7 +375,3 @@
64 # not work for private branches.
65 branch = self.factory.makeAnyBranch()
66 self.assertRaises(AssertionError, branch.composePublicURL, 'https')
67-
68-
69-def test_suite():
70- return unittest.TestLoader().loadTestsFromName(__name__)
71
72=== added file 'lib/lp/code/tests/test_branch_webservice.py'
73--- lib/lp/code/tests/test_branch_webservice.py 1970-01-01 00:00:00 +0000
74+++ lib/lp/code/tests/test_branch_webservice.py 2011-02-01 15:31:14 +0000
75@@ -0,0 +1,61 @@
76+# Copyright 2011 Canonical Ltd. This software is licensed under the
77+# GNU Affero General Public License version 3 (see the file LICENSE).
78+
79+__metaclass__ = type
80+
81+import httplib
82+
83+from zope.component import getUtility
84+
85+from lazr.restfulclient.errors import HTTPError
86+
87+from canonical.testing.layers import DatabaseFunctionalLayer
88+from lp.code.interfaces.branch import IBranchSet
89+from lp.testing import (
90+ launchpadlib_for,
91+ login_person,
92+ logout,
93+ TestCaseWithFactory,
94+ )
95+
96+
97+class TestBranchDeletes(TestCaseWithFactory):
98+
99+ layer = DatabaseFunctionalLayer
100+
101+ def setUp(self):
102+ super(TestBranchDeletes, self).setUp()
103+ self.branch_owner = self.factory.makePerson(name='jimhenson')
104+ self.branch = self.factory.makeBranch(
105+ owner=self.branch_owner,
106+ product=self.factory.makeProduct(name='fraggle'),
107+ name='rock')
108+ self.lp = launchpadlib_for("test", self.branch.owner.name)
109+
110+ def test_delete_branch_without_artifacts(self):
111+ # A branch unencumbered by links or stacked branches deletes.
112+ target_branch = self.lp.branches.getByUniqueName(
113+ unique_name='~jimhenson/fraggle/rock')
114+ target_branch.lp_delete()
115+
116+ login_person(self.branch_owner)
117+ branch_set = getUtility(IBranchSet)
118+ self.assertIs(
119+ None,
120+ branch_set.getByUniqueName('~jimhenson/fraggle/rock'))
121+
122+ def test_delete_branch_with_stacked_branch_errors(self):
123+ # When trying to delete a branch that cannot be deleted, the
124+ # error is raised across the webservice instead of oopsing.
125+ login_person(self.branch_owner)
126+ stacked_branch = self.factory.makeBranch(
127+ stacked_on=self.branch,
128+ owner=self.branch_owner)
129+ logout()
130+ target_branch = self.lp.branches.getByUniqueName(
131+ unique_name='~jimhenson/fraggle/rock')
132+ api_error = self.assertRaises(
133+ HTTPError,
134+ target_branch.lp_delete)
135+ self.assertIn('Cannot delete', api_error.content)
136+ self.assertEqual(httplib.BAD_REQUEST, api_error.response.status)