Merge lp:~spiv/launchpad/bmp-inline-diffs into lp:launchpad

Proposed by Andrew Bennetts
Status: Merged
Approved by: Andrew Bennetts
Approved revision: no longer in the source branch.
Merged at revision: 13481
Proposed branch: lp:~spiv/launchpad/bmp-inline-diffs
Merge into: lp:launchpad
Prerequisite: lp:~danilo/launchpad/expander-anim
Diff against target: 418 lines (+333/-2)
8 files modified
lib/lp/code/browser/branchmergeproposal.py (+11/-0)
lib/lp/code/javascript/branch.revisionexpander.js (+103/-0)
lib/lp/code/javascript/tests/test_branchrevisionexpander.html (+35/-0)
lib/lp/code/javascript/tests/test_branchrevisionexpander.js (+151/-0)
lib/lp/code/templates/branch-macros.pt (+16/-0)
lib/lp/code/templates/branchmergeproposal-index.pt (+11/-1)
lib/lp/code/templates/codereviewnewrevisions-footer.pt (+2/-1)
lib/lp/services/features/flags.py (+4/-0)
To merge this branch: bzr merge lp:~spiv/launchpad/bmp-inline-diffs
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Review via email: mp+66634@code.launchpad.net

Commit message

[r=danilo][bug=813349] Add inline diff expanders to revision summaries in merge proposal comments.

Description of the change

This still needs some polish, but it's close enough to be worth a review. It's guarded by a feature flag so it ought to be safe to land as is.

It adds inline dynamic diffs to branch index and merge proposal pages. It fetches them from loggerhead (proxied via the lpnet webapp).

It's guarded by a feature flag, the jslint is clean, and it has some tests (although it could use more). I don't think this does anything outrageously wrong, but many of the tools used here are new to me so I could be wrong! So let me know what you think!

To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

[tweak] Could you please change the css so you get a pointing-hand cursor (link cursor) on the expander?

Revision history for this message
Martin Pool (mbp) wrote :
Revision history for this message
Martin Pool (mbp) wrote :

[tweak] The flag ought to be called foo.enabled

I think this is not going to work for private branches because of <https://bugs.launchpad.net/launchpad/+bug/806713>. For an initial closed beta we can probably just live with it being broken. As an interim step, I guess we would have the option of disabling it for private branches. Eventually we'd need to fix that.

Revision history for this message
Данило Шеган (danilo) wrote :

I'm adding more API to the expander to allow one to linkify the "toggler" with an easy parameter to setUp in my follow-up branch for expander-anim. expander-anim should also already accept failed parameter to receive method (so it tries to re-load stuff when it fails, just use receive(node, true) in the error handler).

Revision history for this message
Gavin Panella (allenap) wrote :

Hi,

There are conflicts here with Danilo's branch I think. Could you take a look?

Gavin.

Revision history for this message
Andrew Bennetts (spiv) wrote :

Conflicts have been resolved.

Revision history for this message
Martin Pool (mbp) wrote :

Specifically, the thing with the flag is that the name in the templates is not the same as the name in the flags.py documentation.

Revision history for this message
Andrew Bennetts (spiv) wrote :

Regarding the CSS, as far as I'm aware my patch is doing everything it's supposed to. It looks to me like a bug in the expander infrastructure rather than this patch. I've pinged Danilo on IRC about it.

I've fixed the feature flag name: I actually had it correct in the TAL, but misnamed in flags.py. Fixed now, I think.

Revision history for this message
Данило Шеган (danilo) wrote :

Andrew, just a few minor comments:

 - getDiffUrl could probably be dropped and replaced with bmpGetDiffUrl use (i.e. bmpGetDiffUrl(revno, revno-1))
 - perhaps you could accept undefined (and use Y.Lang.isUndefined or Y.Lang.isValue [which is false for nulls and undefineds]) as the end_revno in bmpGetDiffUrl when you just want one revision
 - with the changes to the expander in the meantime, it'd probably be better to instanciate the expander directly instead of using createByCSS. That will allow you to call expander.setUp(true), when the content inside the "toggle" node will be turned into a proper link (and you shouldn't wrap it inside <a> in TAL, or it'll end up being <a> inside an <a>)
 - alternatively to the above, you can just add 'href="#"' to get the pointing hand cursor to appear if you feel like the link should stay in TAL or you prefer createByCSS API
 - generally, tables should not be used for non-tabular-data formatting, but if existing CSS classes you use don't work if you simply replace <td>s with <div>s, just keep it as is
 - you may also need to change test set-up (especially in HTML) because of the recent changes to the test infrastructure; try looking at other existing tests for reference and make sure the test passes when run as "bin/test -t test_branchrevisionexpander.html"

I consider most of the above minor issues, so here's a conditional approval on fixing those listed above.

review: Approve
Revision history for this message
Данило Шеган (danilo) wrote :

FWIW, expander-anim is landing as soon as we get out of RC mode.

Revision history for this message
Andrew Bennetts (spiv) wrote :

 - I dropped getDiffUrl (and renamed bmpGetDiffUrl to bmp_get_diff_url to be more consistent with the rest of the file)
 - I added more JS tests for bmp_get_diff_url (and fixed a bug that uncovered! — diff ranges starting with 0 other than 0..1 were wrong)
 - bmp_get_diff_url now accepts either a single revno, or a pair of revnos describing a range
 - I'm still using createByCSS, as that seems to be more concise and convenient than the alternative
 - I added href="#', which fixes the cursor, thanks!
 - The table is precisely how the existing merge proposal diff is formatted. If that should change, then that's an issue for a separate patch I think. I'm just reusing what we already do.
 - I tested with bin/test as suggested, and the test appears to run just fine. Hooray!

Thanks for the review. I've addressed all the points, so I think this is ready to land. Sweet!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branchmergeproposal.py'
2--- lib/lp/code/browser/branchmergeproposal.py 2011-07-04 15:48:18 +0000
3+++ lib/lp/code/browser/branchmergeproposal.py 2011-07-20 03:39:50 +0000
4@@ -38,6 +38,7 @@
5 from lazr.lifecycle.event import ObjectModifiedEvent
6 from lazr.restful.interface import copy_field
7 from lazr.restful.interfaces import (
8+ IJSONRequestCache,
9 IWebServiceClientRequest,
10 )
11 import simplejson
12@@ -596,6 +597,16 @@
13 label = "Proposal to merge branch"
14 schema = ClaimButton
15
16+ def initialize(self):
17+ super(BranchMergeProposalView, self).initialize()
18+ cache = IJSONRequestCache(self.request)
19+ cache.objects.update({
20+ 'branch_diff_link':
21+ 'https://%s/+loggerhead/%s/diff/' %
22+ (config.launchpad.code_domain,
23+ self.context.source_branch.unique_name)
24+ })
25+
26 @action('Claim', name='claim')
27 def claim_action(self, action, data):
28 """Claim this proposal."""
29
30=== added file 'lib/lp/code/javascript/branch.revisionexpander.js'
31--- lib/lp/code/javascript/branch.revisionexpander.js 1970-01-01 00:00:00 +0000
32+++ lib/lp/code/javascript/branch.revisionexpander.js 2011-07-20 03:39:50 +0000
33@@ -0,0 +1,103 @@
34+/* Copyright 2011 Canonical Ltd. This software is licensed under the
35+ * GNU Affero General Public License version 3 (see the file LICENSE).
36+ *
37+ * Expandable branch revisions.
38+ *
39+ * @module lp.code.branch.revisionexpander
40+ * @requires node, lp.client.plugins
41+ */
42+
43+YUI.add('lp.code.branch.revisionexpander', function(Y) {
44+
45+var namespace = Y.namespace('lp.code.branch.revisionexpander');
46+
47+/*
48+ * Take a single revno, or a pair of revnos specifying a revision range, and
49+ * construct a URL to get the diff of those revnos using
50+ * LP.cache.branch_diff_link.
51+ */
52+function bmp_get_diff_url(start_revno, end_revno) {
53+ var diff_url;
54+ if (Y.Lang.isUndefined(end_revno)) {
55+ /* No end_revno passed, so only after a single revision diff. */
56+ return LP.cache.branch_diff_link + start_revno;
57+ }
58+ diff_url = LP.cache.branch_diff_link + end_revno;
59+ if (start_revno !== 0) {
60+ diff_url += '/' + start_revno;
61+ } else if (start_revno === 0 && end_revno !== 1) {
62+ diff_url += '/null:';
63+ }
64+ return diff_url;
65+}
66+
67+function difftext_to_node(difftext) {
68+ var node = Y.Node.create('<table class="diff"></table>');
69+ var difflines = difftext.split('\n');
70+ var i;
71+ /* Remove the empty final row caused by a trailing newline
72+ * (if it is empty) */
73+ if (difflines.length > 0 && difflines[difflines.length-1] === '') {
74+ difflines.pop();
75+ }
76+
77+ for (i=0; i < difflines.length; i++) {
78+ var line = difflines[i];
79+ var line_node = Y.Node.create('<td/>');
80+ line_node.set('text', line + '\n');
81+ /* Colour the unified diff */
82+ var header_pat = /^(===|---|\+\+\+) /;
83+ var chunk_pat = /^@@ /;
84+ if (line.match(header_pat)) {
85+ line_node.addClass('diff-header');
86+ } else if (line.match(chunk_pat)) {
87+ line_node.addClass('diff-chunk');
88+ } else {
89+ switch (line[0]) {
90+ case '+':
91+ line_node.addClass('diff-added');
92+ break;
93+ case '-':
94+ line_node.addClass('diff-removed');
95+ break;
96+ }
97+ }
98+ line_node.addClass('text');
99+ var row = Y.Node.create('<tr></tr>');
100+ row.appendChild(line_node);
101+ node.appendChild(row);
102+ }
103+ return node;
104+}
105+
106+function revision_expander_config(expander){
107+ return {
108+ on: {
109+ success: function nodify_result(diff) {
110+ expander.receive(difftext_to_node(diff));
111+ },
112+ failure: function(trid, response, args) {
113+ expander.receive(Y.Node.create('<pre><i>Error</i></pre>'));
114+ }
115+ }
116+ };
117+}
118+
119+function bmp_diff_loader(expander, lp_client) {
120+ if (lp_client === undefined) {
121+ lp_client = new Y.lp.client.Launchpad();
122+ }
123+ var rev_no_range = expander.icon_node.get(
124+ 'id').replace('expandable-', '').split('-');
125+ var start_revno = rev_no_range[0]-1;
126+ var end_revno = rev_no_range[1];
127+
128+ lp_client.get(bmp_get_diff_url(start_revno, end_revno),
129+ revision_expander_config(expander));
130+}
131+
132+namespace.bmp_diff_loader = bmp_diff_loader;
133+namespace.difftext_to_node = difftext_to_node;
134+namespace.bmp_get_diff_url = bmp_get_diff_url;
135+
136+}, "0.1", {"requires": ["node", "lp.app.widgets.expander"]});
137
138=== added file 'lib/lp/code/javascript/tests/test_branchrevisionexpander.html'
139--- lib/lp/code/javascript/tests/test_branchrevisionexpander.html 1970-01-01 00:00:00 +0000
140+++ lib/lp/code/javascript/tests/test_branchrevisionexpander.html 2011-07-20 03:39:50 +0000
141@@ -0,0 +1,35 @@
142+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
143+
144+<html>
145+ <head>
146+ <title>Test page for branch revisionexpander JavaScript unit tests</title>
147+
148+ <!-- YUI 3.0 Setup -->
149+ <script type="text/javascript" src="../../../../canonical/launchpad/icing/yui/yui/yui.js"></script>
150+ <script type="text/javascript" src="../../../../canonical/launchpad/icing/lazr/build/lazr.js"></script>
151+ <script type="text/javascript" src="../../../app/javascript/client.js"></script>
152+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssreset/reset.css"/>
153+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssfonts/fonts.css"/>
154+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/yui/cssbase/base.css"/>
155+ <link rel="stylesheet" href="../../../../canonical/launchpad/javascript/test.css" />
156+ <link rel="stylesheet" href="../../../../canonical/launchpad/icing/lazr/build/lazr-sam.css" />
157+
158+ <!-- expected variable -->
159+ <script type="text/javascript">
160+ var LP = {
161+ cache: {},
162+ links: {}
163+ };
164+ </script>
165+
166+ <!-- The module under test -->
167+ <script type="text/javascript" src="../branch.revisionexpander.js"></script>
168+
169+ <!-- The test suite -->
170+ <script type="text/javascript" src="test_branchrevisionexpander.js"></script>
171+ </head>
172+
173+<body class="yui3-skin-sam">
174+</body>
175+</html>
176+
177
178=== added file 'lib/lp/code/javascript/tests/test_branchrevisionexpander.js'
179--- lib/lp/code/javascript/tests/test_branchrevisionexpander.js 1970-01-01 00:00:00 +0000
180+++ lib/lp/code/javascript/tests/test_branchrevisionexpander.js 2011-07-20 03:39:50 +0000
181@@ -0,0 +1,151 @@
182+/* Copyright 2010 Canonical Ltd. This software is licensed under the
183+ * GNU Affero General Public License version 3 (see the file LICENSE).
184+ *
185+ * Tests for lp.code.branch.revisionexpander.
186+ *
187+ */
188+
189+YUI({
190+ base: '../../../../canonical/launchpad/icing/yui/',
191+ filter: 'raw', combine: false
192+ }).use('test', 'console', 'node-event-simulate', 'lp.client',
193+ 'lp.code.branch.revisionexpander', function(Y) {
194+
195+ var module = Y.lp.code.branch.revisionexpander;
196+ var suite = new Y.Test.Suite("branch.revisionexpander Tests");
197+
198+ var MockClient = function() {};
199+ MockClient.prototype = {
200+ 'calls': [],
201+ 'get': function(uri, config) {
202+ this.calls.push({'uri': uri});
203+ config.on.success(samplediff);
204+ }
205+ };
206+
207+ var samplediff = (
208+ "=== modified file 'README'\n" +
209+ "--- README 2011-01-20 23:05:06 +0000\n" +
210+ "+++ README 2011-06-30 10:47:28 +0000\n" +
211+ "@@ -1,3 +1,4 @@\n" +
212+ "+Green sheep!\n" +
213+ " =========\n" +
214+ " testtools\n" +
215+ " =========\n" +
216+ " \n");
217+
218+ suite.add(new Y.Test.Case({
219+ name: 'Test difftext_to_node',
220+
221+ /*
222+ * Unified diffs are rendered to a table, one row per line
223+ */
224+ test_difftext_to_node_outputs_table: function() {
225+ var node = module.difftext_to_node(samplediff);
226+ Y.Assert.areEqual('TABLE', node.get('tagName'));
227+ Y.Assert.isTrue(node.hasClass('diff'));
228+ /* samplediff has 9 lines, so the table will have 9 rows
229+ * (trailing newlines don't result in a final row containing an
230+ * empty string) */
231+ Y.Assert.areEqual(9, node.get('children').size());
232+ },
233+
234+ /*
235+ * Diffs are not interpreted as HTML.
236+ */
237+ test_difftext_to_node_escaping: function() {
238+ var node = module.difftext_to_node("<p>hello</p>");
239+ var td = node.one('td');
240+ Y.Assert.isNull(td.one('p'));
241+ }
242+ }));
243+
244+ suite.add(new Y.Test.Case({
245+ name: 'Tests for bmp_diff_loader and bmp_get_diff_url',
246+
247+ setUp: function() {
248+ LP.cache.branch_diff_link = 'fake-link-base/';
249+ },
250+
251+ tearDown: function() {
252+ delete LP.cache.branch_diff_link;
253+ },
254+
255+ /*
256+ * bmp_diff_loader fetches from the URI specified by the div id and
257+ * renders a diff.
258+ */
259+ test_bmp_diff_loader_fetches_from_diff_uri: function() {
260+ var FakeExpander = function() {};
261+ FakeExpander.prototype = {
262+ 'icon_node':
263+ Y.Node.create('<div id="expandable-23-45"></div>'),
264+ 'receive': function (node) {
265+ this.received_node = node;
266+ }
267+ };
268+ var mock_client = new MockClient();
269+ var fake_expander = new FakeExpander();
270+ module.bmp_diff_loader(fake_expander, mock_client);
271+ Y.Assert.areEqual(
272+ 'fake-link-base/45/22', mock_client.calls[0].uri);
273+ Y.Assert.areEqual(
274+ 'TABLE', fake_expander.received_node.get('tagName'));
275+ },
276+
277+ /*
278+ * bmp_get_diff_url(revno) gets the URL for a diff of just that revision
279+ */
280+ test_bmp_get_diff_url_one_arg: function() {
281+ Y.Assert.areEqual(
282+ 'fake-link-base/1234',
283+ module.bmp_get_diff_url(1234));
284+ },
285+
286+ /*
287+ * bmp_get_diff_url(start_revno, end_revno) gets the URL for a diff of
288+ * the given revision range.
289+ */
290+ test_bmp_get_diff_url_two_args: function() {
291+ Y.Assert.areEqual(
292+ 'fake-link-base/33/22',
293+ module.bmp_get_diff_url(22, 33));
294+ },
295+
296+ /*
297+ * bmp_get_diff_url(0, 1) just returns URL_BASE/1, rather than
298+ * URL_BASE/1/0 which Loggerhead will reject.
299+ */
300+ test_bmp_get_diff_url_of_0_to_1: function() {
301+ Y.Assert.areEqual(
302+ 'fake-link-base/1',
303+ module.bmp_get_diff_url(0, 1));
304+ },
305+
306+ /*
307+ * bmp_get_diff_url(0, 2) just returns URL_BASE/1, rather than
308+ * URL_BASE/1/0 which Loggerhead will reject.
309+ */
310+ test_bmp_get_diff_url_of_0_to_2: function() {
311+ Y.Assert.areEqual(
312+ 'fake-link-base/2/null:',
313+ module.bmp_get_diff_url(0, 2));
314+ }
315+ }));
316+
317+ var handle_complete = function(data) {
318+ window.status = '::::' + JSON.stringify(data);
319+ };
320+ Y.Test.Runner.on('complete', handle_complete);
321+ Y.Test.Runner.add(suite);
322+
323+ var console = new Y.Console({newestOnTop: false});
324+ console.render('#log');
325+
326+ // Start the test runner on Y.after to ensure all setup has had a
327+ // chance to complete.
328+ Y.after('domready', function() {
329+ Y.Test.Runner.run();
330+ });
331+});
332+
333
334=== modified file 'lib/lp/code/templates/branch-macros.pt'
335--- lib/lp/code/templates/branch-macros.pt 2011-07-06 14:54:46 +0000
336+++ lib/lp/code/templates/branch-macros.pt 2011-07-20 03:39:50 +0000
337@@ -202,6 +202,22 @@
338 condition="revisions | nothing">
339 <metal:landing-target use-macro="branch/@@+macros/revision-text"/>
340 </tal:revision>
341+ <tal:ajax-revision-diffs
342+ condition="request/features/code.ajax_revision_diffs.enabled">
343+ <tal:diff-expander condition="show_diff_expander | nothing">
344+ <div class="revision-group-diff" tal:condition="revisions | nothing">
345+ <a href="#" class="unseen expander-icon"
346+ tal:define="start_revno python:revisions[0].sequence;
347+ last_revno python:revisions[-1].sequence"
348+ tal:attributes="id string:expandable-${start_revno}-${last_revno}"
349+ >Changes added by revision <tal:revno replace="start_revno">1</tal:revno>
350+ <tal:plural condition="python: start_revno!=last_revno">
351+ to revision <tal:revno replace="last_revno">2</tal:revno>
352+ </tal:plural></a>
353+ <div class="unseen expander-content">Loading diff</div>
354+ </div>
355+ </tal:diff-expander>
356+ </tal:ajax-revision-diffs>
357 </dl>
358
359 </metal:branch-revisions>
360
361=== modified file 'lib/lp/code/templates/branchmergeproposal-index.pt'
362--- lib/lp/code/templates/branchmergeproposal-index.pt 2011-02-23 22:32:15 +0000
363+++ lib/lp/code/templates/branchmergeproposal-index.pt 2011-07-20 03:39:50 +0000
364@@ -200,7 +200,8 @@
365 conf = <tal:status-config replace="view/status_config" />
366 <!--
367 LPS.use('io-base', 'lp.code.branchmergeproposal.reviewcomment',
368- 'lp.code.branchmergeproposal.status', 'lp.app.comment', function(Y) {
369+ 'lp.code.branchmergeproposal.status', 'lp.app.comment',
370+ 'lp.app.widgets.expander', 'lp.code.branch.revisionexpander', function(Y) {
371
372 Y.on('load', function() {
373 var logged_in = LP.links['me'] !== undefined;
374@@ -219,6 +220,15 @@
375 (new Y.lp.code.branchmergeproposal.reviewcomment.NumberToggle()
376 ).render();
377 }, window);
378+
379+ Y.on('domready', function() {
380+ Y.lp.app.widgets.expander.createByCSS(
381+ '.revision-group-diff',
382+ '.expander-icon',
383+ '.expander-content',
384+ false,
385+ Y.lp.code.branch.revisionexpander.bmp_diff_loader);
386+ });
387 });
388 -->
389 <tal:script replace="structure string:&lt;/script&gt;" />
390
391=== modified file 'lib/lp/code/templates/codereviewnewrevisions-footer.pt'
392--- lib/lp/code/templates/codereviewnewrevisions-footer.pt 2010-10-06 17:07:11 +0000
393+++ lib/lp/code/templates/codereviewnewrevisions-footer.pt 2011-07-20 03:39:50 +0000
394@@ -4,7 +4,8 @@
395 omit-tag="">
396
397 <tal:revisions define="branch context/branch;
398- revisions context/revisions">
399+ revisions context/revisions;
400+ show_diff_expander python:True;">
401 <metal:landing-target use-macro="branch/@@+macros/branch-revisions"/>
402 </tal:revisions>
403 <tal:diff condition="context/diff" replace="structure context/diff/text/fmt:diff" />
404
405=== modified file 'lib/lp/services/features/flags.py'
406--- lib/lp/services/features/flags.py 2011-07-19 14:42:52 +0000
407+++ lib/lp/services/features/flags.py 2011-07-20 03:39:50 +0000
408@@ -44,6 +44,10 @@
409 'boolean',
410 ('Enables the display of bugtracker components.'),
411 ''),
412+ ('code.ajax_revision_diffs.enabled',
413+ 'boolean',
414+ ("Offer expandable inline diffs for branch revisions."),
415+ ''),
416 ('code.branchmergequeue',
417 'boolean',
418 'Enables merge queue pages and lists them on branch pages.',