Merge lp:~cjwatson/launchpad/better-bzr-mp-diffs into lp:launchpad
- better-bzr-mp-diffs
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 18707 |
Proposed branch: | lp:~cjwatson/launchpad/better-bzr-mp-diffs |
Merge into: | lp:launchpad |
Prerequisite: | lp:~cjwatson/launchpad/branch-hosting-client |
Diff against target: |
506 lines (+230/-34) 11 files modified
lib/lp/code/browser/branch.py (+48/-0) lib/lp/code/browser/branchmergeproposal.py (+9/-12) lib/lp/code/browser/configure.zcml (+5/-1) lib/lp/code/browser/gitrepository.py (+12/-10) lib/lp/code/browser/tests/test_branch.py (+72/-0) lib/lp/code/browser/tests/test_branchmergeproposal.py (+19/-2) lib/lp/code/browser/tests/test_gitrepository.py (+18/-2) lib/lp/code/interfaces/branch.py (+10/-1) lib/lp/code/model/branch.py (+6/-0) lib/lp/code/tests/helpers.py (+16/-1) lib/lp/services/webapp/publisher.py (+15/-5) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/better-bzr-mp-diffs |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant (community) | code | Approve | |
Review via email: mp+345705@code.launchpad.net |
Commit message
Proxy loggerhead branch diffs through the webapp, allowing AJAX MP revision diffs to work for private branches.
Description of the change
This is the same approach as we already use for Git, built on top of my recent work to expose a suitable loggerhead API on a private port.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/browser/branch.py' | |||
2 | --- lib/lp/code/browser/branch.py 2018-03-16 21:20:00 +0000 | |||
3 | +++ lib/lp/code/browser/branch.py 2018-06-22 10:03:28 +0000 | |||
4 | @@ -46,6 +46,7 @@ | |||
5 | 46 | providedBy, | 46 | providedBy, |
6 | 47 | ) | 47 | ) |
7 | 48 | from zope.publisher.interfaces import NotFound | 48 | from zope.publisher.interfaces import NotFound |
8 | 49 | from zope.publisher.interfaces.browser import IBrowserPublisher | ||
9 | 49 | from zope.schema import ( | 50 | from zope.schema import ( |
10 | 50 | Bool, | 51 | Bool, |
11 | 51 | Choice, | 52 | Choice, |
12 | @@ -136,6 +137,7 @@ | |||
13 | 136 | from lp.services.webapp.breadcrumb import NameBreadcrumb | 137 | from lp.services.webapp.breadcrumb import NameBreadcrumb |
14 | 137 | from lp.services.webapp.escaping import structured | 138 | from lp.services.webapp.escaping import structured |
15 | 138 | from lp.services.webapp.interfaces import ICanonicalUrlData | 139 | from lp.services.webapp.interfaces import ICanonicalUrlData |
16 | 140 | from lp.services.webapp.publisher import DataDownloadView | ||
17 | 139 | from lp.services.webhooks.browser import WebhookTargetNavigationMixin | 141 | from lp.services.webhooks.browser import WebhookTargetNavigationMixin |
18 | 140 | from lp.snappy.browser.hassnaps import ( | 142 | from lp.snappy.browser.hassnaps import ( |
19 | 141 | HasSnapsMenuMixin, | 143 | HasSnapsMenuMixin, |
20 | @@ -203,6 +205,22 @@ | |||
21 | 203 | return None | 205 | return None |
22 | 204 | return self.context.getMergeProposalByID(id) | 206 | return self.context.getMergeProposalByID(id) |
23 | 205 | 207 | ||
24 | 208 | @stepto("+diff") | ||
25 | 209 | def traverse_diff(self): | ||
26 | 210 | segments = list(self.request.getTraversalStack()) | ||
27 | 211 | if len(segments) == 1: | ||
28 | 212 | new = segments.pop() | ||
29 | 213 | old = None | ||
30 | 214 | self.request.stepstogo.consume() | ||
31 | 215 | elif len(segments) == 2: | ||
32 | 216 | new = segments.pop() | ||
33 | 217 | old = segments.pop() | ||
34 | 218 | self.request.stepstogo.consume() | ||
35 | 219 | self.request.stepstogo.consume() | ||
36 | 220 | else: | ||
37 | 221 | return None | ||
38 | 222 | return BranchDiffView(self.context, self.request, new, old=old) | ||
39 | 223 | |||
40 | 206 | @stepto("+code-import") | 224 | @stepto("+code-import") |
41 | 207 | def traverse_code_import(self): | 225 | def traverse_code_import(self): |
42 | 208 | """Traverses to the `ICodeImport` for the branch.""" | 226 | """Traverses to the `ICodeImport` for the branch.""" |
43 | @@ -1280,3 +1298,33 @@ | |||
44 | 1280 | 'target_branch', | 1298 | 'target_branch', |
45 | 1281 | "This branch is not mergeable into %s." % | 1299 | "This branch is not mergeable into %s." % |
46 | 1282 | target_branch.bzr_identity) | 1300 | target_branch.bzr_identity) |
47 | 1301 | |||
48 | 1302 | |||
49 | 1303 | @implementer(IBrowserPublisher) | ||
50 | 1304 | class BranchDiffView(DataDownloadView): | ||
51 | 1305 | |||
52 | 1306 | content_type = "text/x-patch" | ||
53 | 1307 | |||
54 | 1308 | def __init__(self, context, request, new, old=None): | ||
55 | 1309 | super(BranchDiffView, self).__init__(context, request) | ||
56 | 1310 | self.new = new | ||
57 | 1311 | self.old = old | ||
58 | 1312 | |||
59 | 1313 | def __call__(self): | ||
60 | 1314 | if getFeatureFlag(u"code.bzr.diff.disable_proxy"): | ||
61 | 1315 | self.request.response.setStatus(401) | ||
62 | 1316 | return "Proxying of branch diffs is disabled.\n" | ||
63 | 1317 | return super(BranchDiffView, self).__call__() | ||
64 | 1318 | |||
65 | 1319 | @property | ||
66 | 1320 | def filename(self): | ||
67 | 1321 | if self.old is None: | ||
68 | 1322 | return "%s_%s.diff" % (self.context.name, self.new) | ||
69 | 1323 | else: | ||
70 | 1324 | return "%s_%s_%s.diff" % (self.context.name, self.old, self.new) | ||
71 | 1325 | |||
72 | 1326 | def getBody(self): | ||
73 | 1327 | return self.context.getDiff(self.new, old=self.old) | ||
74 | 1328 | |||
75 | 1329 | def browserDefault(self, request): | ||
76 | 1330 | return self, () | ||
77 | 1283 | 1331 | ||
78 | === modified file 'lib/lp/code/browser/branchmergeproposal.py' | |||
79 | --- lib/lp/code/browser/branchmergeproposal.py 2018-04-25 12:01:33 +0000 | |||
80 | +++ lib/lp/code/browser/branchmergeproposal.py 2018-06-22 10:03:28 +0000 | |||
81 | @@ -617,19 +617,16 @@ | |||
82 | 617 | super(BranchMergeProposalView, self).initialize() | 617 | super(BranchMergeProposalView, self).initialize() |
83 | 618 | cache = IJSONRequestCache(self.request) | 618 | cache = IJSONRequestCache(self.request) |
84 | 619 | cache.objects['branch_name'] = self.context.merge_source.name | 619 | cache.objects['branch_name'] = self.context.merge_source.name |
92 | 620 | if IBranch.providedBy(self.context.merge_source): | 620 | if (IBranch.providedBy(self.context.merge_source) and |
93 | 621 | cache.objects.update({ | 621 | getFeatureFlag("code.bzr.diff.disable_proxy")): |
94 | 622 | 'branch_diff_link': | 622 | # This fallback works for public branches, but not private ones. |
95 | 623 | 'https://%s/+loggerhead/%s/diff/' % ( | 623 | cache.objects['branch_diff_link'] = ( |
96 | 624 | config.launchpad.code_domain, | 624 | 'https://%s/+loggerhead/%s/diff/' % ( |
97 | 625 | self.context.source_branch.unique_name), | 625 | config.launchpad.code_domain, |
98 | 626 | }) | 626 | self.context.source_branch.unique_name)) |
99 | 627 | else: | 627 | else: |
105 | 628 | cache.objects.update({ | 628 | cache.objects['branch_diff_link'] = ( |
106 | 629 | 'branch_diff_link': | 629 | canonical_url(self.context.parent) + '/+diff/') |
102 | 630 | canonical_url(self.context.source_git_repository) + | ||
103 | 631 | '/+diff/', | ||
104 | 632 | }) | ||
107 | 633 | if getFeatureFlag("longpoll.merge_proposals.enabled"): | 630 | if getFeatureFlag("longpoll.merge_proposals.enabled"): |
108 | 634 | cache.objects['merge_proposal_event_key'] = subscribe( | 631 | cache.objects['merge_proposal_event_key'] = subscribe( |
109 | 635 | self.context).event_key | 632 | self.context).event_key |
110 | 636 | 633 | ||
111 | === modified file 'lib/lp/code/browser/configure.zcml' | |||
112 | --- lib/lp/code/browser/configure.zcml 2017-09-27 13:50:02 +0000 | |||
113 | +++ lib/lp/code/browser/configure.zcml 2018-06-22 10:03:28 +0000 | |||
114 | @@ -1,4 +1,4 @@ | |||
116 | 1 | <!-- Copyright 2009-2017 Canonical Ltd. This software is licensed under the | 1 | <!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
117 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | 2 | GNU Affero General Public License version 3 (see the file LICENSE). |
118 | 3 | --> | 3 | --> |
119 | 4 | 4 | ||
120 | @@ -442,6 +442,10 @@ | |||
121 | 442 | class="lp.code.browser.codeimport.CodeImportEditView" | 442 | class="lp.code.browser.codeimport.CodeImportEditView" |
122 | 443 | permission="launchpad.Edit" | 443 | permission="launchpad.Edit" |
123 | 444 | template="../../app/templates/generic-edit.pt"/> | 444 | template="../../app/templates/generic-edit.pt"/> |
124 | 445 | <class class="lp.code.browser.branch.BranchDiffView"> | ||
125 | 446 | <allow interface="zope.publisher.interfaces.browser.IBrowserPublisher" | ||
126 | 447 | attributes="__call__"/> | ||
127 | 448 | </class> | ||
128 | 445 | <browser:page | 449 | <browser:page |
129 | 446 | name="+delete" | 450 | name="+delete" |
130 | 447 | for="lp.code.interfaces.branch.IBranch" | 451 | for="lp.code.interfaces.branch.IBranch" |
131 | 448 | 452 | ||
132 | === modified file 'lib/lp/code/browser/gitrepository.py' | |||
133 | --- lib/lp/code/browser/gitrepository.py 2016-10-15 01:14:32 +0000 | |||
134 | +++ lib/lp/code/browser/gitrepository.py 2018-06-22 10:03:28 +0000 | |||
135 | @@ -97,6 +97,7 @@ | |||
136 | 97 | from lp.services.webapp.breadcrumb import Breadcrumb | 97 | from lp.services.webapp.breadcrumb import Breadcrumb |
137 | 98 | from lp.services.webapp.escaping import structured | 98 | from lp.services.webapp.escaping import structured |
138 | 99 | from lp.services.webapp.interfaces import ICanonicalUrlData | 99 | from lp.services.webapp.interfaces import ICanonicalUrlData |
139 | 100 | from lp.services.webapp.publisher import DataDownloadView | ||
140 | 100 | from lp.services.webhooks.browser import WebhookTargetNavigationMixin | 101 | from lp.services.webhooks.browser import WebhookTargetNavigationMixin |
141 | 101 | from lp.snappy.browser.hassnaps import HasSnapsViewMixin | 102 | from lp.snappy.browser.hassnaps import HasSnapsViewMixin |
142 | 102 | 103 | ||
143 | @@ -638,23 +639,24 @@ | |||
144 | 638 | 639 | ||
145 | 639 | 640 | ||
146 | 640 | @implementer(IBrowserPublisher) | 641 | @implementer(IBrowserPublisher) |
148 | 641 | class GitRepositoryDiffView: | 642 | class GitRepositoryDiffView(DataDownloadView): |
149 | 643 | |||
150 | 644 | content_type = "text/x-patch" | ||
151 | 645 | charset = "UTF-8" | ||
152 | 642 | 646 | ||
153 | 643 | def __init__(self, context, request, old, new): | 647 | def __init__(self, context, request, old, new): |
154 | 648 | super(GitRepositoryDiffView, self).__init__(context, request) | ||
155 | 644 | self.context = context | 649 | self.context = context |
156 | 645 | self.request = request | 650 | self.request = request |
157 | 646 | self.old = old | 651 | self.old = old |
158 | 647 | self.new = new | 652 | self.new = new |
159 | 648 | 653 | ||
169 | 649 | def __call__(self): | 654 | @property |
170 | 650 | content = self.context.getDiff(self.old, self.new) | 655 | def filename(self): |
171 | 651 | filename = "%s_%s.diff" % (self.old, self.new) | 656 | return "%s_%s_%s.diff" % (self.context.name, self.old, self.new) |
172 | 652 | self.request.response.setHeader( | 657 | |
173 | 653 | "Content-Type", 'text/x-patch; charset="UTF-8"') | 658 | def getBody(self): |
174 | 654 | self.request.response.setHeader("Content-Length", str(len(content))) | 659 | return self.context.getDiff(self.old, self.new) |
166 | 655 | self.request.response.setHeader( | ||
167 | 656 | "Content-Disposition", "attachment; filename=%s" % filename) | ||
168 | 657 | return content | ||
175 | 658 | 660 | ||
176 | 659 | def browserDefault(self, request): | 661 | def browserDefault(self, request): |
177 | 660 | return self, () | 662 | return self, () |
178 | 661 | 663 | ||
179 | === modified file 'lib/lp/code/browser/tests/test_branch.py' | |||
180 | --- lib/lp/code/browser/tests/test_branch.py 2018-02-01 18:38:24 +0000 | |||
181 | +++ lib/lp/code/browser/tests/test_branch.py 2018-06-22 10:03:28 +0000 | |||
182 | @@ -12,10 +12,12 @@ | |||
183 | 12 | 12 | ||
184 | 13 | from fixtures import FakeLogger | 13 | from fixtures import FakeLogger |
185 | 14 | import pytz | 14 | import pytz |
186 | 15 | from six.moves.urllib_error import HTTPError | ||
187 | 15 | from storm.store import Store | 16 | from storm.store import Store |
188 | 16 | from testtools.matchers import Equals | 17 | from testtools.matchers import Equals |
189 | 17 | from zope.component import getUtility | 18 | from zope.component import getUtility |
190 | 18 | from zope.publisher.interfaces import NotFound | 19 | from zope.publisher.interfaces import NotFound |
191 | 20 | from zope.security.interfaces import Unauthorized | ||
192 | 19 | from zope.security.proxy import removeSecurityProxy | 21 | from zope.security.proxy import removeSecurityProxy |
193 | 20 | 22 | ||
194 | 21 | from lp.app.enums import InformationType | 23 | from lp.app.enums import InformationType |
195 | @@ -32,12 +34,14 @@ | |||
196 | 32 | RepositoryFormat, | 34 | RepositoryFormat, |
197 | 33 | ) | 35 | ) |
198 | 34 | from lp.code.enums import BranchType | 36 | from lp.code.enums import BranchType |
199 | 37 | from lp.code.tests.helpers import BranchHostingFixture | ||
200 | 35 | from lp.registry.enums import BranchSharingPolicy | 38 | from lp.registry.enums import BranchSharingPolicy |
201 | 36 | from lp.registry.interfaces.accesspolicy import IAccessPolicySource | 39 | from lp.registry.interfaces.accesspolicy import IAccessPolicySource |
202 | 37 | from lp.registry.interfaces.person import PersonVisibility | 40 | from lp.registry.interfaces.person import PersonVisibility |
203 | 38 | from lp.services.beautifulsoup import BeautifulSoup | 41 | from lp.services.beautifulsoup import BeautifulSoup |
204 | 39 | from lp.services.config import config | 42 | from lp.services.config import config |
205 | 40 | from lp.services.database.constants import UTC_NOW | 43 | from lp.services.database.constants import UTC_NOW |
206 | 44 | from lp.services.features.testing import FeatureFixture | ||
207 | 41 | from lp.services.helpers import truncate_text | 45 | from lp.services.helpers import truncate_text |
208 | 42 | from lp.services.webapp.publisher import canonical_url | 46 | from lp.services.webapp.publisher import canonical_url |
209 | 43 | from lp.services.webapp.servers import LaunchpadTestRequest | 47 | from lp.services.webapp.servers import LaunchpadTestRequest |
210 | @@ -1220,3 +1224,71 @@ | |||
211 | 1220 | InformationType.USERDATA.description, description.renderContents()) | 1224 | InformationType.USERDATA.description, description.renderContents()) |
212 | 1221 | self.assertIsNotNone( | 1225 | self.assertIsNotNone( |
213 | 1222 | soup.find('a', id='privacy-link', attrs={'href': edit_url})) | 1226 | soup.find('a', id='privacy-link', attrs={'href': edit_url})) |
214 | 1227 | |||
215 | 1228 | |||
216 | 1229 | class TestBranchDiffView(BrowserTestCase): | ||
217 | 1230 | |||
218 | 1231 | layer = DatabaseFunctionalLayer | ||
219 | 1232 | |||
220 | 1233 | def test_feature_disabled(self): | ||
221 | 1234 | self.useFixture(FeatureFixture({"code.bzr.diff.disable_proxy": "on"})) | ||
222 | 1235 | hosting_fixture = self.useFixture(BranchHostingFixture()) | ||
223 | 1236 | person = self.factory.makePerson() | ||
224 | 1237 | branch = self.factory.makeBranch(owner=person) | ||
225 | 1238 | e = self.assertRaises( | ||
226 | 1239 | HTTPError, self.getUserBrowser, | ||
227 | 1240 | canonical_url(branch) + "/+diff/2/1") | ||
228 | 1241 | self.assertEqual(401, e.code) | ||
229 | 1242 | self.assertEqual("Proxying of branch diffs is disabled.\n", e.read()) | ||
230 | 1243 | self.assertEqual([], hosting_fixture.getDiff.calls) | ||
231 | 1244 | |||
232 | 1245 | def test_render(self): | ||
233 | 1246 | diff = b"A fake diff\n" | ||
234 | 1247 | hosting_fixture = self.useFixture(BranchHostingFixture(diff=diff)) | ||
235 | 1248 | person = self.factory.makePerson() | ||
236 | 1249 | branch = self.factory.makeBranch(owner=person, name="some-branch") | ||
237 | 1250 | browser = self.getUserBrowser( | ||
238 | 1251 | canonical_url(branch) + "/+diff/2/1") | ||
239 | 1252 | with person_logged_in(person): | ||
240 | 1253 | self.assertEqual( | ||
241 | 1254 | [((branch.id, "2"), {"old": "1"})], | ||
242 | 1255 | hosting_fixture.getDiff.calls) | ||
243 | 1256 | self.assertEqual("text/x-patch", browser.headers["Content-Type"]) | ||
244 | 1257 | self.assertEqual(str(len(diff)), browser.headers["Content-Length"]) | ||
245 | 1258 | self.assertEqual( | ||
246 | 1259 | 'attachment; filename="some-branch_1_2.diff"', | ||
247 | 1260 | browser.headers["Content-Disposition"]) | ||
248 | 1261 | self.assertEqual(diff, browser.contents) | ||
249 | 1262 | |||
250 | 1263 | def test_security(self): | ||
251 | 1264 | # A user who can see a private branch can fetch diffs from it, but | ||
252 | 1265 | # other users cannot. | ||
253 | 1266 | diff = b"A fake diff\n" | ||
254 | 1267 | self.useFixture(BranchHostingFixture(diff=diff)) | ||
255 | 1268 | person = self.factory.makePerson() | ||
256 | 1269 | project = self.factory.makeProduct( | ||
257 | 1270 | owner=person, information_type=InformationType.PROPRIETARY) | ||
258 | 1271 | with person_logged_in(person): | ||
259 | 1272 | branch = self.factory.makeBranch( | ||
260 | 1273 | owner=person, product=project, | ||
261 | 1274 | information_type=InformationType.PROPRIETARY) | ||
262 | 1275 | branch_url = canonical_url(branch) | ||
263 | 1276 | browser = self.getUserBrowser(branch_url + "/+diff/2/1", user=person) | ||
264 | 1277 | self.assertEqual(diff, browser.contents) | ||
265 | 1278 | self.useFixture(FakeLogger()) | ||
266 | 1279 | self.assertRaises( | ||
267 | 1280 | Unauthorized, self.getUserBrowser, branch_url + "/+diff/2/1") | ||
268 | 1281 | |||
269 | 1282 | def test_filename_quoting(self): | ||
270 | 1283 | # If we construct revisions containing metacharacters and somehow | ||
271 | 1284 | # manage to get that past the hosting service, the Content-Disposition | ||
272 | 1285 | # header is quoted properly. | ||
273 | 1286 | diff = b"A fake diff\n" | ||
274 | 1287 | self.useFixture(BranchHostingFixture(diff=diff)) | ||
275 | 1288 | person = self.factory.makePerson() | ||
276 | 1289 | branch = self.factory.makeBranch(owner=person, name="some-branch") | ||
277 | 1290 | browser = self.getUserBrowser( | ||
278 | 1291 | canonical_url(branch) + '/+diff/foo"/"bar') | ||
279 | 1292 | self.assertEqual( | ||
280 | 1293 | r'attachment; filename="some-branch_\"bar_foo\".diff"', | ||
281 | 1294 | browser.headers["Content-Disposition"]) | ||
282 | 1223 | 1295 | ||
283 | === modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py' | |||
284 | --- lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-04-25 12:01:33 +0000 | |||
285 | +++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-06-22 10:03:28 +0000 | |||
286 | @@ -89,6 +89,7 @@ | |||
287 | 89 | PersonVisibility, | 89 | PersonVisibility, |
288 | 90 | TeamMembershipPolicy, | 90 | TeamMembershipPolicy, |
289 | 91 | ) | 91 | ) |
290 | 92 | from lp.services.features.testing import FeatureFixture | ||
291 | 92 | from lp.services.librarian.interfaces.client import LibrarianServerError | 93 | from lp.services.librarian.interfaces.client import LibrarianServerError |
292 | 93 | from lp.services.messages.model.message import MessageSet | 94 | from lp.services.messages.model.message import MessageSet |
293 | 94 | from lp.services.timeout import TimeoutError | 95 | from lp.services.timeout import TimeoutError |
294 | @@ -1634,8 +1635,24 @@ | |||
295 | 1634 | "on 2015-01-01" % sha1, extract_text(tag)) | 1635 | "on 2015-01-01" % sha1, extract_text(tag)) |
296 | 1635 | 1636 | ||
297 | 1636 | def test_client_cache_bzr(self): | 1637 | def test_client_cache_bzr(self): |
300 | 1637 | # For Bazaar, the client cache contains the branch name and a | 1638 | # For Bazaar, the client cache contains the branch name and a link |
301 | 1638 | # loggerhead-based diff link. | 1639 | # to the Loggerhead diff via the webapp. |
302 | 1640 | bmp = self.factory.makeBranchMergeProposal() | ||
303 | 1641 | view = create_initialized_view(bmp, '+index') | ||
304 | 1642 | cache = IJSONRequestCache(view.request) | ||
305 | 1643 | self.assertThat(cache.objects, ContainsDict({ | ||
306 | 1644 | 'branch_name': Equals(bmp.source_branch.name), | ||
307 | 1645 | 'branch_diff_link': Equals( | ||
308 | 1646 | 'http://code.launchpad.dev/%s/+diff/' % | ||
309 | 1647 | bmp.source_branch.unique_name), | ||
310 | 1648 | })) | ||
311 | 1649 | |||
312 | 1650 | def test_client_cache_bzr_proxy_feature_disabled(self): | ||
313 | 1651 | # For Bazaar, a feature flag can be set to cause the client cache to | ||
314 | 1652 | # contain a direct link to the Loggerhead diff rather than via the | ||
315 | 1653 | # webapp. (This only works for public branches.) | ||
316 | 1654 | self.useFixture( | ||
317 | 1655 | FeatureFixture({u'code.bzr.diff.disable_proxy': u'on'})) | ||
318 | 1639 | bmp = self.factory.makeBranchMergeProposal() | 1656 | bmp = self.factory.makeBranchMergeProposal() |
319 | 1640 | view = create_initialized_view(bmp, '+index') | 1657 | view = create_initialized_view(bmp, '+index') |
320 | 1641 | cache = IJSONRequestCache(view.request) | 1658 | cache = IJSONRequestCache(view.request) |
321 | 1642 | 1659 | ||
322 | === modified file 'lib/lp/code/browser/tests/test_gitrepository.py' | |||
323 | --- lib/lp/code/browser/tests/test_gitrepository.py 2018-02-01 18:38:24 +0000 | |||
324 | +++ lib/lp/code/browser/tests/test_gitrepository.py 2018-06-22 10:03:28 +0000 | |||
325 | @@ -875,7 +875,8 @@ | |||
326 | 875 | hosting_fixture = self.useFixture(GitHostingFixture( | 875 | hosting_fixture = self.useFixture(GitHostingFixture( |
327 | 876 | diff={"patch": diff})) | 876 | diff={"patch": diff})) |
328 | 877 | person = self.factory.makePerson() | 877 | person = self.factory.makePerson() |
330 | 878 | repository = self.factory.makeGitRepository(owner=person) | 878 | repository = self.factory.makeGitRepository( |
331 | 879 | owner=person, name="some-repository") | ||
332 | 879 | browser = self.getUserBrowser( | 880 | browser = self.getUserBrowser( |
333 | 880 | canonical_url(repository) + "/+diff/0123456/0123456^") | 881 | canonical_url(repository) + "/+diff/0123456/0123456^") |
334 | 881 | with person_logged_in(person): | 882 | with person_logged_in(person): |
335 | @@ -886,7 +887,7 @@ | |||
336 | 886 | 'text/x-patch;charset=UTF-8', browser.headers["Content-Type"]) | 887 | 'text/x-patch;charset=UTF-8', browser.headers["Content-Type"]) |
337 | 887 | self.assertEqual(str(len(diff)), browser.headers["Content-Length"]) | 888 | self.assertEqual(str(len(diff)), browser.headers["Content-Length"]) |
338 | 888 | self.assertEqual( | 889 | self.assertEqual( |
340 | 889 | "attachment; filename=0123456^_0123456.diff", | 890 | 'attachment; filename="some-repository_0123456^_0123456.diff"', |
341 | 890 | browser.headers["Content-Disposition"]) | 891 | browser.headers["Content-Disposition"]) |
342 | 891 | self.assertEqual(diff, browser.contents) | 892 | self.assertEqual(diff, browser.contents) |
343 | 892 | 893 | ||
344 | @@ -911,6 +912,21 @@ | |||
345 | 911 | Unauthorized, self.getUserBrowser, | 912 | Unauthorized, self.getUserBrowser, |
346 | 912 | repository_url + "/+diff/0123456/0123456^") | 913 | repository_url + "/+diff/0123456/0123456^") |
347 | 913 | 914 | ||
348 | 915 | def test_filename_quoting(self): | ||
349 | 916 | # If we construct revisions containing metacharacters and somehow | ||
350 | 917 | # manage to get that past the hosting service, the | ||
351 | 918 | # Content-Disposition header is quoted properly. | ||
352 | 919 | diff = "A fake diff\n" | ||
353 | 920 | self.useFixture(GitHostingFixture(diff={"patch": diff})) | ||
354 | 921 | person = self.factory.makePerson() | ||
355 | 922 | repository = self.factory.makeGitRepository( | ||
356 | 923 | owner=person, name="some-repository") | ||
357 | 924 | browser = self.getUserBrowser( | ||
358 | 925 | canonical_url(repository) + '/+diff/foo"/"bar') | ||
359 | 926 | self.assertEqual( | ||
360 | 927 | r'attachment; filename="some-repository_\"bar_foo\".diff"', | ||
361 | 928 | browser.headers["Content-Disposition"]) | ||
362 | 929 | |||
363 | 914 | 930 | ||
364 | 915 | class TestGitRepositoryDeletionView(BrowserTestCase): | 931 | class TestGitRepositoryDeletionView(BrowserTestCase): |
365 | 916 | 932 | ||
366 | 917 | 933 | ||
367 | === modified file 'lib/lp/code/interfaces/branch.py' | |||
368 | --- lib/lp/code/interfaces/branch.py 2018-03-06 00:59:06 +0000 | |||
369 | +++ lib/lp/code/interfaces/branch.py 2018-06-22 10:03:28 +0000 | |||
370 | @@ -1,4 +1,4 @@ | |||
372 | 1 | # Copyright 2009-2016 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
373 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
374 | 3 | 3 | ||
375 | 4 | """Branch interfaces.""" | 4 | """Branch interfaces.""" |
376 | @@ -765,6 +765,15 @@ | |||
377 | 765 | :param launchbag: `ILaunchBag`. | 765 | :param launchbag: `ILaunchBag`. |
378 | 766 | """ | 766 | """ |
379 | 767 | 767 | ||
380 | 768 | def getDiff(new, old): | ||
381 | 769 | """Get the diff between two revisions in this branch. | ||
382 | 770 | |||
383 | 771 | :param new: The new revno or revision ID. | ||
384 | 772 | :param old: The old revno or revision ID. Defaults to the parent | ||
385 | 773 | revision of `new`. | ||
386 | 774 | :return: The diff as a byte string. | ||
387 | 775 | """ | ||
388 | 776 | |||
389 | 768 | @export_read_operation() | 777 | @export_read_operation() |
390 | 769 | @operation_for_version('beta') | 778 | @operation_for_version('beta') |
391 | 770 | def canBeDeleted(): | 779 | def canBeDeleted(): |
392 | 771 | 780 | ||
393 | === modified file 'lib/lp/code/model/branch.py' | |||
394 | --- lib/lp/code/model/branch.py 2018-03-06 00:59:06 +0000 | |||
395 | +++ lib/lp/code/model/branch.py 2018-06-22 10:03:28 +0000 | |||
396 | @@ -105,6 +105,7 @@ | |||
397 | 105 | WrongNumberOfReviewTypeArguments, | 105 | WrongNumberOfReviewTypeArguments, |
398 | 106 | ) | 106 | ) |
399 | 107 | from lp.code.interfaces.branchcollection import IAllBranches | 107 | from lp.code.interfaces.branchcollection import IAllBranches |
400 | 108 | from lp.code.interfaces.branchhosting import IBranchHostingClient | ||
401 | 108 | from lp.code.interfaces.branchlookup import IBranchLookup | 109 | from lp.code.interfaces.branchlookup import IBranchLookup |
402 | 109 | from lp.code.interfaces.branchmergeproposal import ( | 110 | from lp.code.interfaces.branchmergeproposal import ( |
403 | 110 | BRANCH_MERGE_PROPOSAL_FINAL_STATES, | 111 | BRANCH_MERGE_PROPOSAL_FINAL_STATES, |
404 | @@ -782,6 +783,11 @@ | |||
405 | 782 | RevisionAuthor, revisions, ['revision_author_id']) | 783 | RevisionAuthor, revisions, ['revision_author_id']) |
406 | 783 | return DecoratedResultSet(result, pre_iter_hook=eager_load) | 784 | return DecoratedResultSet(result, pre_iter_hook=eager_load) |
407 | 784 | 785 | ||
408 | 786 | def getDiff(self, new, old=None): | ||
409 | 787 | """See `IBranch`.""" | ||
410 | 788 | hosting_client = getUtility(IBranchHostingClient) | ||
411 | 789 | return hosting_client.getDiff(self.id, new, old=old) | ||
412 | 790 | |||
413 | 785 | def canBeDeleted(self): | 791 | def canBeDeleted(self): |
414 | 786 | """See `IBranch`.""" | 792 | """See `IBranch`.""" |
415 | 787 | if ((len(self.deletionRequirements()) != 0) or not | 793 | if ((len(self.deletionRequirements()) != 0) or not |
416 | 788 | 794 | ||
417 | === modified file 'lib/lp/code/tests/helpers.py' | |||
418 | --- lib/lp/code/tests/helpers.py 2017-12-19 17:16:38 +0000 | |||
419 | +++ lib/lp/code/tests/helpers.py 2018-06-22 10:03:28 +0000 | |||
420 | @@ -1,4 +1,4 @@ | |||
422 | 1 | # Copyright 2009-2017 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
423 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
424 | 3 | 3 | ||
425 | 4 | """Helper functions for code testing live here.""" | 4 | """Helper functions for code testing live here.""" |
426 | @@ -8,6 +8,7 @@ | |||
427 | 8 | __metaclass__ = type | 8 | __metaclass__ = type |
428 | 9 | __all__ = [ | 9 | __all__ = [ |
429 | 10 | 'add_revision_to_branch', | 10 | 'add_revision_to_branch', |
430 | 11 | 'BranchHostingFixture', | ||
431 | 11 | 'get_non_existant_source_package_branch_unique_name', | 12 | 'get_non_existant_source_package_branch_unique_name', |
432 | 12 | 'GitHostingFixture', | 13 | 'GitHostingFixture', |
433 | 13 | 'make_erics_fooix_project', | 14 | 'make_erics_fooix_project', |
434 | @@ -35,6 +36,7 @@ | |||
435 | 35 | ) | 36 | ) |
436 | 36 | 37 | ||
437 | 37 | from lp.app.enums import InformationType | 38 | from lp.app.enums import InformationType |
438 | 39 | from lp.code.interfaces.branchhosting import IBranchHostingClient | ||
439 | 38 | from lp.code.interfaces.branchmergeproposal import ( | 40 | from lp.code.interfaces.branchmergeproposal import ( |
440 | 39 | IBranchMergeProposalJobSource, | 41 | IBranchMergeProposalJobSource, |
441 | 40 | ) | 42 | ) |
442 | @@ -301,6 +303,19 @@ | |||
443 | 301 | c.execute('delete from branch') | 303 | c.execute('delete from branch') |
444 | 302 | 304 | ||
445 | 303 | 305 | ||
446 | 306 | class BranchHostingFixture(fixtures.Fixture): | ||
447 | 307 | """A fixture that temporarily registers a fake Bazaar hosting client.""" | ||
448 | 308 | |||
449 | 309 | def __init__(self, diff=None, inventory=None, blob=None): | ||
450 | 310 | self.create = FakeMethod() | ||
451 | 311 | self.getDiff = FakeMethod(result=diff or {}) | ||
452 | 312 | self.getInventory = FakeMethod(result=inventory or {}) | ||
453 | 313 | self.getBlob = FakeMethod(result=blob) | ||
454 | 314 | |||
455 | 315 | def _setUp(self): | ||
456 | 316 | self.useFixture(ZopeUtilityFixture(self, IBranchHostingClient)) | ||
457 | 317 | |||
458 | 318 | |||
459 | 304 | class GitHostingFixture(fixtures.Fixture): | 319 | class GitHostingFixture(fixtures.Fixture): |
460 | 305 | """A fixture that temporarily registers a fake Git hosting client.""" | 320 | """A fixture that temporarily registers a fake Git hosting client.""" |
461 | 306 | 321 | ||
462 | 307 | 322 | ||
463 | === modified file 'lib/lp/services/webapp/publisher.py' | |||
464 | --- lib/lp/services/webapp/publisher.py 2017-11-22 13:05:11 +0000 | |||
465 | +++ lib/lp/services/webapp/publisher.py 2018-06-22 10:03:28 +0000 | |||
466 | @@ -29,6 +29,7 @@ | |||
467 | 29 | from cgi import FieldStorage | 29 | from cgi import FieldStorage |
468 | 30 | import httplib | 30 | import httplib |
469 | 31 | import re | 31 | import re |
470 | 32 | from wsgiref.headers import Headers | ||
471 | 32 | 33 | ||
472 | 33 | from lazr.restful import ( | 34 | from lazr.restful import ( |
473 | 34 | EntryResource, | 35 | EntryResource, |
474 | @@ -233,9 +234,12 @@ | |||
475 | 233 | class DataDownloadView: | 234 | class DataDownloadView: |
476 | 234 | """Download data without templating. | 235 | """Download data without templating. |
477 | 235 | 236 | ||
479 | 236 | Subclasses must provide getBody, content_type and filename. | 237 | Subclasses must provide getBody, content_type and filename, and may |
480 | 238 | provide charset. | ||
481 | 237 | """ | 239 | """ |
482 | 238 | 240 | ||
483 | 241 | charset = None | ||
484 | 242 | |||
485 | 239 | def __init__(self, context, request): | 243 | def __init__(self, context, request): |
486 | 240 | self.context = context | 244 | self.context = context |
487 | 241 | self.request = request | 245 | self.request = request |
488 | @@ -246,10 +250,16 @@ | |||
489 | 246 | It is not necessary to supply Content-length, because this is added by | 250 | It is not necessary to supply Content-length, because this is added by |
490 | 247 | the caller. | 251 | the caller. |
491 | 248 | """ | 252 | """ |
496 | 249 | self.request.response.setHeader('Content-Type', self.content_type) | 253 | headers = Headers([]) |
497 | 250 | self.request.response.setHeader( | 254 | content_type_params = {} |
498 | 251 | 'Content-Disposition', 'attachment; filename="%s"' % ( | 255 | if self.charset is not None: |
499 | 252 | self.filename)) | 256 | content_type_params['charset'] = self.charset |
500 | 257 | headers.add_header( | ||
501 | 258 | 'Content-Type', self.content_type, **content_type_params) | ||
502 | 259 | headers.add_header( | ||
503 | 260 | 'Content-Disposition', 'attachment', filename=self.filename) | ||
504 | 261 | for key, value in headers.items(): | ||
505 | 262 | self.request.response.setHeader(key, value) | ||
506 | 253 | return self.getBody() | 263 | return self.getBody() |
507 | 254 | 264 | ||
508 | 255 | 265 |