##// END OF EJS Templates
fixes #326 some html special chars where not escaped in diffs + code garden in helpers
marcink -
r1781:089c81cf beta
parent child Browse files
Show More
@@ -1,447 +1,447 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7
8
7
8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import difflib
30 30
31 31 from itertools import tee, imap
32 32
33 from mercurial.match import match
34
35 33 from vcs.exceptions import VCSError
36 34 from vcs.nodes import FileNode
35 import markupsafe
36
37 37
38 38 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
39 39 """
40 40 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
41
41
42 42 :param ignore_whitespace: ignore whitespaces in diff
43 43 """
44 44
45 45 for filenode in (filenode_old, filenode_new):
46 46 if not isinstance(filenode, FileNode):
47 47 raise VCSError("Given object should be FileNode object, not %s"
48 48 % filenode.__class__)
49 49
50 50 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
51 51 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
52 52
53 53 repo = filenode_new.changeset.repository
54 54 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
55 55 ignore_whitespace, context)
56 56
57 57 return vcs_gitdiff
58 58
59 59
60 60 class DiffProcessor(object):
61 61 """
62 62 Give it a unified diff and it returns a list of the files that were
63 63 mentioned in the diff together with a dict of meta information that
64 64 can be used to render it in a HTML template.
65 65 """
66 66 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
67 67
68 68 def __init__(self, diff, differ='diff', format='udiff'):
69 69 """
70 70 :param diff: a text in diff format or generator
71 71 :param format: format of diff passed, `udiff` or `gitdiff`
72 72 """
73 73 if isinstance(diff, basestring):
74 74 diff = [diff]
75 75
76 76 self.__udiff = diff
77 77 self.__format = format
78 78 self.adds = 0
79 79 self.removes = 0
80 80
81 81 if isinstance(self.__udiff, basestring):
82 82 self.lines = iter(self.__udiff.splitlines(1))
83 83
84 84 elif self.__format == 'gitdiff':
85 85 udiff_copy = self.copy_iterator()
86 86 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
87 87 else:
88 88 udiff_copy = self.copy_iterator()
89 89 self.lines = imap(self.escaper, udiff_copy)
90 90
91 91 # Select a differ.
92 92 if differ == 'difflib':
93 93 self.differ = self._highlight_line_difflib
94 94 else:
95 95 self.differ = self._highlight_line_udiff
96 96
97 97 def escaper(self, string):
98 return string.replace('<', '&lt;').replace('>', '&gt;')
98 return markupsafe.escape(string)
99 99
100 100 def copy_iterator(self):
101 101 """
102 102 make a fresh copy of generator, we should not iterate thru
103 103 an original as it's needed for repeating operations on
104 104 this instance of DiffProcessor
105 105 """
106 106 self.__udiff, iterator_copy = tee(self.__udiff)
107 107 return iterator_copy
108 108
109 109 def _extract_rev(self, line1, line2):
110 110 """
111 111 Extract the filename and revision hint from a line.
112 112 """
113 113
114 114 try:
115 115 if line1.startswith('--- ') and line2.startswith('+++ '):
116 116 l1 = line1[4:].split(None, 1)
117 117 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
118 118 old_rev = l1[1] if len(l1) == 2 else 'old'
119 119
120 120 l2 = line2[4:].split(None, 1)
121 121 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
122 122 new_rev = l2[1] if len(l2) == 2 else 'new'
123 123
124 124 filename = old_filename if (old_filename !=
125 125 'dev/null') else new_filename
126 126
127 127 return filename, new_rev, old_rev
128 128 except (ValueError, IndexError):
129 129 pass
130 130
131 131 return None, None, None
132 132
133 133 def _parse_gitdiff(self, diffiterator):
134 134 def line_decoder(l):
135 135 if l.startswith('+') and not l.startswith('+++'):
136 136 self.adds += 1
137 137 elif l.startswith('-') and not l.startswith('---'):
138 138 self.removes += 1
139 139 return l.decode('utf8', 'replace')
140 140
141 141 output = list(diffiterator)
142 142 size = len(output)
143 143
144 144 if size == 2:
145 145 l = []
146 146 l.extend([output[0]])
147 147 l.extend(output[1].splitlines(1))
148 148 return map(line_decoder, l)
149 149 elif size == 1:
150 150 return map(line_decoder, output[0].splitlines(1))
151 151 elif size == 0:
152 152 return []
153 153
154 154 raise Exception('wrong size of diff %s' % size)
155 155
156 def _highlight_line_difflib(self, line, next):
156 def _highlight_line_difflib(self, line, next_):
157 157 """
158 158 Highlight inline changes in both lines.
159 159 """
160 160
161 161 if line['action'] == 'del':
162 old, new = line, next
162 old, new = line, next_
163 163 else:
164 old, new = next, line
164 old, new = next_, line
165 165
166 166 oldwords = re.split(r'(\W)', old['line'])
167 167 newwords = re.split(r'(\W)', new['line'])
168 168
169 169 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
170 170
171 171 oldfragments, newfragments = [], []
172 172 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
173 173 oldfrag = ''.join(oldwords[i1:i2])
174 174 newfrag = ''.join(newwords[j1:j2])
175 175 if tag != 'equal':
176 176 if oldfrag:
177 177 oldfrag = '<del>%s</del>' % oldfrag
178 178 if newfrag:
179 179 newfrag = '<ins>%s</ins>' % newfrag
180 180 oldfragments.append(oldfrag)
181 181 newfragments.append(newfrag)
182 182
183 183 old['line'] = "".join(oldfragments)
184 184 new['line'] = "".join(newfragments)
185 185
186 def _highlight_line_udiff(self, line, next):
186 def _highlight_line_udiff(self, line, next_):
187 187 """
188 188 Highlight inline changes in both lines.
189 189 """
190 190 start = 0
191 limit = min(len(line['line']), len(next['line']))
192 while start < limit and line['line'][start] == next['line'][start]:
191 limit = min(len(line['line']), len(next_['line']))
192 while start < limit and line['line'][start] == next_['line'][start]:
193 193 start += 1
194 194 end = -1
195 195 limit -= start
196 while -end <= limit and line['line'][end] == next['line'][end]:
196 while -end <= limit and line['line'][end] == next_['line'][end]:
197 197 end -= 1
198 198 end += 1
199 199 if start or end:
200 200 def do(l):
201 201 last = end + len(l['line'])
202 202 if l['action'] == 'add':
203 203 tag = 'ins'
204 204 else:
205 205 tag = 'del'
206 206 l['line'] = '%s<%s>%s</%s>%s' % (
207 207 l['line'][:start],
208 208 tag,
209 209 l['line'][start:last],
210 210 tag,
211 211 l['line'][last:]
212 212 )
213 213 do(line)
214 do(next)
214 do(next_)
215 215
216 216 def _parse_udiff(self):
217 217 """
218 218 Parse the diff an return data for the template.
219 219 """
220 220 lineiter = self.lines
221 221 files = []
222 222 try:
223 223 line = lineiter.next()
224 224 # skip first context
225 225 skipfirst = True
226 226 while 1:
227 227 # continue until we found the old file
228 228 if not line.startswith('--- '):
229 229 line = lineiter.next()
230 230 continue
231 231
232 232 chunks = []
233 233 filename, old_rev, new_rev = \
234 234 self._extract_rev(line, lineiter.next())
235 235 files.append({
236 236 'filename': filename,
237 237 'old_revision': old_rev,
238 238 'new_revision': new_rev,
239 239 'chunks': chunks
240 240 })
241 241
242 242 line = lineiter.next()
243 243 while line:
244 244 match = self._chunk_re.match(line)
245 245 if not match:
246 246 break
247 247
248 248 lines = []
249 249 chunks.append(lines)
250 250
251 251 old_line, old_end, new_line, new_end = \
252 252 [int(x or 1) for x in match.groups()[:-1]]
253 253 old_line -= 1
254 254 new_line -= 1
255 255 context = len(match.groups()) == 5
256 256 old_end += old_line
257 257 new_end += new_line
258 258
259 259 if context:
260 260 if not skipfirst:
261 261 lines.append({
262 262 'old_lineno': '...',
263 263 'new_lineno': '...',
264 264 'action': 'context',
265 265 'line': line,
266 266 })
267 267 else:
268 268 skipfirst = False
269 269
270 270 line = lineiter.next()
271 271 while old_line < old_end or new_line < new_end:
272 272 if line:
273 273 command, line = line[0], line[1:]
274 274 else:
275 275 command = ' '
276 276 affects_old = affects_new = False
277 277
278 278 # ignore those if we don't expect them
279 279 if command in '#@':
280 280 continue
281 281 elif command == '+':
282 282 affects_new = True
283 283 action = 'add'
284 284 elif command == '-':
285 285 affects_old = True
286 286 action = 'del'
287 287 else:
288 288 affects_old = affects_new = True
289 289 action = 'unmod'
290 290
291 291 old_line += affects_old
292 292 new_line += affects_new
293 293 lines.append({
294 294 'old_lineno': affects_old and old_line or '',
295 295 'new_lineno': affects_new and new_line or '',
296 296 'action': action,
297 297 'line': line
298 298 })
299 299 line = lineiter.next()
300 300
301 301 except StopIteration:
302 302 pass
303 303
304 304 # highlight inline changes
305 for file in files:
305 for _ in files:
306 306 for chunk in chunks:
307 307 lineiter = iter(chunk)
308 308 #first = True
309 309 try:
310 310 while 1:
311 311 line = lineiter.next()
312 312 if line['action'] != 'unmod':
313 313 nextline = lineiter.next()
314 314 if nextline['action'] == 'unmod' or \
315 315 nextline['action'] == line['action']:
316 316 continue
317 317 self.differ(line, nextline)
318 318 except StopIteration:
319 319 pass
320 320
321 321 return files
322 322
323 323 def prepare(self):
324 324 """
325 325 Prepare the passed udiff for HTML rendering. It'l return a list
326 326 of dicts
327 327 """
328 328 return self._parse_udiff()
329 329
330 330 def _safe_id(self, idstring):
331 331 """Make a string safe for including in an id attribute.
332 332
333 333 The HTML spec says that id attributes 'must begin with
334 334 a letter ([A-Za-z]) and may be followed by any number
335 335 of letters, digits ([0-9]), hyphens ("-"), underscores
336 336 ("_"), colons (":"), and periods (".")'. These regexps
337 337 are slightly over-zealous, in that they remove colons
338 338 and periods unnecessarily.
339 339
340 340 Whitespace is transformed into underscores, and then
341 341 anything which is not a hyphen or a character that
342 342 matches \w (alphanumerics and underscore) is removed.
343 343
344 344 """
345 345 # Transform all whitespace to underscore
346 346 idstring = re.sub(r'\s', "_", '%s' % idstring)
347 347 # Remove everything that is not a hyphen or a member of \w
348 348 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
349 349 return idstring
350 350
351 351 def raw_diff(self):
352 352 """
353 353 Returns raw string as udiff
354 354 """
355 355 udiff_copy = self.copy_iterator()
356 356 if self.__format == 'gitdiff':
357 357 udiff_copy = self._parse_gitdiff(udiff_copy)
358 358 return u''.join(udiff_copy)
359 359
360 360 def as_html(self, table_class='code-difftable', line_class='line',
361 361 new_lineno_class='lineno old', old_lineno_class='lineno new',
362 362 code_class='code'):
363 363 """
364 364 Return udiff as html table with customized css classes
365 365 """
366 366 def _link_to_if(condition, label, url):
367 367 """
368 368 Generates a link if condition is meet or just the label if not.
369 369 """
370 370
371 371 if condition:
372 372 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
373 373 'label': label}
374 374 else:
375 375 return label
376 376 diff_lines = self.prepare()
377 377 _html_empty = True
378 378 _html = []
379 379 _html.append('''<table class="%(table_class)s">\n''' \
380 380 % {'table_class': table_class})
381 381 for diff in diff_lines:
382 382 for line in diff['chunks']:
383 383 _html_empty = False
384 384 for change in line:
385 385 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
386 386 % {'line_class': line_class,
387 387 'action': change['action']})
388 388 anchor_old_id = ''
389 389 anchor_new_id = ''
390 390 anchor_old = "%(filename)s_o%(oldline_no)s" % \
391 391 {'filename': self._safe_id(diff['filename']),
392 392 'oldline_no': change['old_lineno']}
393 393 anchor_new = "%(filename)s_n%(oldline_no)s" % \
394 394 {'filename': self._safe_id(diff['filename']),
395 395 'oldline_no': change['new_lineno']}
396 396 cond_old = change['old_lineno'] != '...' and \
397 397 change['old_lineno']
398 398 cond_new = change['new_lineno'] != '...' and \
399 399 change['new_lineno']
400 400 if cond_old:
401 401 anchor_old_id = 'id="%s"' % anchor_old
402 402 if cond_new:
403 403 anchor_new_id = 'id="%s"' % anchor_new
404 404 ###########################################################
405 405 # OLD LINE NUMBER
406 406 ###########################################################
407 407 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
408 408 % {'a_id': anchor_old_id,
409 409 'old_lineno_cls': old_lineno_class})
410 410
411 411 _html.append('''%(link)s''' \
412 412 % {'link':
413 413 _link_to_if(cond_old, change['old_lineno'], '#%s' \
414 414 % anchor_old)})
415 415 _html.append('''</td>\n''')
416 416 ###########################################################
417 417 # NEW LINE NUMBER
418 418 ###########################################################
419 419
420 420 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
421 421 % {'a_id': anchor_new_id,
422 422 'new_lineno_cls': new_lineno_class})
423 423
424 424 _html.append('''%(link)s''' \
425 425 % {'link':
426 426 _link_to_if(cond_new, change['new_lineno'], '#%s' \
427 427 % anchor_new)})
428 428 _html.append('''</td>\n''')
429 429 ###########################################################
430 430 # CODE
431 431 ###########################################################
432 432 _html.append('''\t<td class="%(code_class)s">''' \
433 433 % {'code_class': code_class})
434 434 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
435 435 % {'code': change['line']})
436 436 _html.append('''\t</td>''')
437 437 _html.append('''\n</tr>\n''')
438 438 _html.append('''</table>''')
439 439 if _html_empty:
440 440 return None
441 441 return ''.join(_html)
442 442
443 443 def stat(self):
444 444 """
445 445 Returns tuple of added, and removed lines for this instance
446 446 """
447 447 return self.adds, self.removes
@@ -1,739 +1,751 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11
12 12 from datetime import datetime
13 13 from pygments.formatters.html import HtmlFormatter
14 14 from pygments import highlight as code_highlight
15 15 from pylons import url, request, config
16 16 from pylons.i18n.translation import _, ungettext
17 17
18 18 from webhelpers.html import literal, HTML, escape
19 19 from webhelpers.html.tools import *
20 20 from webhelpers.html.builder import make_tag
21 21 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
22 22 end_form, file, form, hidden, image, javascript_link, link_to, \
23 23 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
24 24 submit, text, password, textarea, title, ul, xml_declaration, radio
25 25 from webhelpers.html.tools import auto_link, button_to, highlight, \
26 26 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
27 27 from webhelpers.number import format_byte_size, format_bit_size
28 28 from webhelpers.pylonslib import Flash as _Flash
29 29 from webhelpers.pylonslib.secure_form import secure_form
30 30 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
31 31 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
32 32 replace_whitespace, urlify, truncate, wrap_paragraphs
33 33 from webhelpers.date import time_ago_in_words
34 34 from webhelpers.paginate import Page
35 35 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
36 36 convert_boolean_attrs, NotGiven, _make_safe_id_component
37 37
38 38 from rhodecode.lib.annotate import annotate_highlight
39 39 from rhodecode.lib.utils import repo_name_slug
40 40 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
41 41 from rhodecode.lib.markup_renderer import MarkupRenderer
42 42
43 43 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
44 44 """
45 45 Reset button
46 46 """
47 47 _set_input_attrs(attrs, type, name, value)
48 48 _set_id_attr(attrs, id, name)
49 49 convert_boolean_attrs(attrs, ["disabled"])
50 50 return HTML.input(**attrs)
51 51
52 52 reset = _reset
53 53 safeid = _make_safe_id_component
54 54
55 55
56 56 def FID(raw_id,path):
57 57 """
58 58 Creates a uniqe ID for filenode based on it's path and revision
59 59
60 60 :param raw_id:
61 61 :param path:
62 62 """
63 63 return 'C-%s-%s' % (short_id(raw_id), safeid(safe_unicode(path)))
64 64
65 65
66 66 def get_token():
67 67 """Return the current authentication token, creating one if one doesn't
68 68 already exist.
69 69 """
70 70 token_key = "_authentication_token"
71 71 from pylons import session
72 72 if not token_key in session:
73 73 try:
74 74 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
75 75 except AttributeError: # Python < 2.4
76 76 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
77 77 session[token_key] = token
78 78 if hasattr(session, 'save'):
79 79 session.save()
80 80 return session[token_key]
81 81
82 82 class _GetError(object):
83 83 """Get error from form_errors, and represent it as span wrapped error
84 84 message
85 85
86 86 :param field_name: field to fetch errors for
87 87 :param form_errors: form errors dict
88 88 """
89 89
90 90 def __call__(self, field_name, form_errors):
91 91 tmpl = """<span class="error_msg">%s</span>"""
92 92 if form_errors and form_errors.has_key(field_name):
93 93 return literal(tmpl % form_errors.get(field_name))
94 94
95 95 get_error = _GetError()
96 96
97 97 class _ToolTip(object):
98 98
99 99 def __call__(self, tooltip_title, trim_at=50):
100 100 """Special function just to wrap our text into nice formatted
101 101 autowrapped text
102 102
103 103 :param tooltip_title:
104 104 """
105 105 return escape(tooltip_title)
106 106 tooltip = _ToolTip()
107 107
108 108 class _FilesBreadCrumbs(object):
109 109
110 110 def __call__(self, repo_name, rev, paths):
111 111 if isinstance(paths, str):
112 112 paths = safe_unicode(paths)
113 113 url_l = [link_to(repo_name, url('files_home',
114 114 repo_name=repo_name,
115 115 revision=rev, f_path=''))]
116 116 paths_l = paths.split('/')
117 117 for cnt, p in enumerate(paths_l):
118 118 if p != '':
119 119 url_l.append(link_to(p,
120 120 url('files_home',
121 121 repo_name=repo_name,
122 122 revision=rev,
123 123 f_path='/'.join(paths_l[:cnt + 1])
124 124 )
125 125 )
126 126 )
127 127
128 128 return literal('/'.join(url_l))
129 129
130 130 files_breadcrumbs = _FilesBreadCrumbs()
131 131
132 132 class CodeHtmlFormatter(HtmlFormatter):
133 133 """My code Html Formatter for source codes
134 134 """
135 135
136 136 def wrap(self, source, outfile):
137 137 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
138 138
139 139 def _wrap_code(self, source):
140 140 for cnt, it in enumerate(source):
141 141 i, t = it
142 142 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
143 143 yield i, t
144 144
145 145 def _wrap_tablelinenos(self, inner):
146 146 dummyoutfile = StringIO.StringIO()
147 147 lncount = 0
148 148 for t, line in inner:
149 149 if t:
150 150 lncount += 1
151 151 dummyoutfile.write(line)
152 152
153 153 fl = self.linenostart
154 154 mw = len(str(lncount + fl - 1))
155 155 sp = self.linenospecial
156 156 st = self.linenostep
157 157 la = self.lineanchors
158 158 aln = self.anchorlinenos
159 159 nocls = self.noclasses
160 160 if sp:
161 161 lines = []
162 162
163 163 for i in range(fl, fl + lncount):
164 164 if i % st == 0:
165 165 if i % sp == 0:
166 166 if aln:
167 167 lines.append('<a href="#%s%d" class="special">%*d</a>' %
168 168 (la, i, mw, i))
169 169 else:
170 170 lines.append('<span class="special">%*d</span>' % (mw, i))
171 171 else:
172 172 if aln:
173 173 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
174 174 else:
175 175 lines.append('%*d' % (mw, i))
176 176 else:
177 177 lines.append('')
178 178 ls = '\n'.join(lines)
179 179 else:
180 180 lines = []
181 181 for i in range(fl, fl + lncount):
182 182 if i % st == 0:
183 183 if aln:
184 184 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
185 185 else:
186 186 lines.append('%*d' % (mw, i))
187 187 else:
188 188 lines.append('')
189 189 ls = '\n'.join(lines)
190 190
191 191 # in case you wonder about the seemingly redundant <div> here: since the
192 192 # content in the other cell also is wrapped in a div, some browsers in
193 193 # some configurations seem to mess up the formatting...
194 194 if nocls:
195 195 yield 0, ('<table class="%stable">' % self.cssclass +
196 196 '<tr><td><div class="linenodiv" '
197 197 'style="background-color: #f0f0f0; padding-right: 10px">'
198 198 '<pre style="line-height: 125%">' +
199 199 ls + '</pre></div></td><td id="hlcode" class="code">')
200 200 else:
201 201 yield 0, ('<table class="%stable">' % self.cssclass +
202 202 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
203 203 ls + '</pre></div></td><td id="hlcode" class="code">')
204 204 yield 0, dummyoutfile.getvalue()
205 205 yield 0, '</td></tr></table>'
206 206
207 207
208 208 def pygmentize(filenode, **kwargs):
209 209 """pygmentize function using pygments
210 210
211 211 :param filenode:
212 212 """
213 213
214 214 return literal(code_highlight(filenode.content,
215 215 filenode.lexer, CodeHtmlFormatter(**kwargs)))
216 216
217
217 218 def pygmentize_annotation(repo_name, filenode, **kwargs):
218 """pygmentize function for annotation
219 """
220 pygmentize function for annotation
219 221
220 222 :param filenode:
221 223 """
222 224
223 225 color_dict = {}
226
224 227 def gen_color(n=10000):
225 228 """generator for getting n of evenly distributed colors using
226 229 hsv color and golden ratio. It always return same order of colors
227 230
228 231 :returns: RGB tuple
229 232 """
230 233
231 234 def hsv_to_rgb(h, s, v):
232 if s == 0.0: return v, v, v
233 i = int(h * 6.0) # XXX assume int() truncates!
235 if s == 0.0:
236 return v, v, v
237 i = int(h * 6.0) # XXX assume int() truncates!
234 238 f = (h * 6.0) - i
235 239 p = v * (1.0 - s)
236 240 q = v * (1.0 - s * f)
237 241 t = v * (1.0 - s * (1.0 - f))
238 242 i = i % 6
239 if i == 0: return v, t, p
240 if i == 1: return q, v, p
241 if i == 2: return p, v, t
242 if i == 3: return p, q, v
243 if i == 4: return t, p, v
244 if i == 5: return v, p, q
243 if i == 0:
244 return v, t, p
245 if i == 1:
246 return q, v, p
247 if i == 2:
248 return p, v, t
249 if i == 3:
250 return p, q, v
251 if i == 4:
252 return t, p, v
253 if i == 5:
254 return v, p, q
245 255
246 256 golden_ratio = 0.618033988749895
247 257 h = 0.22717784590367374
248 258
249 259 for _ in xrange(n):
250 260 h += golden_ratio
251 261 h %= 1
252 262 HSV_tuple = [h, 0.95, 0.95]
253 263 RGB_tuple = hsv_to_rgb(*HSV_tuple)
254 yield map(lambda x:str(int(x * 256)), RGB_tuple)
264 yield map(lambda x: str(int(x * 256)), RGB_tuple)
255 265
256 266 cgenerator = gen_color()
257 267
258 268 def get_color_string(cs):
259 if color_dict.has_key(cs):
269 if cs in color_dict:
260 270 col = color_dict[cs]
261 271 else:
262 272 col = color_dict[cs] = cgenerator.next()
263 273 return "color: rgb(%s)! important;" % (', '.join(col))
264 274
265 275 def url_func(repo_name):
266 276
267 277 def _url_func(changeset):
268 278 author = changeset.author
269 279 date = changeset.date
270 280 message = tooltip(changeset.message)
271 281
272 282 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
273 283 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
274 284 "</b> %s<br/></div>")
275 285
276 286 tooltip_html = tooltip_html % (author, date, message)
277 287 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
278 288 short_id(changeset.raw_id))
279 289 uri = link_to(
280 290 lnk_format,
281 291 url('changeset_home', repo_name=repo_name,
282 292 revision=changeset.raw_id),
283 293 style=get_color_string(changeset.raw_id),
284 294 class_='tooltip',
285 295 title=tooltip_html
286 296 )
287 297
288 298 uri += '\n'
289 299 return uri
290 300 return _url_func
291 301
292 302 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
293 303
304
294 305 def is_following_repo(repo_name, user_id):
295 306 from rhodecode.model.scm import ScmModel
296 307 return ScmModel().is_following_repo(repo_name, user_id)
297 308
298 309 flash = _Flash()
299 310
300 311 #==============================================================================
301 312 # SCM FILTERS available via h.
302 313 #==============================================================================
303 314 from vcs.utils import author_name, author_email
304 315 from rhodecode.lib import credentials_filter, age as _age
305 316 from rhodecode.model.db import User
306 317
307 age = lambda x:_age(x)
318 age = lambda x: _age(x)
308 319 capitalize = lambda x: x.capitalize()
309 320 email = author_email
310 321 short_id = lambda x: x[:12]
311 322 hide_credentials = lambda x: ''.join(credentials_filter(x))
312 323
313 324
314 325 def email_or_none(author):
315 326 _email = email(author)
316 327 if _email != '':
317 328 return _email
318 329
319 330 # See if it contains a username we can get an email from
320 331 user = User.get_by_username(author_name(author), case_insensitive=True,
321 332 cache=True)
322 333 if user is not None:
323 334 return user.email
324 335
325 336 # No valid email, not a valid user in the system, none!
326 337 return None
327 338
339
328 340 def person(author):
329 341 # attr to return from fetched user
330 342 person_getter = lambda usr: usr.username
331
343
332 344 # Valid email in the attribute passed, see if they're in the system
333 345 _email = email(author)
334 346 if _email != '':
335 347 user = User.get_by_email(_email, case_insensitive=True, cache=True)
336 348 if user is not None:
337 349 return person_getter(user)
338 350 return _email
339 351
340 352 # Maybe it's a username?
341 353 _author = author_name(author)
342 354 user = User.get_by_username(_author, case_insensitive=True,
343 355 cache=True)
344 356 if user is not None:
345 357 return person_getter(user)
346 358
347 359 # Still nothing? Just pass back the author name then
348 360 return _author
349 361
350 362 def bool2icon(value):
351 363 """Returns True/False values represented as small html image of true/false
352 364 icons
353 365
354 366 :param value: bool value
355 367 """
356 368
357 369 if value is True:
358 370 return HTML.tag('img', src=url("/images/icons/accept.png"),
359 371 alt=_('True'))
360 372
361 373 if value is False:
362 374 return HTML.tag('img', src=url("/images/icons/cancel.png"),
363 375 alt=_('False'))
364 376
365 377 return value
366 378
367 379
368 380 def action_parser(user_log, feed=False):
369 381 """This helper will action_map the specified string action into translated
370 382 fancy names with icons and links
371 383
372 384 :param user_log: user log instance
373 385 :param feed: use output for feeds (no html and fancy icons)
374 386 """
375 387
376 388 action = user_log.action
377 389 action_params = ' '
378 390
379 391 x = action.split(':')
380 392
381 393 if len(x) > 1:
382 394 action, action_params = x
383 395
384 396 def get_cs_links():
385 397 revs_limit = 3 #display this amount always
386 398 revs_top_limit = 50 #show upto this amount of changesets hidden
387 399 revs = action_params.split(',')
388 400 repo_name = user_log.repository.repo_name
389 401
390 402 from rhodecode.model.scm import ScmModel
391 403 repo = user_log.repository.scm_instance
392 404
393 405 message = lambda rev: get_changeset_safe(repo, rev).message
394 406 cs_links = []
395 407 cs_links.append(" " + ', '.join ([link_to(rev,
396 408 url('changeset_home',
397 409 repo_name=repo_name,
398 410 revision=rev), title=tooltip(message(rev)),
399 411 class_='tooltip') for rev in revs[:revs_limit] ]))
400 412
401 413 compare_view = (' <div class="compare_view tooltip" title="%s">'
402 414 '<a href="%s">%s</a> '
403 415 '</div>' % (_('Show all combined changesets %s->%s' \
404 416 % (revs[0], revs[-1])),
405 417 url('changeset_home', repo_name=repo_name,
406 418 revision='%s...%s' % (revs[0], revs[-1])
407 419 ),
408 420 _('compare view'))
409 421 )
410 422
411 423 if len(revs) > revs_limit:
412 424 uniq_id = revs[0]
413 425 html_tmpl = ('<span> %s '
414 426 '<a class="show_more" id="_%s" href="#more">%s</a> '
415 427 '%s</span>')
416 428 if not feed:
417 429 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
418 430 % (len(revs) - revs_limit),
419 431 _('revisions')))
420 432
421 433 if not feed:
422 434 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
423 435 else:
424 436 html_tmpl = '<span id="%s"> %s </span>'
425 437
426 438 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
427 439 url('changeset_home',
428 440 repo_name=repo_name, revision=rev),
429 441 title=message(rev), class_='tooltip')
430 442 for rev in revs[revs_limit:revs_top_limit]])))
431 443 if len(revs) > 1:
432 444 cs_links.append(compare_view)
433 445 return ''.join(cs_links)
434 446
435 447 def get_fork_name():
436 448 repo_name = action_params
437 449 return _('fork name ') + str(link_to(action_params, url('summary_home',
438 450 repo_name=repo_name,)))
439 451
440 452 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
441 453 'user_created_repo':(_('[created] repository'), None),
442 454 'user_created_fork':(_('[created] repository as fork'), None),
443 455 'user_forked_repo':(_('[forked] repository'), get_fork_name),
444 456 'user_updated_repo':(_('[updated] repository'), None),
445 457 'admin_deleted_repo':(_('[delete] repository'), None),
446 458 'admin_created_repo':(_('[created] repository'), None),
447 459 'admin_forked_repo':(_('[forked] repository'), None),
448 460 'admin_updated_repo':(_('[updated] repository'), None),
449 461 'push':(_('[pushed] into'), get_cs_links),
450 462 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
451 463 'push_remote':(_('[pulled from remote] into'), get_cs_links),
452 464 'pull':(_('[pulled] from'), None),
453 465 'started_following_repo':(_('[started following] repository'), None),
454 466 'stopped_following_repo':(_('[stopped following] repository'), None),
455 467 }
456 468
457 469 action_str = action_map.get(action, action)
458 470 if feed:
459 471 action = action_str[0].replace('[', '').replace(']', '')
460 472 else:
461 473 action = action_str[0].replace('[', '<span class="journal_highlight">')\
462 474 .replace(']', '</span>')
463 475
464 476 action_params_func = lambda :""
465 477
466 478 if callable(action_str[1]):
467 479 action_params_func = action_str[1]
468 480
469 481 return [literal(action), action_params_func]
470 482
471 483 def action_parser_icon(user_log):
472 484 action = user_log.action
473 485 action_params = None
474 486 x = action.split(':')
475 487
476 488 if len(x) > 1:
477 489 action, action_params = x
478 490
479 491 tmpl = """<img src="%s%s" alt="%s"/>"""
480 492 map = {'user_deleted_repo':'database_delete.png',
481 493 'user_created_repo':'database_add.png',
482 494 'user_created_fork':'arrow_divide.png',
483 495 'user_forked_repo':'arrow_divide.png',
484 496 'user_updated_repo':'database_edit.png',
485 497 'admin_deleted_repo':'database_delete.png',
486 498 'admin_created_repo':'database_add.png',
487 499 'admin_forked_repo':'arrow_divide.png',
488 500 'admin_updated_repo':'database_edit.png',
489 501 'push':'script_add.png',
490 502 'push_local':'script_edit.png',
491 503 'push_remote':'connect.png',
492 504 'pull':'down_16.png',
493 505 'started_following_repo':'heart_add.png',
494 506 'stopped_following_repo':'heart_delete.png',
495 507 }
496 508 return literal(tmpl % ((url('/images/icons/')),
497 509 map.get(action, action), action))
498 510
499 511
500 512 #==============================================================================
501 513 # PERMS
502 514 #==============================================================================
503 515 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
504 516 HasRepoPermissionAny, HasRepoPermissionAll
505 517
506 518 #==============================================================================
507 519 # GRAVATAR URL
508 520 #==============================================================================
509 521
510 522 def gravatar_url(email_address, size=30):
511 523 if (not str2bool(config['app_conf'].get('use_gravatar')) or
512 524 not email_address or email_address == 'anonymous@rhodecode.org'):
513 525 return url("/images/user%s.png" % size)
514 526
515 527 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
516 528 default = 'identicon'
517 529 baseurl_nossl = "http://www.gravatar.com/avatar/"
518 530 baseurl_ssl = "https://secure.gravatar.com/avatar/"
519 531 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
520 532
521 533 if isinstance(email_address, unicode):
522 534 #hashlib crashes on unicode items
523 535 email_address = safe_str(email_address)
524 536 # construct the url
525 537 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
526 538 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
527 539
528 540 return gravatar_url
529 541
530 542
531 543 #==============================================================================
532 544 # REPO PAGER, PAGER FOR REPOSITORY
533 545 #==============================================================================
534 546 class RepoPage(Page):
535 547
536 548 def __init__(self, collection, page=1, items_per_page=20,
537 549 item_count=None, url=None, **kwargs):
538 550
539 551 """Create a "RepoPage" instance. special pager for paging
540 552 repository
541 553 """
542 554 self._url_generator = url
543 555
544 556 # Safe the kwargs class-wide so they can be used in the pager() method
545 557 self.kwargs = kwargs
546 558
547 559 # Save a reference to the collection
548 560 self.original_collection = collection
549 561
550 562 self.collection = collection
551 563
552 564 # The self.page is the number of the current page.
553 565 # The first page has the number 1!
554 566 try:
555 567 self.page = int(page) # make it int() if we get it as a string
556 568 except (ValueError, TypeError):
557 569 self.page = 1
558 570
559 571 self.items_per_page = items_per_page
560 572
561 573 # Unless the user tells us how many items the collections has
562 574 # we calculate that ourselves.
563 575 if item_count is not None:
564 576 self.item_count = item_count
565 577 else:
566 578 self.item_count = len(self.collection)
567 579
568 580 # Compute the number of the first and last available page
569 581 if self.item_count > 0:
570 582 self.first_page = 1
571 583 self.page_count = int(math.ceil(float(self.item_count) /
572 584 self.items_per_page))
573 585 self.last_page = self.first_page + self.page_count - 1
574 586
575 587 # Make sure that the requested page number is the range of
576 588 # valid pages
577 589 if self.page > self.last_page:
578 590 self.page = self.last_page
579 591 elif self.page < self.first_page:
580 592 self.page = self.first_page
581 593
582 594 # Note: the number of items on this page can be less than
583 595 # items_per_page if the last page is not full
584 596 self.first_item = max(0, (self.item_count) - (self.page *
585 597 items_per_page))
586 598 self.last_item = ((self.item_count - 1) - items_per_page *
587 599 (self.page - 1))
588 600
589 601 self.items = list(self.collection[self.first_item:self.last_item + 1])
590 602
591 603
592 604 # Links to previous and next page
593 605 if self.page > self.first_page:
594 606 self.previous_page = self.page - 1
595 607 else:
596 608 self.previous_page = None
597 609
598 610 if self.page < self.last_page:
599 611 self.next_page = self.page + 1
600 612 else:
601 613 self.next_page = None
602 614
603 615 # No items available
604 616 else:
605 617 self.first_page = None
606 618 self.page_count = 0
607 619 self.last_page = None
608 620 self.first_item = None
609 621 self.last_item = None
610 622 self.previous_page = None
611 623 self.next_page = None
612 624 self.items = []
613 625
614 626 # This is a subclass of the 'list' type. Initialise the list now.
615 627 list.__init__(self, reversed(self.items))
616 628
617 629
618 630 def changed_tooltip(nodes):
619 631 """
620 632 Generates a html string for changed nodes in changeset page.
621 633 It limits the output to 30 entries
622 634
623 635 :param nodes: LazyNodesGenerator
624 636 """
625 637 if nodes:
626 638 pref = ': <br/> '
627 639 suf = ''
628 640 if len(nodes) > 30:
629 641 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
630 642 return literal(pref + '<br/> '.join([safe_unicode(x.path)
631 643 for x in nodes[:30]]) + suf)
632 644 else:
633 645 return ': ' + _('No Files')
634 646
635 647
636 648
637 649 def repo_link(groups_and_repos):
638 650 """
639 651 Makes a breadcrumbs link to repo within a group
640 652 joins &raquo; on each group to create a fancy link
641 653
642 654 ex::
643 655 group >> subgroup >> repo
644 656
645 657 :param groups_and_repos:
646 658 """
647 659 groups, repo_name = groups_and_repos
648 660
649 661 if not groups:
650 662 return repo_name
651 663 else:
652 664 def make_link(group):
653 665 return link_to(group.name, url('repos_group_home',
654 666 group_name=group.group_name))
655 667 return literal(' &raquo; '.join(map(make_link, groups)) + \
656 668 " &raquo; " + repo_name)
657 669
658 670 def fancy_file_stats(stats):
659 671 """
660 672 Displays a fancy two colored bar for number of added/deleted
661 673 lines of code on file
662 674
663 675 :param stats: two element list of added/deleted lines of code
664 676 """
665 677
666 678 a, d, t = stats[0], stats[1], stats[0] + stats[1]
667 679 width = 100
668 680 unit = float(width) / (t or 1)
669 681
670 682 # needs > 9% of width to be visible or 0 to be hidden
671 683 a_p = max(9, unit * a) if a > 0 else 0
672 684 d_p = max(9, unit * d) if d > 0 else 0
673 685 p_sum = a_p + d_p
674 686
675 687 if p_sum > width:
676 688 #adjust the percentage to be == 100% since we adjusted to 9
677 689 if a_p > d_p:
678 690 a_p = a_p - (p_sum - width)
679 691 else:
680 692 d_p = d_p - (p_sum - width)
681 693
682 694 a_v = a if a > 0 else ''
683 695 d_v = d if d > 0 else ''
684 696
685 697
686 698 def cgen(l_type):
687 699 mapping = {'tr':'top-right-rounded-corner',
688 700 'tl':'top-left-rounded-corner',
689 701 'br':'bottom-right-rounded-corner',
690 702 'bl':'bottom-left-rounded-corner'}
691 703 map_getter = lambda x:mapping[x]
692 704
693 705 if l_type == 'a' and d_v:
694 706 #case when added and deleted are present
695 707 return ' '.join(map(map_getter, ['tl', 'bl']))
696 708
697 709 if l_type == 'a' and not d_v:
698 710 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
699 711
700 712 if l_type == 'd' and a_v:
701 713 return ' '.join(map(map_getter, ['tr', 'br']))
702 714
703 715 if l_type == 'd' and not a_v:
704 716 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
705 717
706 718
707 719
708 720 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
709 721 a_p, a_v)
710 722 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
711 723 d_p, d_v)
712 724 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
713 725
714 726
715 727 def urlify_text(text):
716 728 import re
717 729
718 730 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
719 731 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
720 732
721 733 def url_func(match_obj):
722 734 url_full = match_obj.groups()[0]
723 735 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
724 736
725 737 return literal(url_pat.sub(url_func, text))
726 738
727 739
728 740 def rst(source):
729 741 return literal('<div class="rst-block">%s</div>' %
730 742 MarkupRenderer.rst(source))
731 743
732 744 def rst_w_mentions(source):
733 745 """
734 746 Wrapped rst renderer with @mention highlighting
735 747
736 748 :param source:
737 749 """
738 750 return literal('<div class="rst-block">%s</div>' %
739 751 MarkupRenderer.rst_with_mentions(source))
General Comments 0
You need to be logged in to leave comments. Login now