Mercurial > genshi > mirror
comparison examples/trac/trac/versioncontrol/web_ui/changeset.py @ 39:93b4dcbafd7b trunk
Copy Trac to main branch.
author | cmlenz |
---|---|
date | Mon, 03 Jul 2006 18:53:27 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
38:ee669cb9cccc | 39:93b4dcbafd7b |
---|---|
1 # -*- coding: utf-8 -*- | |
2 # | |
3 # Copyright (C) 2003-2006 Edgewall Software | |
4 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com> | |
5 # Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de> | |
6 # Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr> | |
7 # All rights reserved. | |
8 # | |
9 # This software is licensed as described in the file COPYING, which | |
10 # you should have received as part of this distribution. The terms | |
11 # are also available at http://trac.edgewall.com/license.html. | |
12 # | |
13 # This software consists of voluntary contributions made by many | |
14 # individuals. For the exact contribution history, see the revision | |
15 # history and logs, available at http://projects.edgewall.com/trac/. | |
16 # | |
17 # Author: Jonas Borgström <jonas@edgewall.com> | |
18 # Christopher Lenz <cmlenz@gmx.de> | |
19 # Christian Boos <cboos@neuf.fr> | |
20 | |
21 import posixpath | |
22 import re | |
23 from StringIO import StringIO | |
24 import time | |
25 | |
26 from trac import util | |
27 from trac.config import BoolOption, IntOption | |
28 from trac.core import * | |
29 from trac.mimeview import Mimeview, is_binary | |
30 from trac.perm import IPermissionRequestor | |
31 from trac.Search import ISearchSource, search_to_sql, shorten_result | |
32 from trac.Timeline import ITimelineEventProvider | |
33 from trac.util.datefmt import format_datetime, pretty_timedelta | |
34 from trac.util.markup import html, escape, unescape, Markup | |
35 from trac.util.text import unicode_urlencode, shorten_line, CRLF | |
36 from trac.versioncontrol import Changeset, Node | |
37 from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff | |
38 from trac.versioncontrol.svn_authz import SubversionAuthorizer | |
39 from trac.versioncontrol.web_ui.util import render_node_property | |
40 from trac.web import IRequestHandler | |
41 from trac.web.chrome import INavigationContributor, add_link, add_stylesheet | |
42 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider, \ | |
43 Formatter | |
44 | |
45 | |
46 class DiffArgs(dict): | |
47 def __getattr__(self, str): | |
48 return self[str] | |
49 | |
50 | |
51 class ChangesetModule(Component): | |
52 """Provide flexible functionality for showing sets of differences. | |
53 | |
54 If the differences shown are coming from a specific changeset, | |
55 then that changeset informations can be shown too. | |
56 | |
57 In addition, it is possible to show only a subset of the changeset: | |
58 Only the changes affecting a given path will be shown. | |
59 This is called the ''restricted'' changeset. | |
60 | |
61 But the differences can also be computed in a more general way, | |
62 between two arbitrary paths and/or between two arbitrary revisions. | |
63 In that case, there's no changeset information displayed. | |
64 """ | |
65 | |
66 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, | |
67 ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource) | |
68 | |
69 timeline_show_files = IntOption('timeline', 'changeset_show_files', 0, | |
70 """Number of files to show (`-1` for unlimited, `0` to disable).""") | |
71 | |
72 timeline_long_messages = BoolOption('timeline', 'changeset_long_messages', | |
73 'false', | |
74 """Whether wiki-formatted changeset messages should be multiline or not. | |
75 | |
76 If this option is not specified or is false and `wiki_format_messages` | |
77 is set to true, changeset messages will be single line only, losing | |
78 some formatting (bullet points, etc).""") | |
79 | |
80 max_diff_files = IntOption('changeset', 'max_diff_files', 0, | |
81 """Maximum number of modified files for which the changeset view will | |
82 attempt to show the diffs inlined (''since 0.10'')."""), | |
83 | |
84 max_diff_bytes = IntOption('changeset', 'max_diff_bytes', 10000000, | |
85 """Maximum total size in bytes of the modified files (their old size | |
86 plus their new size) for which the changeset view will attempt to show | |
87 the diffs inlined (''since 0.10'').""") | |
88 | |
89 wiki_format_messages = BoolOption('changeset', 'wiki_format_messages', | |
90 'true', | |
91 """Whether wiki formatting should be applied to changeset messages. | |
92 | |
93 If this option is disabled, changeset messages will be rendered as | |
94 pre-formatted text.""") | |
95 | |
96 # INavigationContributor methods | |
97 | |
98 def get_active_navigation_item(self, req): | |
99 return 'browser' | |
100 | |
101 def get_navigation_items(self, req): | |
102 return [] | |
103 | |
104 # IPermissionRequestor methods | |
105 | |
106 def get_permission_actions(self): | |
107 return ['CHANGESET_VIEW'] | |
108 | |
109 # IRequestHandler methods | |
110 | |
111 _request_re = re.compile(r"/changeset(?:/([^/]+))?(/.*)?$") | |
112 | |
113 def match_request(self, req): | |
114 match = re.match(self._request_re, req.path_info) | |
115 if match: | |
116 new, new_path = match.groups() | |
117 if new: | |
118 req.args['new'] = new | |
119 if new_path: | |
120 req.args['new_path'] = new_path | |
121 return True | |
122 | |
123 def process_request(self, req): | |
124 """The appropriate mode of operation is inferred from the request | |
125 parameters: | |
126 | |
127 * If `new_path` and `old_path` are equal (or `old_path` is omitted) | |
128 and `new` and `old` are equal (or `old` is omitted), | |
129 then we're about to view a revision Changeset: `chgset` is True. | |
130 Furthermore, if the path is not the root, the changeset is | |
131 ''restricted'' to that path (only the changes affecting that path, | |
132 its children or its ancestor directories will be shown). | |
133 * In any other case, the set of changes corresponds to arbitrary | |
134 differences between path@rev pairs. If `new_path` and `old_path` | |
135 are equal, the ''restricted'' flag will also be set, meaning in this | |
136 case that the differences between two revisions are restricted to | |
137 those occurring on that path. | |
138 | |
139 In any case, either path@rev pairs must exist. | |
140 """ | |
141 req.perm.assert_permission('CHANGESET_VIEW') | |
142 | |
143 # -- retrieve arguments | |
144 new_path = req.args.get('new_path') | |
145 new = req.args.get('new') | |
146 old_path = req.args.get('old_path') | |
147 old = req.args.get('old') | |
148 | |
149 if old and '@' in old: | |
150 old_path, old = unescape(old).split('@') | |
151 if new and '@' in new: | |
152 new_path, new = unescape(new).split('@') | |
153 | |
154 # -- normalize and check for special case | |
155 repos = self.env.get_repository(req.authname) | |
156 new_path = repos.normalize_path(new_path) | |
157 new = repos.normalize_rev(new) | |
158 old_path = repos.normalize_path(old_path or new_path) | |
159 old = repos.normalize_rev(old or new) | |
160 | |
161 authzperm = SubversionAuthorizer(self.env, req.authname) | |
162 authzperm.assert_permission_for_changeset(new) | |
163 | |
164 if old_path == new_path and old == new: # revert to Changeset | |
165 old_path = old = None | |
166 | |
167 diff_options = get_diff_options(req) | |
168 | |
169 # -- setup the `chgset` and `restricted` flags, see docstring above. | |
170 chgset = not old and not old_path | |
171 if chgset: | |
172 restricted = new_path not in ('', '/') # (subset or not) | |
173 else: | |
174 restricted = old_path == new_path # (same path or not) | |
175 | |
176 # -- redirect if changing the diff options | |
177 if req.args.has_key('update'): | |
178 if chgset: | |
179 if restricted: | |
180 req.redirect(req.href.changeset(new, new_path)) | |
181 else: | |
182 req.redirect(req.href.changeset(new)) | |
183 else: | |
184 req.redirect(req.href.changeset(new, new_path, old=old, | |
185 old_path=old_path)) | |
186 | |
187 # -- preparing the diff arguments | |
188 if chgset: | |
189 prev = repos.get_node(new_path, new).get_previous() | |
190 if prev: | |
191 prev_path, prev_rev = prev[:2] | |
192 else: | |
193 prev_path, prev_rev = new_path, repos.previous_rev(new) | |
194 diff_args = DiffArgs(old_path=prev_path, old_rev=prev_rev, | |
195 new_path=new_path, new_rev=new) | |
196 else: | |
197 if not new: | |
198 new = repos.youngest_rev | |
199 elif not old: | |
200 old = repos.youngest_rev | |
201 if not old_path: | |
202 old_path = new_path | |
203 diff_args = DiffArgs(old_path=old_path, old_rev=old, | |
204 new_path=new_path, new_rev=new) | |
205 if chgset: | |
206 chgset = repos.get_changeset(new) | |
207 message = chgset.message or '--' | |
208 if self.wiki_format_messages: | |
209 message = wiki_to_html(message, self.env, req, | |
210 escape_newlines=True) | |
211 else: | |
212 message = html.PRE(message) | |
213 req.check_modified(chgset.date, [ | |
214 diff_options[0], | |
215 ''.join(diff_options[1]), | |
216 repos.name, | |
217 repos.rev_older_than(new, repos.youngest_rev), | |
218 message, | |
219 pretty_timedelta(chgset.date, None, 3600)]) | |
220 else: | |
221 message = None # FIXME: what date should we choose for a diff? | |
222 | |
223 req.hdf['changeset'] = diff_args | |
224 | |
225 format = req.args.get('format') | |
226 | |
227 if format in ['diff', 'zip']: | |
228 req.perm.assert_permission('FILE_VIEW') | |
229 # choosing an appropriate filename | |
230 rpath = new_path.replace('/','_') | |
231 if chgset: | |
232 if restricted: | |
233 filename = 'changeset_%s_r%s' % (rpath, new) | |
234 else: | |
235 filename = 'changeset_r%s' % new | |
236 else: | |
237 if restricted: | |
238 filename = 'diff-%s-from-r%s-to-r%s' \ | |
239 % (rpath, old, new) | |
240 elif old_path == '/': # special case for download (#238) | |
241 filename = '%s-r%s' % (rpath, old) | |
242 else: | |
243 filename = 'diff-from-%s-r%s-to-%s-r%s' \ | |
244 % (old_path.replace('/','_'), old, rpath, new) | |
245 if format == 'diff': | |
246 self._render_diff(req, filename, repos, diff_args, | |
247 diff_options) | |
248 return | |
249 elif format == 'zip': | |
250 self._render_zip(req, filename, repos, diff_args) | |
251 return | |
252 | |
253 # -- HTML format | |
254 self._render_html(req, repos, chgset, restricted, message, | |
255 diff_args, diff_options) | |
256 if chgset: | |
257 diff_params = 'new=%s' % new | |
258 else: | |
259 diff_params = unicode_urlencode({'new_path': new_path, | |
260 'new': new, | |
261 'old_path': old_path, | |
262 'old': old}) | |
263 add_link(req, 'alternate', '?format=diff&'+diff_params, 'Unified Diff', | |
264 'text/plain', 'diff') | |
265 add_link(req, 'alternate', '?format=zip&'+diff_params, 'Zip Archive', | |
266 'application/zip', 'zip') | |
267 add_stylesheet(req, 'common/css/changeset.css') | |
268 add_stylesheet(req, 'common/css/diff.css') | |
269 add_stylesheet(req, 'common/css/code.css') | |
270 return 'changeset.cs', None | |
271 | |
272 # Internal methods | |
273 | |
274 def _render_html(self, req, repos, chgset, restricted, message, | |
275 diff, diff_options): | |
276 """HTML version""" | |
277 req.hdf['changeset'] = { | |
278 'chgset': chgset and True, | |
279 'restricted': restricted, | |
280 'href': { | |
281 'new_rev': req.href.changeset(diff.new_rev), | |
282 'old_rev': req.href.changeset(diff.old_rev), | |
283 'new_path': req.href.browser(diff.new_path, rev=diff.new_rev), | |
284 'old_path': req.href.browser(diff.old_path, rev=diff.old_rev) | |
285 } | |
286 } | |
287 | |
288 if chgset: # Changeset Mode (possibly restricted on a path) | |
289 path, rev = diff.new_path, diff.new_rev | |
290 | |
291 # -- getting the change summary from the Changeset.get_changes | |
292 def get_changes(): | |
293 for npath, kind, change, opath, orev in chgset.get_changes(): | |
294 old_node = new_node = None | |
295 if (restricted and | |
296 not (npath == path or # same path | |
297 npath.startswith(path + '/') or # npath is below | |
298 path.startswith(npath + '/'))): # npath is above | |
299 continue | |
300 if change != Changeset.ADD: | |
301 old_node = repos.get_node(opath, orev) | |
302 if change != Changeset.DELETE: | |
303 new_node = repos.get_node(npath, rev) | |
304 yield old_node, new_node, kind, change | |
305 | |
306 def _changeset_title(rev): | |
307 if restricted: | |
308 return 'Changeset %s for %s' % (rev, path) | |
309 else: | |
310 return 'Changeset %s' % rev | |
311 | |
312 title = _changeset_title(rev) | |
313 properties = [] | |
314 for name, value, wikiflag, htmlclass in chgset.get_properties(): | |
315 if wikiflag: | |
316 value = wiki_to_html(value or '', self.env, req) | |
317 properties.append({'name': name, 'value': value, | |
318 'htmlclass': htmlclass}) | |
319 | |
320 req.hdf['changeset'] = { | |
321 'revision': chgset.rev, | |
322 'time': format_datetime(chgset.date), | |
323 'age': pretty_timedelta(chgset.date, None, 3600), | |
324 'author': chgset.author or 'anonymous', | |
325 'message': message, 'properties': properties | |
326 } | |
327 oldest_rev = repos.oldest_rev | |
328 if chgset.rev != oldest_rev: | |
329 if restricted: | |
330 prev = repos.get_node(path, rev).get_previous() | |
331 if prev: | |
332 prev_path, prev_rev = prev[:2] | |
333 if prev_rev: | |
334 prev_href = req.href.changeset(prev_rev, prev_path) | |
335 else: | |
336 prev_path = prev_rev = None | |
337 else: | |
338 add_link(req, 'first', req.href.changeset(oldest_rev), | |
339 'Changeset %s' % oldest_rev) | |
340 prev_path = diff.old_path | |
341 prev_rev = repos.previous_rev(chgset.rev) | |
342 if prev_rev: | |
343 prev_href = req.href.changeset(prev_rev) | |
344 if prev_rev: | |
345 add_link(req, 'prev', prev_href, _changeset_title(prev_rev)) | |
346 youngest_rev = repos.youngest_rev | |
347 if str(chgset.rev) != str(youngest_rev): | |
348 if restricted: | |
349 next_rev = repos.next_rev(chgset.rev, path) | |
350 if next_rev: | |
351 next_href = req.href.changeset(next_rev, path) | |
352 else: | |
353 add_link(req, 'last', req.href.changeset(youngest_rev), | |
354 'Changeset %s' % youngest_rev) | |
355 next_rev = repos.next_rev(chgset.rev) | |
356 if next_rev: | |
357 next_href = req.href.changeset(next_rev) | |
358 if next_rev: | |
359 add_link(req, 'next', next_href, _changeset_title(next_rev)) | |
360 | |
361 else: # Diff Mode | |
362 # -- getting the change summary from the Repository.get_changes | |
363 def get_changes(): | |
364 for d in repos.get_changes(**diff): | |
365 yield d | |
366 | |
367 reverse_href = req.href.changeset(diff.old_rev, diff.old_path, | |
368 old=diff.new_rev, | |
369 old_path=diff.new_path) | |
370 req.hdf['changeset.reverse_href'] = reverse_href | |
371 req.hdf['changeset.href.log'] = req.href.log( | |
372 diff.new_path, rev=diff.new_rev, stop_rev=diff.old_rev) | |
373 title = self.title_for_diff(diff) | |
374 req.hdf['title'] = title | |
375 | |
376 if not req.perm.has_permission('BROWSER_VIEW'): | |
377 return | |
378 | |
379 def _change_info(old_node, new_node, change): | |
380 info = {'change': change} | |
381 if old_node: | |
382 info['path.old'] = old_node.path | |
383 info['rev.old'] = old_node.rev | |
384 info['shortrev.old'] = repos.short_rev(old_node.rev) | |
385 old_href = req.href.browser(old_node.created_path, | |
386 rev=old_node.created_rev) | |
387 # Reminder: old_node.path may not exist at old_node.rev | |
388 # as long as old_node.rev==old_node.created_rev | |
389 # ... and diff.old_rev may have nothing to do | |
390 # with _that_ node specific history... | |
391 info['browser_href.old'] = old_href | |
392 if new_node: | |
393 info['path.new'] = new_node.path | |
394 info['rev.new'] = new_node.rev # created rev. | |
395 info['shortrev.new'] = repos.short_rev(new_node.rev) | |
396 new_href = req.href.browser(new_node.created_path, | |
397 rev=new_node.created_rev) | |
398 # (same remark as above) | |
399 info['browser_href.new'] = new_href | |
400 return info | |
401 | |
402 hidden_properties = self.config.getlist('browser', 'hide_properties') | |
403 | |
404 def _prop_changes(old_node, new_node): | |
405 old_props = old_node.get_properties() | |
406 new_props = new_node.get_properties() | |
407 changed_props = {} | |
408 if old_props != new_props: | |
409 for k,v in old_props.items(): | |
410 if not k in new_props: | |
411 changed_props[k] = { | |
412 'old': render_node_property(self.env, k, v)} | |
413 elif v != new_props[k]: | |
414 changed_props[k] = { | |
415 'old': render_node_property(self.env, k, v), | |
416 'new': render_node_property(self.env, k, | |
417 new_props[k])} | |
418 for k,v in new_props.items(): | |
419 if not k in old_props: | |
420 changed_props[k] = { | |
421 'new': render_node_property(self.env, k, v)} | |
422 for k in hidden_properties: | |
423 if k in changed_props: | |
424 del changed_props[k] | |
425 changed_properties = [] | |
426 for name, props in changed_props.iteritems(): | |
427 props.update({'name': name}) | |
428 changed_properties.append(props) | |
429 return changed_properties | |
430 | |
431 def _estimate_changes(old_node, new_node): | |
432 old_size = old_node.get_content_length() | |
433 new_size = new_node.get_content_length() | |
434 return old_size + new_size | |
435 | |
436 def _content_changes(old_node, new_node): | |
437 """Returns the list of differences. | |
438 | |
439 The list is empty when no differences between comparable files | |
440 are detected, but the return value is None for non-comparable files. | |
441 """ | |
442 old_content = old_node.get_content().read() | |
443 if is_binary(old_content): | |
444 return None | |
445 | |
446 new_content = new_node.get_content().read() | |
447 if is_binary(new_content): | |
448 return None | |
449 | |
450 mview = Mimeview(self.env) | |
451 old_content = mview.to_unicode(old_content, old_node.content_type) | |
452 new_content = mview.to_unicode(new_content, new_node.content_type) | |
453 | |
454 if old_content != new_content: | |
455 context = 3 | |
456 options = diff_options[1] | |
457 for option in options: | |
458 if option.startswith('-U'): | |
459 context = int(option[2:]) | |
460 break | |
461 if context < 0: | |
462 context = None | |
463 tabwidth = self.config['diff'].getint('tab_width', | |
464 self.config['mimeviewer'].getint('tab_width')) | |
465 return hdf_diff(old_content.splitlines(), | |
466 new_content.splitlines(), | |
467 context, tabwidth, | |
468 ignore_blank_lines='-B' in options, | |
469 ignore_case='-i' in options, | |
470 ignore_space_changes='-b' in options) | |
471 else: | |
472 return [] | |
473 | |
474 if req.perm.has_permission('FILE_VIEW'): | |
475 diff_bytes = diff_files = 0 | |
476 if self.max_diff_bytes or self.max_diff_files: | |
477 for old_node, new_node, kind, change in get_changes(): | |
478 if change == Changeset.EDIT and kind == Node.FILE: | |
479 diff_files += 1 | |
480 diff_bytes += _estimate_changes(old_node, new_node) | |
481 show_diffs = (not self.max_diff_files or \ | |
482 diff_files <= self.max_diff_files) and \ | |
483 (not self.max_diff_bytes or \ | |
484 diff_bytes <= self.max_diff_bytes or \ | |
485 diff_files == 1) | |
486 else: | |
487 show_diffs = False | |
488 | |
489 idx = 0 | |
490 for old_node, new_node, kind, change in get_changes(): | |
491 show_entry = change != Changeset.EDIT | |
492 if change in (Changeset.EDIT, Changeset.COPY, Changeset.MOVE) and \ | |
493 req.perm.has_permission('FILE_VIEW'): | |
494 assert old_node and new_node | |
495 props = _prop_changes(old_node, new_node) | |
496 if props: | |
497 req.hdf['changeset.changes.%d.props' % idx] = props | |
498 show_entry = True | |
499 if kind == Node.FILE and show_diffs: | |
500 diffs = _content_changes(old_node, new_node) | |
501 if diffs != []: | |
502 if diffs: | |
503 req.hdf['changeset.changes.%d.diff' % idx] = diffs | |
504 # elif None (means: manually compare to (previous)) | |
505 show_entry = True | |
506 if show_entry or not show_diffs: | |
507 info = _change_info(old_node, new_node, change) | |
508 if change == Changeset.EDIT and not show_diffs: | |
509 if chgset: | |
510 diff_href = req.href.changeset(new_node.rev, | |
511 new_node.path) | |
512 else: | |
513 diff_href = req.href.changeset( | |
514 new_node.created_rev, new_node.created_path, | |
515 old=old_node.created_rev, | |
516 old_path=old_node.created_path) | |
517 info['diff_href'] = diff_href | |
518 req.hdf['changeset.changes.%d' % idx] = info | |
519 idx += 1 # the sequence should be immutable | |
520 | |
521 def _render_diff(self, req, filename, repos, diff, diff_options): | |
522 """Raw Unified Diff version""" | |
523 req.send_response(200) | |
524 req.send_header('Content-Type', 'text/plain;charset=utf-8') | |
525 req.send_header('Content-Disposition', 'inline;' | |
526 'filename=%s.diff' % filename) | |
527 req.end_headers() | |
528 | |
529 mimeview = Mimeview(self.env) | |
530 for old_node, new_node, kind, change in repos.get_changes(**diff): | |
531 # TODO: Property changes | |
532 | |
533 # Content changes | |
534 if kind == Node.DIRECTORY: | |
535 continue | |
536 | |
537 new_content = old_content = '' | |
538 new_node_info = old_node_info = ('','') | |
539 mimeview = Mimeview(self.env) | |
540 | |
541 if old_node: | |
542 old_content = old_node.get_content().read() | |
543 if is_binary(old_content): | |
544 continue | |
545 old_node_info = (old_node.path, old_node.rev) | |
546 old_content = mimeview.to_unicode(old_content, | |
547 old_node.content_type) | |
548 if new_node: | |
549 new_content = new_node.get_content().read() | |
550 if is_binary(new_content): | |
551 continue | |
552 new_node_info = (new_node.path, new_node.rev) | |
553 new_path = new_node.path | |
554 new_content = mimeview.to_unicode(new_content, | |
555 new_node.content_type) | |
556 else: | |
557 old_node_path = repos.normalize_path(old_node.path) | |
558 diff_old_path = repos.normalize_path(diff.old_path) | |
559 new_path = posixpath.join(diff.new_path, | |
560 old_node_path[len(diff_old_path)+1:]) | |
561 | |
562 if old_content != new_content: | |
563 context = 3 | |
564 options = diff_options[1] | |
565 for option in options: | |
566 if option.startswith('-U'): | |
567 context = int(option[2:]) | |
568 break | |
569 if not old_node_info[0]: | |
570 old_node_info = new_node_info # support for 'A'dd changes | |
571 req.write('Index: ' + new_path + CRLF) | |
572 req.write('=' * 67 + CRLF) | |
573 req.write('--- %s (revision %s)' % old_node_info + CRLF) | |
574 req.write('+++ %s (revision %s)' % new_node_info + CRLF) | |
575 for line in unified_diff(old_content.splitlines(), | |
576 new_content.splitlines(), context, | |
577 ignore_blank_lines='-B' in options, | |
578 ignore_case='-i' in options, | |
579 ignore_space_changes='-b' in options): | |
580 req.write(line + CRLF) | |
581 | |
582 def _render_zip(self, req, filename, repos, diff): | |
583 """ZIP archive with all the added and/or modified files.""" | |
584 new_rev = diff.new_rev | |
585 req.send_response(200) | |
586 req.send_header('Content-Type', 'application/zip') | |
587 req.send_header('Content-Disposition', 'attachment;' | |
588 'filename=%s.zip' % filename) | |
589 | |
590 from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED | |
591 | |
592 buf = StringIO() | |
593 zipfile = ZipFile(buf, 'w', ZIP_DEFLATED) | |
594 for old_node, new_node, kind, change in repos.get_changes(**diff): | |
595 if kind == Node.FILE and change != Changeset.DELETE: | |
596 assert new_node | |
597 zipinfo = ZipInfo() | |
598 zipinfo.filename = new_node.path.encode('utf-8') | |
599 # Note: unicode filenames are not supported by zipfile. | |
600 # UTF-8 is not supported by all Zip tools either, | |
601 # but as some does, I think UTF-8 is the best option here. | |
602 zipinfo.date_time = time.gmtime(new_node.last_modified)[:6] | |
603 zipinfo.compress_type = ZIP_DEFLATED | |
604 zipfile.writestr(zipinfo, new_node.get_content().read()) | |
605 zipfile.close() | |
606 | |
607 buf.seek(0, 2) # be sure to be at the end | |
608 req.send_header("Content-Length", buf.tell()) | |
609 req.end_headers() | |
610 | |
611 req.write(buf.getvalue()) | |
612 | |
613 def title_for_diff(self, diff): | |
614 if diff.new_path == diff.old_path: # ''diff between 2 revisions'' mode | |
615 return 'Diff r%s:%s for %s' \ | |
616 % (diff.old_rev or 'latest', diff.new_rev or 'latest', | |
617 diff.new_path or '/') | |
618 else: # ''arbitrary diff'' mode | |
619 return 'Diff from %s@%s to %s@%s' \ | |
620 % (diff.old_path or '/', diff.old_rev or 'latest', | |
621 diff.new_path or '/', diff.new_rev or 'latest') | |
622 | |
623 # ITimelineEventProvider methods | |
624 | |
625 def get_timeline_filters(self, req): | |
626 if req.perm.has_permission('CHANGESET_VIEW'): | |
627 yield ('changeset', 'Repository checkins') | |
628 | |
629 def get_timeline_events(self, req, start, stop, filters): | |
630 if 'changeset' in filters: | |
631 format = req.args.get('format') | |
632 wiki_format = self.wiki_format_messages | |
633 show_files = self.timeline_show_files | |
634 db = self.env.get_db_cnx() | |
635 repos = self.env.get_repository(req.authname) | |
636 for chgset in repos.get_changesets(start, stop): | |
637 message = chgset.message or '--' | |
638 if wiki_format: | |
639 shortlog = wiki_to_oneliner(message, self.env, db, | |
640 shorten=True) | |
641 else: | |
642 shortlog = shorten_line(message) | |
643 | |
644 if format == 'rss': | |
645 title = Markup('Changeset [%s]: %s', chgset.rev, shortlog) | |
646 href = req.abs_href.changeset(chgset.rev) | |
647 if wiki_format: | |
648 message = wiki_to_html(message, self.env, req, db, | |
649 absurls=True) | |
650 else: | |
651 message = html.PRE(message) | |
652 else: | |
653 title = Markup('Changeset <em>[%s]</em> by %s', chgset.rev, | |
654 chgset.author) | |
655 href = req.href.changeset(chgset.rev) | |
656 | |
657 if wiki_format: | |
658 if self.timeline_long_messages: | |
659 message = wiki_to_html(message, self.env, req, db, | |
660 absurls=True) | |
661 else: | |
662 message = wiki_to_oneliner(message, self.env, db, | |
663 shorten=True) | |
664 else: | |
665 message = shortlog | |
666 | |
667 if show_files and req.perm.has_permission('BROWSER_VIEW'): | |
668 files = [] | |
669 for chg in chgset.get_changes(): | |
670 if show_files > 0 and len(files) >= show_files: | |
671 files.append(html.LI(Markup('…'))) | |
672 break | |
673 files.append(html.LI(html.DIV(class_=chg[2]), | |
674 chg[0] or '/')) | |
675 message = html.UL(files, class_="changes") + message | |
676 | |
677 yield 'changeset', href, title, chgset.date, chgset.author,\ | |
678 message | |
679 | |
680 # IWikiSyntaxProvider methods | |
681 | |
682 CHANGESET_ID = r"(?:\d+|[a-fA-F\d]{6,})" # only "long enough" hexa ids | |
683 | |
684 def get_wiki_syntax(self): | |
685 yield ( | |
686 # [...] form: start with optional intertrac: [T... or [trac ... | |
687 r"!?\[(?P<it_changeset>%s\s*)" % Formatter.INTERTRAC_SCHEME + | |
688 # hex digits + optional /path for the restricted changeset | |
689 r"%s(?:/[^\]]*)?\]|" % self.CHANGESET_ID + | |
690 # r... form: allow r1 but not r1:2 (handled by the log syntax) | |
691 r"(?:\b|!)r%s\b(?!:%s)" % ((self.CHANGESET_ID,)*2), | |
692 lambda x, y, z: | |
693 self._format_changeset_link(x, 'changeset', | |
694 y[0] == 'r' and y[1:] or y[1:-1], | |
695 y, z)) | |
696 | |
697 def get_link_resolvers(self): | |
698 yield ('changeset', self._format_changeset_link) | |
699 yield ('diff', self._format_diff_link) | |
700 | |
701 def _format_changeset_link(self, formatter, ns, chgset, label, | |
702 fullmatch=None): | |
703 intertrac = formatter.shorthand_intertrac_helper(ns, chgset, label, | |
704 fullmatch) | |
705 if intertrac: | |
706 return intertrac | |
707 sep = chgset.find('/') | |
708 if sep > 0: | |
709 rev, path = chgset[:sep], chgset[sep:] | |
710 else: | |
711 rev, path = chgset, None | |
712 cursor = formatter.db.cursor() | |
713 cursor.execute('SELECT message FROM revision WHERE rev=%s', (rev,)) | |
714 row = cursor.fetchone() | |
715 if row: | |
716 return html.A(label, class_="changeset", | |
717 title=shorten_line(row[0]), | |
718 href=formatter.href.changeset(rev, path)) | |
719 else: | |
720 return html.A(label, class_="missing changeset", | |
721 href=formatter.href.changeset(rev, path), | |
722 rel="nofollow") | |
723 | |
724 def _format_diff_link(self, formatter, ns, params, label): | |
725 def pathrev(path): | |
726 if '@' in path: | |
727 return path.split('@', 1) | |
728 else: | |
729 return (path, None) | |
730 if '//' in params: | |
731 p1, p2 = params.split('//', 1) | |
732 old, new = pathrev(p1), pathrev(p2) | |
733 diff = DiffArgs(old_path=old[0], old_rev=old[1], | |
734 new_path=new[0], new_rev=new[1]) | |
735 else: | |
736 old_path, old_rev = pathrev(params) | |
737 new_rev = None | |
738 if old_rev and ':' in old_rev: | |
739 old_rev, new_rev = old_rev.split(':', 1) | |
740 diff = DiffArgs(old_path=old_path, old_rev=old_rev, | |
741 new_path=old_path, new_rev=new_rev) | |
742 title = self.title_for_diff(diff) | |
743 href = formatter.href.changeset(new_path=diff.new_path or None, | |
744 new=diff.new_rev, | |
745 old_path=diff.old_path or None, | |
746 old=diff.old_rev) | |
747 return html.A(label, class_="changeset", title=title, href=href) | |
748 | |
749 # ISearchSource methods | |
750 | |
751 def get_search_filters(self, req): | |
752 if req.perm.has_permission('CHANGESET_VIEW'): | |
753 yield ('changeset', 'Changesets') | |
754 | |
755 def get_search_results(self, req, terms, filters): | |
756 if not 'changeset' in filters: | |
757 return | |
758 authzperm = SubversionAuthorizer(self.env, req.authname) | |
759 db = self.env.get_db_cnx() | |
760 sql, args = search_to_sql(db, ['message', 'author'], terms) | |
761 cursor = db.cursor() | |
762 cursor.execute("SELECT rev,time,author,message " | |
763 "FROM revision WHERE " + sql, args) | |
764 for rev, date, author, log in cursor: | |
765 if not authzperm.has_permission_for_changeset(rev): | |
766 continue | |
767 yield (req.href.changeset(rev), | |
768 '[%s]: %s' % (rev, shorten_line(log)), | |
769 date, author, shorten_result(log, terms)) | |
770 | |
771 | |
772 class AnyDiffModule(Component): | |
773 | |
774 implements(IRequestHandler) | |
775 | |
776 # IRequestHandler methods | |
777 | |
778 def match_request(self, req): | |
779 return re.match(r'/anydiff$', req.path_info) | |
780 | |
781 def process_request(self, req): | |
782 # -- retrieve arguments | |
783 new_path = req.args.get('new_path') | |
784 new_rev = req.args.get('new_rev') | |
785 old_path = req.args.get('old_path') | |
786 old_rev = req.args.get('old_rev') | |
787 | |
788 # -- normalize | |
789 repos = self.env.get_repository(req.authname) | |
790 new_path = repos.normalize_path(new_path) | |
791 new_rev = repos.normalize_rev(new_rev) | |
792 old_path = repos.normalize_path(old_path) | |
793 old_rev = repos.normalize_rev(old_rev) | |
794 | |
795 authzperm = SubversionAuthorizer(self.env, req.authname) | |
796 authzperm.assert_permission_for_changeset(new_rev) | |
797 authzperm.assert_permission_for_changeset(old_rev) | |
798 | |
799 # -- prepare rendering | |
800 req.hdf['anydiff'] = { | |
801 'new_path': new_path, | |
802 'new_rev': new_rev, | |
803 'old_path': old_path, | |
804 'old_rev': old_rev, | |
805 'changeset_href': req.href.changeset(), | |
806 } | |
807 | |
808 return 'anydiff.cs', None |