launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22547
[Merge] lp:~cjwatson/launchpad/better-bzr-mp-diffs into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/better-bzr-mp-diffs into lp:launchpad with lp:~cjwatson/launchpad/branch-hosting-client as a prerequisite.
Commit message:
Proxy loggerhead branch diffs through the webapp, allowing AJAX MP revision diffs to work for private branches.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #904070 in Launchpad itself: "broken inline incremental ajax diff on private branches"
https://bugs.launchpad.net/launchpad/+bug/904070
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/better-bzr-mp-diffs/+merge/345705
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.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/better-bzr-mp-diffs into lp:launchpad.
=== modified file 'lib/lp/code/browser/branch.py'
--- lib/lp/code/browser/branch.py 2018-03-16 21:20:00 +0000
+++ lib/lp/code/browser/branch.py 2018-05-16 19:08:06 +0000
@@ -46,6 +46,7 @@
providedBy,
)
from zope.publisher.interfaces import NotFound
+from zope.publisher.interfaces.browser import IBrowserPublisher
from zope.schema import (
Bool,
Choice,
@@ -203,6 +204,22 @@
return None
return self.context.getMergeProposalByID(id)
+ @stepto("+diff")
+ def traverse_diff(self):
+ segments = list(self.request.getTraversalStack())
+ if len(segments) == 1:
+ new = segments.pop()
+ old = None
+ self.request.stepstogo.consume()
+ elif len(segments) == 2:
+ new = segments.pop()
+ old = segments.pop()
+ self.request.stepstogo.consume()
+ self.request.stepstogo.consume()
+ else:
+ return None
+ return BranchDiffView(self.context, self.request, new, old=old)
+
@stepto("+code-import")
def traverse_code_import(self):
"""Traverses to the `ICodeImport` for the branch."""
@@ -1280,3 +1297,28 @@
'target_branch',
"This branch is not mergeable into %s." %
target_branch.bzr_identity)
+
+
+@implementer(IBrowserPublisher)
+class BranchDiffView:
+
+ def __init__(self, context, request, new, old=None):
+ self.context = context
+ self.request = request
+ self.new = new
+ self.old = old
+
+ def __call__(self):
+ content = self.context.getDiff(self.new, old=self.old)
+ if self.old is None:
+ filename = "%s.diff" % self.new
+ else:
+ filename = "%s_%s.diff" % (self.old, self.new)
+ self.request.response.setHeader("Content-Type", "text/x-patch")
+ self.request.response.setHeader("Content-Length", str(len(content)))
+ self.request.response.setHeader(
+ "Content-Disposition", "attachment; filename=%s" % filename)
+ return content
+
+ def browserDefault(self, request):
+ return self, ()
=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
--- lib/lp/code/browser/branchmergeproposal.py 2018-04-25 12:01:33 +0000
+++ lib/lp/code/browser/branchmergeproposal.py 2018-05-16 19:08:06 +0000
@@ -617,19 +617,8 @@
super(BranchMergeProposalView, self).initialize()
cache = IJSONRequestCache(self.request)
cache.objects['branch_name'] = self.context.merge_source.name
- if IBranch.providedBy(self.context.merge_source):
- cache.objects.update({
- 'branch_diff_link':
- 'https://%s/+loggerhead/%s/diff/' % (
- config.launchpad.code_domain,
- self.context.source_branch.unique_name),
- })
- else:
- cache.objects.update({
- 'branch_diff_link':
- canonical_url(self.context.source_git_repository) +
- '/+diff/',
- })
+ cache.objects['branch_diff_link'] = (
+ canonical_url(self.context.parent) + '/+diff/')
if getFeatureFlag("longpoll.merge_proposals.enabled"):
cache.objects['merge_proposal_event_key'] = subscribe(
self.context).event_key
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2017-09-27 13:50:02 +0000
+++ lib/lp/code/browser/configure.zcml 2018-05-16 19:08:06 +0000
@@ -442,6 +442,10 @@
class="lp.code.browser.codeimport.CodeImportEditView"
permission="launchpad.Edit"
template="../../app/templates/generic-edit.pt"/>
+ <class class="lp.code.browser.branch.BranchDiffView">
+ <allow interface="zope.publisher.interfaces.browser.IBrowserPublisher"
+ attributes="__call__"/>
+ </class>
<browser:page
name="+delete"
for="lp.code.interfaces.branch.IBranch"
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py 2018-02-01 18:38:24 +0000
+++ lib/lp/code/browser/tests/test_branch.py 2018-05-16 19:08:06 +0000
@@ -16,6 +16,7 @@
from testtools.matchers import Equals
from zope.component import getUtility
from zope.publisher.interfaces import NotFound
+from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
@@ -32,6 +33,7 @@
RepositoryFormat,
)
from lp.code.enums import BranchType
+from lp.code.tests.helpers import BranchHostingFixture
from lp.registry.enums import BranchSharingPolicy
from lp.registry.interfaces.accesspolicy import IAccessPolicySource
from lp.registry.interfaces.person import PersonVisibility
@@ -1220,3 +1222,45 @@
InformationType.USERDATA.description, description.renderContents())
self.assertIsNotNone(
soup.find('a', id='privacy-link', attrs={'href': edit_url}))
+
+
+class TestBranchDiffView(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def test_render(self):
+ diff = b"A fake diff\n"
+ hosting_fixture = self.useFixture(BranchHostingFixture(diff=diff))
+ person = self.factory.makePerson()
+ branch = self.factory.makeBranch(owner=person)
+ browser = self.getUserBrowser(
+ canonical_url(branch) + "/+diff/2/1")
+ with person_logged_in(person):
+ self.assertEqual(
+ [((branch.unique_name, "2"), {"old": "1"})],
+ hosting_fixture.getDiff.calls)
+ self.assertEqual("text/x-patch", browser.headers["Content-Type"])
+ self.assertEqual(str(len(diff)), browser.headers["Content-Length"])
+ self.assertEqual(
+ "attachment; filename=1_2.diff",
+ browser.headers["Content-Disposition"])
+ self.assertEqual(diff, browser.contents)
+
+ def test_security(self):
+ # A user who can see a private branch can fetch diffs from it, but
+ # other users cannot.
+ diff = b"A fake diff\n"
+ self.useFixture(BranchHostingFixture(diff=diff))
+ person = self.factory.makePerson()
+ project = self.factory.makeProduct(
+ owner=person, information_type=InformationType.PROPRIETARY)
+ with person_logged_in(person):
+ branch = self.factory.makeBranch(
+ owner=person, product=project,
+ information_type=InformationType.PROPRIETARY)
+ branch_url = canonical_url(branch)
+ browser = self.getUserBrowser(branch_url + "/+diff/2/1", user=person)
+ self.assertEqual(diff, browser.contents)
+ self.useFixture(FakeLogger())
+ self.assertRaises(
+ Unauthorized, self.getUserBrowser, branch_url + "/+diff/2/1")
=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposal.py'
--- lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-04-25 12:01:33 +0000
+++ lib/lp/code/browser/tests/test_branchmergeproposal.py 2018-05-16 19:08:06 +0000
@@ -1635,14 +1635,14 @@
def test_client_cache_bzr(self):
# For Bazaar, the client cache contains the branch name and a
- # loggerhead-based diff link.
+ # webapp-based diff link.
bmp = self.factory.makeBranchMergeProposal()
view = create_initialized_view(bmp, '+index')
cache = IJSONRequestCache(view.request)
self.assertThat(cache.objects, ContainsDict({
'branch_name': Equals(bmp.source_branch.name),
'branch_diff_link': Equals(
- 'https://code.launchpad.dev/+loggerhead/%s/diff/' %
+ 'http://code.launchpad.dev/%s/+diff/' %
bmp.source_branch.unique_name),
}))
=== modified file 'lib/lp/code/interfaces/branch.py'
--- lib/lp/code/interfaces/branch.py 2018-03-06 00:59:06 +0000
+++ lib/lp/code/interfaces/branch.py 2018-05-16 19:08:06 +0000
@@ -765,6 +765,15 @@
:param launchbag: `ILaunchBag`.
"""
+ def getDiff(new, old):
+ """Get the diff between two revisions in this branch.
+
+ :param new: The new revno or revision ID.
+ :param old: The old revno or revision ID. Defaults to the parent
+ revision of `new`.
+ :return: The diff as a byte string.
+ """
+
@export_read_operation()
@operation_for_version('beta')
def canBeDeleted():
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2018-03-06 00:59:06 +0000
+++ lib/lp/code/model/branch.py 2018-05-16 19:08:06 +0000
@@ -105,6 +105,7 @@
WrongNumberOfReviewTypeArguments,
)
from lp.code.interfaces.branchcollection import IAllBranches
+from lp.code.interfaces.branchhosting import IBranchHostingClient
from lp.code.interfaces.branchlookup import IBranchLookup
from lp.code.interfaces.branchmergeproposal import (
BRANCH_MERGE_PROPOSAL_FINAL_STATES,
@@ -782,6 +783,12 @@
RevisionAuthor, revisions, ['revision_author_id'])
return DecoratedResultSet(result, pre_iter_hook=eager_load)
+ def getDiff(self, new, old=None):
+ """See `IBranch`."""
+ hosting_client = getUtility(IBranchHostingClient)
+ diff = hosting_client.getDiff(self.unique_name, new, old=old)
+ return diff
+
def canBeDeleted(self):
"""See `IBranch`."""
if ((len(self.deletionRequirements()) != 0) or not
=== modified file 'lib/lp/code/tests/helpers.py'
--- lib/lp/code/tests/helpers.py 2017-12-19 17:16:38 +0000
+++ lib/lp/code/tests/helpers.py 2018-05-16 19:08:06 +0000
@@ -8,6 +8,7 @@
__metaclass__ = type
__all__ = [
'add_revision_to_branch',
+ 'BranchHostingFixture',
'get_non_existant_source_package_branch_unique_name',
'GitHostingFixture',
'make_erics_fooix_project',
@@ -35,6 +36,7 @@
)
from lp.app.enums import InformationType
+from lp.code.interfaces.branchhosting import IBranchHostingClient
from lp.code.interfaces.branchmergeproposal import (
IBranchMergeProposalJobSource,
)
@@ -301,6 +303,19 @@
c.execute('delete from branch')
+class BranchHostingFixture(fixtures.Fixture):
+ """A fixture that temporarily registers a fake Bazaar hosting client."""
+
+ def __init__(self, diff=None, inventory=None, blob=None):
+ self.create = FakeMethod()
+ self.getDiff = FakeMethod(result=diff or {})
+ self.getInventory = FakeMethod(result=inventory or {})
+ self.getBlob = FakeMethod(result=blob)
+
+ def _setUp(self):
+ self.useFixture(ZopeUtilityFixture(self, IBranchHostingClient))
+
+
class GitHostingFixture(fixtures.Fixture):
"""A fixture that temporarily registers a fake Git hosting client."""
Follow ups