Merge lp:~mkanat/loggerhead/view-default into lp:loggerhead

Proposed by Max Kanat-Alexander
Status: Merged
Approved by: Max Kanat-Alexander
Approved revision: 435
Merged at revision: 433
Proposed branch: lp:~mkanat/loggerhead/view-default
Merge into: lp:loggerhead
Diff against target: 449 lines (+149/-91)
9 files modified
loggerhead/apps/branch.py (+2/-0)
loggerhead/controllers/annotate_ui.py (+54/-0)
loggerhead/controllers/view_ui.py (+33/-48)
loggerhead/highlight.py (+4/-2)
loggerhead/static/css/view.css (+14/-12)
loggerhead/templatefunctions.py (+2/-2)
loggerhead/templates/inventory.pt (+8/-8)
loggerhead/templates/revision.pt (+2/-2)
loggerhead/templates/view.pt (+30/-17)
To merge this branch: bzr merge lp:~mkanat/loggerhead/view-default
Reviewer Review Type Date Requested Status
Martin Pool Approve
Michael Hudson-Doyle Approve
Review via email: mp+42222@code.launchpad.net

Commit message

Create a new "view" controller, and have that be the default view of a file.

Description of the change

This adds a new controller, "view", and makes it the default for viewing a file, instead of "annotate". The "view" controller is just like "annotate", except that it doesn't show the revision for each line. This should significantly improve the performance of loggerhead, particularly on high-level installations (like launchpad) or on large branches (such as launchpad itself) because most of the time, people don't need the annotated information for a branch--they just want to see its content.

In actual tests, the "view" controller is between 3x and 10x faster than the "annotate" controller. (To see similar gains to these, try viewing a large file on the launchpad devel branch without any existing caches in loggerhead.)

The "view" and "annotate" controller both use the same templates, and I've renamed all of the templates and CSS to "view" instead of "annotate" (since we now think of "annotate" as a special case of "view"). I've moved annotate_ui.py to view_ui.py and created a brand-new annotate_ui.py over the old one.

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Yay for progress on this!

I think it's mostly fine. The template is ugly though. Why do you need to put a None at the end of the annotation iterator? It would be better to pad out the results until you've reached the length of the text lines, or something.

If you're going to rename the annotation css file, you should probably rename the rules therein too.

Everything else looks ok. Loading the annotation information via ajax would be nice, but can wait :-)

Cheers,
mwh

review: Approve
Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

> I think it's mostly fine. The template is ugly though. Why do you need to
> put a None at the end of the annotation iterator? It would be better to pad
> out the results until you've reached the length of the text lines, or
> something.

  That's true, I could do that, but that would also then involve code for keeping track of when there were extra lines in 'contents' that were beyond how far along we were with annotating. Would simplify the template, though, which I think is a valid goal. I think the problem is that 'contents' contains an extra final line when there's a "\n" at the end of the file, and annotate_iter doesn't consider that to be a line.

> If you're going to rename the annotation css file, you should probably rename
> the rules therein too.

  Yeah, I suppose you're right. I was trying to keep the changes at least *somewhat* focused though, so that they could be reviewed sensibly. But now that it's passed review I'll just do that change.

> Everything else looks ok. Loading the annotation information via ajax would
> be nice, but can wait :-)

  Yeah, I had the same thought.

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

That looks plausible to me, and reasonably clean.

As we discussed elsewhere, it would be good to get this onto the Launchpad deployment branch separately from changes in loggerhead trunk that are possibly more risky, such as historydb.

I wish there was an automatic test included here, but if there is no framework in place I won't ask you to write one as part of this proposal.

review: Approve
lp:~mkanat/loggerhead/view-default updated
433. By Max Kanat-Alexander

Instead of yielding None for when there are more lines than annotate_iter
has, just fix the off-by-one bug that made us have too many lines in
"contents" (caused by annotate_iter using split_lines and us using
split("\n") in highlight()).

434. By Max Kanat-Alexander

The colors of rows in the annotated view wasn't getting set properly. There
would be a revision number, and then the *next* line would change color.

435. By Max Kanat-Alexander

Rename the anno* CSS classes to view*.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'loggerhead/apps/branch.py'
2--- loggerhead/apps/branch.py 2010-05-21 14:07:06 +0000
3+++ loggerhead/apps/branch.py 2010-12-01 08:13:14 +0000
4@@ -29,6 +29,7 @@
5
6 from loggerhead.apps import static_app
7 from loggerhead.controllers.annotate_ui import AnnotateUI
8+from loggerhead.controllers.view_ui import ViewUI
9 from loggerhead.controllers.atom_ui import AtomUI
10 from loggerhead.controllers.changelog_ui import ChangeLogUI
11 from loggerhead.controllers.diff_ui import DiffUI
12@@ -114,6 +115,7 @@
13 'files': InventoryUI,
14 'revision': RevisionUI,
15 'search': SearchUI,
16+ 'view': ViewUI,
17 }
18
19 def last_updated(self):
20
21=== added file 'loggerhead/controllers/annotate_ui.py'
22--- loggerhead/controllers/annotate_ui.py 1970-01-01 00:00:00 +0000
23+++ loggerhead/controllers/annotate_ui.py 2010-12-01 08:13:14 +0000
24@@ -0,0 +1,54 @@
25+#
26+# Copyright (C) 2010 Canonical Ltd.
27+#
28+# This program is free software; you can redistribute it and/or modify
29+# it under the terms of the GNU General Public License as published by
30+# the Free Software Foundation; either version 2 of the License, or
31+# (at your option) any later version.
32+#
33+# This program is distributed in the hope that it will be useful,
34+# but WITHOUT ANY WARRANTY; without even the implied warranty of
35+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36+# GNU General Public License for more details.
37+#
38+# You should have received a copy of the GNU General Public License
39+# along with this program; if not, write to the Free Software
40+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
41+#
42+
43+from loggerhead.controllers.view_ui import ViewUI
44+from loggerhead import util
45+
46+class AnnotateUI(ViewUI):
47+
48+ def annotate_file(self, info):
49+ file_id = info['file_id']
50+ revid = info['change'].revid
51+
52+ tree = self.tree_for(file_id, revid)
53+
54+ change_cache = {}
55+ last_line_revid = None
56+ parity = 1
57+ for line_revid, text in tree.annotate_iter(file_id):
58+ if line_revid == last_line_revid:
59+ # remember which lines have a new revno and which don't
60+ new_rev = False
61+ else:
62+ new_rev = True
63+ parity ^= 1
64+ last_line_revid = line_revid
65+ if line_revid in change_cache:
66+ change = change_cache[line_revid]
67+ else:
68+ change = self._history.get_changes([line_revid])[0]
69+ change_cache[line_revid] = change
70+
71+ yield util.Container(
72+ parity=parity, new_rev=new_rev, change=change)
73+
74+ def get_values(self, path, kwargs, headers):
75+ values = super(AnnotateUI, self).get_values(path, kwargs, headers)
76+ values['annotated'] = self.annotate_file(values)
77+
78+ return values
79
80=== renamed file 'loggerhead/controllers/annotate_ui.py' => 'loggerhead/controllers/view_ui.py'
81--- loggerhead/controllers/annotate_ui.py 2009-10-17 06:35:33 +0000
82+++ loggerhead/controllers/view_ui.py 2010-12-01 08:13:14 +0000
83@@ -35,20 +35,18 @@
84 from loggerhead import util
85
86
87-class AnnotateUI(TemplatedBranchView):
88-
89- template_path = 'loggerhead.templates.annotate'
90-
91- def annotate_file(self, file_id, revid):
92- z = time.time()
93- lineno = 1
94- parity = 0
95-
96+class ViewUI(TemplatedBranchView):
97+
98+ template_path = 'loggerhead.templates.view'
99+
100+ def tree_for(self, file_id, revid):
101 file_revid = self._history.get_inventory(revid)[file_id].revision
102- tree = self._history._branch.repository.revision_tree(file_revid)
103+ return self._history._branch.repository.revision_tree(file_revid)
104
105+ def text_lines(self, file_id, revid):
106 file_name = os.path.basename(self._history.get_path(revid, file_id))
107-
108+
109+ tree = self.tree_for(file_id, revid)
110 file_text = tree.get_file_text(file_id)
111 encoding = 'utf-8'
112 try:
113@@ -58,44 +56,27 @@
114 file_text = file_text.decode(encoding)
115
116 file_lines = bzrlib.osutils.split_lines(file_text)
117+ # This can throw bzrlib.errors.BinaryFile (which our caller catches).
118+ bzrlib.textfile.check_text_lines(file_lines)
119+
120+ if highlight is not None:
121+ hl_lines = highlight(file_name, file_text, encoding)
122+ # highlight strips off extra newlines at the end of the file.
123+ extra_lines = len(file_lines) - len(hl_lines)
124+ hl_lines.extend([u''] * extra_lines)
125+ else:
126+ hl_lines = map(cgi.escape, file_lines)
127+
128+ return hl_lines;
129
130+ def file_contents(self, file_id, revid):
131 try:
132- bzrlib.textfile.check_text_lines(file_lines)
133+ file_lines = self.text_lines(file_id, revid)
134 except bzrlib.errors.BinaryFile:
135 # bail out; this isn't displayable text
136- yield util.Container(parity=0, lineno=1, status='same',
137- text='(This is a binary file.)',
138- change=util.Container())
139- else:
140- if highlight is not None:
141- hl_lines = highlight(file_name, file_text, encoding)
142- hl_lines.extend([u''] * (len(file_lines) - len(hl_lines)))
143- else:
144- hl_lines = map(cgi.escape, file_lines)
145-
146- change_cache = {}
147-
148- last_line_revid = None
149- for line_revid, text in tree.annotate_iter(file_id):
150- if line_revid == last_line_revid:
151- # remember which lines have a new revno and which don't
152- status = 'same'
153- else:
154- status = 'changed'
155- parity ^= 1
156- last_line_revid = line_revid
157- if line_revid in change_cache:
158- change = change_cache[line_revid]
159- else:
160- change = self._history.get_changes([line_revid])[0]
161- change_cache[line_revid] = change
162-
163- yield util.Container(
164- parity=parity, lineno=lineno, status=status,
165- change=change, text=hl_lines[lineno - 1])
166- lineno += 1
167-
168- self.log.debug('annotate: %r secs' % (time.time() - z,))
169+ return ['(This is a binary file.)']
170+
171+ return file_lines
172
173 def get_values(self, path, kwargs, headers):
174 history = self._history
175@@ -105,7 +86,7 @@
176 file_id = kwargs.get('file_id', None)
177 if (file_id is None) and (path is None):
178 raise HTTPBadRequest('No file_id or filename '
179- 'provided to annotate')
180+ 'provided to view')
181
182 if file_id is None:
183 file_id = history.get_file_id(revid, path)
184@@ -140,13 +121,17 @@
185 branch_breadcrumbs = util.branch_breadcrumbs(path, inv, 'files')
186
187 return {
188+ # In AnnotateUI, "annotated" is a generator giving revision
189+ # numbers per lines, but the template checks if "annotated" is
190+ # true or not before using it, so we have to define it here also.
191+ 'annotated': False,
192 'revno_url': revno_url,
193 'file_id': file_id,
194- 'path': path,
195+ 'file_path': path,
196 'filename': filename,
197 'navigation': navigation,
198 'change': change,
199- 'contents': list(self.annotate_file(file_id, revid)),
200+ 'contents': self.file_contents(file_id, revid),
201 'fileview_active': True,
202 'directory_breadcrumbs': directory_breadcrumbs,
203 'branch_breadcrumbs': branch_breadcrumbs,
204
205=== modified file 'loggerhead/highlight.py'
206--- loggerhead/highlight.py 2010-04-22 03:23:55 +0000
207+++ loggerhead/highlight.py 2010-12-01 08:13:14 +0000
208@@ -16,6 +16,7 @@
209 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
210 #
211
212+import bzrlib.osutils
213 import cgi
214
215 from pygments import highlight as _highlight_func
216@@ -36,7 +37,7 @@
217 """
218
219 if len(text) > MAX_HIGHLIGHT_SIZE:
220- return map(cgi.escape, text.split('\n'))
221+ return map(cgi.escape, bzrlib.osutils.split_lines(text))
222
223 formatter = HtmlFormatter(style=style, nowrap=True, classprefix='pyg-')
224
225@@ -48,6 +49,7 @@
226 except (ClassNotFound, ValueError):
227 lexer = TextLexer(encoding=encoding)
228
229- hl_lines = _highlight_func(text, lexer, formatter).split('\n')
230+ hl_lines = _highlight_func(text, lexer, formatter)
231+ hl_lines = bzrlib.osutils.split_lines(hl_lines)
232
233 return hl_lines
234
235=== renamed file 'loggerhead/static/css/annotate.css' => 'loggerhead/static/css/view.css'
236--- loggerhead/static/css/annotate.css 2009-04-30 10:39:05 +0000
237+++ loggerhead/static/css/view.css 2010-12-01 08:13:14 +0000
238@@ -1,29 +1,31 @@
239 /*table*/
240-.annoLineTit, .annoLine, .annoRevTit, .annoRev, .annoComm, .annoCommTit, .annoContTit, .annoCont {
241- width:45px;
242+.viewLineTit, .viewLine, .viewRevTit, .viewRev,
243+.viewContTit, .viewCont
244+{
245 border:1px solid #d2d2d2;
246 }
247-.annoLine, .annoRev, .annoComm, .annoCont {
248+.viewLine, .viewRev, .viewComm, .viewCont {
249 border:none;
250 }
251-.annoLine {
252+.viewLine, .viewLineTit {
253+ padding-right: .2em;
254+}
255+.viewLine {
256 width:37px;
257 }
258-.annoContTit, .annoCont {
259+.viewContTit, .viewCont {
260 width:auto;
261+ padding-left: .3em;
262 }
263-.annoRevTit, .annoRev {
264+.viewRevTit, .viewRev {
265 width:70px;
266 text-align:center;
267 }
268-.annoComm, .annoCommTit {
269- width:200px;
270-}
271-.annoLine, .annoCont {
272+.viewLine, .viewCont {
273 font:normal 12px/normal monospace;
274 whitespace: pre;
275 }
276-.annoCont pre { margin: 0; }
277-.annoLine {
278+.viewCont pre { margin: 0; }
279+.viewLine {
280 text-align:right;
281 }
282
283=== modified file 'loggerhead/templatefunctions.py'
284--- loggerhead/templatefunctions.py 2009-10-17 08:47:38 +0000
285+++ loggerhead/templatefunctions.py 2010-12-01 08:13:14 +0000
286@@ -121,9 +121,9 @@
287
288
289 @templatefunc
290-def annotate_link(url, revno, path):
291+def view_link(url, revno, path):
292 return '<a href="%s" title="Annotate %s">%s</a>' % (
293- url(['/annotate', revno, path]), cgi.escape(path), cgi.escape(path))
294+ url(['/view', revno, path]), cgi.escape(path), cgi.escape(path))
295
296 @templatefunc
297 def revision_link(url, revno, path, frag=''):
298
299=== modified file 'loggerhead/templates/inventory.pt'
300--- loggerhead/templates/inventory.pt 2009-10-17 06:55:25 +0000
301+++ loggerhead/templates/inventory.pt 2010-12-01 08:13:14 +0000
302@@ -89,11 +89,11 @@
303
304 <!-- Show this if it's a symlink -->
305 <tr tal:attributes="class string:blueRow${repeat/file/even}" tal:condition="python:file.kind=='symlink'">
306- <td class="autcell"><a tal:attributes="href python:url(['/annotate', change.revno, file.absolutepath])">
307+ <td class="autcell"><a tal:attributes="href python:url(['/view', change.revno, file.absolutepath])">
308 <img tal:attributes="src python:branch.static_url('/static/images/ico_file_flecha.gif')" alt="Symlink" />
309 </a>
310
311- <a tal:attributes="href python:url(['/annotate', revno_url, file.absolutepath])"
312+ <a tal:attributes="href python:url(['/view', revno_url, file.absolutepath])"
313 tal:content="file/filename" class="link"></a>
314 </td>
315 <td class="date"><a tal:attributes="href python:url(['/revision', file.change.revno]);
316@@ -113,17 +113,17 @@
317
318 <!-- Show this if it's a regular file -->
319 <tr tal:attributes="class string:blueRow${repeat/file/even}" tal:condition="python:file.kind=='file'">
320- <td class="autcell"><a tal:attributes="href python:url(['/annotate', revno_url, file.absolutepath])">
321+ <td class="autcell"><a tal:attributes="href python:url(['/view', revno_url, file.absolutepath])">
322 <img tal:attributes="src python:branch.static_url('/static/images/ico_file.gif');
323- title string:Annotate ${file/filename}"
324+ title string:View ${file/filename}"
325 tal:condition="python:file.executable is False" />
326 <!-- Show a different icon id the file executable -->
327 <img tal:attributes="src python:branch.static_url('/static/images/ico_file_modify.gif');
328- title string:Annotate ${file/filename}"
329+ title string:View ${file/filename}"
330 tal:condition="python:file.executable is True" alt="File" />
331 </a>
332
333- <a tal:attributes="href python:url(['/annotate', revno_url, file.absolutepath])"
334+ <a tal:attributes="href python:url(['/view', revno_url, file.absolutepath])"
335 tal:content="file/filename" class="link"></a></td>
336 <td class="date"><a tal:attributes="href python:url(['/revision', file.change.revno]);
337 title string:Show revision ${file/change/revno}"
338@@ -131,8 +131,8 @@
339 </td>
340 <td class="date" tal:content="python:util.date_time(file.change.date)"></td>
341 <td class="timedate2" tal:content="python:util.human_size(file.size)"></td>
342- <td class="expcell"><a tal:attributes="href python:url(['/annotate', revno_url, file.absolutepath]);
343- title string:Annotate ${file/filename}">
344+ <td class="expcell"><a tal:attributes="href python:url(['/view', revno_url, file.absolutepath]);
345+ title string:View ${file/filename}">
346 <img tal:attributes="src python:branch.static_url('/static/images/ico_planilla.gif')" alt="Diff" />
347 </a>
348 </td>
349
350=== modified file 'loggerhead/templates/revision.pt'
351--- loggerhead/templates/revision.pt 2009-04-28 03:01:54 +0000
352+++ loggerhead/templates/revision.pt 2010-12-01 08:13:14 +0000
353@@ -42,7 +42,7 @@
354 </tal:compare-to>
355 </span>
356 <span class="breadcrumb" tal:condition="specific_path">
357- : <tal:annotate content="structure python:annotate_link(url, change.revno, specific_path)" />
358+ : <tal:annotate content="structure python:view_link(url, change.revno, specific_path)" />
359 </span>
360 </h1>
361 <tal:branch-info replace="structure python:branchinfo(branch)" />
362@@ -55,7 +55,7 @@
363 </a>
364 </p>
365 <p tal:condition="specific_path">
366- Viewing changes to <tal:annotate content="structure python:annotate_link(url, change.revno, specific_path)" />
367+ Viewing changes to <tal:annotate content="structure python:view_link(url, change.revno, specific_path)" />
368 </p>
369 <ul id="submenuTabs">
370 <li id="first"><a tal:attributes="href python:url(['/files', change.revno]);
371
372=== renamed file 'loggerhead/templates/annotate.pt' => 'loggerhead/templates/view.pt'
373--- loggerhead/templates/annotate.pt 2009-04-30 10:39:05 +0000
374+++ loggerhead/templates/view.pt 2010-12-01 08:13:14 +0000
375@@ -3,12 +3,12 @@
376 <html xmlns="http://www.w3.org/1999/xhtml" metal:use-macro="macros/main">
377 <head>
378 <title metal:fill-slot="title"
379- tal:content="string:${branch/friendly_name} : contents of ${path}
380+ tal:content="string:${branch/friendly_name} : contents of ${file_path}
381 at revision ${change/revno}">
382 </title>
383 <metal:block fill-slot="header_extras">
384 <link rel="stylesheet" type="text/css" media="all"
385- tal:attributes="href python:branch.static_url('/static/css/annotate.css')"/>
386+ tal:attributes="href python:branch.static_url('/static/css/view.css')"/>
387 <link rel="stylesheet" type="text/css" media="all"
388 tal:attributes="href python:branch.static_url('/static/css/highlight.css')"/>
389 </metal:block>
390@@ -36,6 +36,14 @@
391 <li id="first">
392 <a tal:attributes="href python:url(['/files', change.revno], clear=1)">browse files</a>
393 </li>
394+ <li tal:condition="not:annotated">
395+ <a tal:attributes="href python:url(['/annotate', revno_url, file_path], clear=1)"
396+ >view revision numbers per line</a>
397+ </li>
398+ <li tal:condition="annotated">
399+ <a tal:attributes="href python:url(['/view', revno_url, file_path], clear=1)"
400+ >view without revision numbers</a>
401+ </li>
402 <li>
403 <a tal:attributes="href python:url(['/revision', change.revno], clear=1)">view revision</a>
404 </li>
405@@ -50,25 +58,30 @@
406 </li>
407 </ul>
408
409- <div class="annotate">
410+ <div class="view">
411 <table id="logentries">
412 <tr class="logheader">
413- <td class="annoLineTit">Line</td>
414- <td class="annoRevTit">Revision</td>
415- <td class="annoContTit">Contents</td>
416+ <td class="viewLineTit">Line</td>
417+ <td class="viewRevTit" tal:condition="annotated">Revision</td>
418+ <td class="viewContTit">Contents</td>
419 </tr>
420
421- <tr tal:repeat="line contents"
422- tal:attributes="class string:blueRow${line/parity}">
423- <td class="annoLine"><a tal:attributes="id string:L${line/lineno}; href string:#L${line/lineno}" tal:content="line/lineno">1</a></td>
424- <td class="annoRev">
425- <a tal:condition="python:line.status=='changed'"
426- tal:content="python:util.trunc(line.change.revno)"
427- tal:attributes="href python:url(['/revision', line.change.revno], clear=1);
428- title python:'%s by %s, on %s (%s)'%(line.change.revno, ', '.join(util.hide_emails(line.change.authors)), line.change.date.strftime('%d %b %Y %H:%M'), util.date_time(line.change.date))"></a>
429- </td>
430- <td class="annoCont"><pre tal:content="structure line/text"></pre></td>
431- </tr>
432+ <tal:rep tal:repeat="line contents">
433+ <tr tal:define="anno python:annotated and annotated.next()"
434+ tal:attributes="class python:anno and 'blueRow' + str(anno.parity) or None">
435+ <td class="viewLine">
436+ <a tal:attributes="id string:L${repeat/line/number}; href string:#L${repeat/line/number}"
437+ tal:content="repeat/line/number">1</a>
438+ </td>
439+ <td class="viewRev" tal:condition="annotated">
440+ <a tal:condition="python:anno.new_rev"
441+ tal:content="python:util.trunc(anno.change.revno)"
442+ tal:attributes="href python:url(['/revision', anno.change.revno], clear=1);
443+ title python:'%s by %s, on %s (%s)' % (anno.change.revno, ', '.join(util.hide_emails(anno.change.authors)), anno.change.date.strftime('%d %b %Y %H:%M'), util.date_time(anno.change.date))"></a>
444+ </td>
445+ <td class="viewCont"><pre tal:content="structure line"></pre></td>
446+ </tr>
447+ </tal:rep>
448 </table>
449 </div>
450

Subscribers

People subscribed via source and target branches