Merge lp:~spiv/launchpad/bmp-inline-diffs into lp:launchpad
- bmp-inline-diffs
- Merge into devel
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 |
Related bugs: |
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!
Martin Pool (mbp) wrote : | # |
Martin Pool (mbp) wrote : | # |
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:/
Данило Шеган (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).
Gavin Panella (allenap) wrote : | # |
Hi,
There are conflicts here with Danilo's branch I think. Could you take a look?
Gavin.
Andrew Bennetts (spiv) wrote : | # |
Conflicts have been resolved.
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.
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.
Данило Шеган (danilo) wrote : | # |
Andrew, just a few minor comments:
- getDiffUrl could probably be dropped and replaced with bmpGetDiffUrl use (i.e. bmpGetDiffUrl(
- 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.
- 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_branchrevi
I consider most of the above minor issues, so here's a conditional approval on fixing those listed above.
Данило Шеган (danilo) wrote : | # |
FWIW, expander-anim is landing as soon as we get out of RC mode.
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
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:</script>" /> |
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.', |
[tweak] Could you please change the css so you get a pointing-hand cursor (link cursor) on the expander?