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('&hellip;')))
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
Copyright (C) 2012-2017 Edgewall Software