##// END OF EJS Templates
white space cleanup
marcink -
r2367:86aa4f1f beta
parent child Browse files
Show More
@@ -1,952 +1,952 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 import logging
12 12
13 13 from datetime import datetime
14 14 from pygments.formatters.html import HtmlFormatter
15 15 from pygments import highlight as code_highlight
16 16 from pylons import url, request, config
17 17 from pylons.i18n.translation import _, ungettext
18 18 from hashlib import md5
19 19
20 20 from webhelpers.html import literal, HTML, escape
21 21 from webhelpers.html.tools import *
22 22 from webhelpers.html.builder import make_tag
23 23 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
24 24 end_form, file, form, hidden, image, javascript_link, link_to, \
25 25 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
26 26 submit, text, password, textarea, title, ul, xml_declaration, radio
27 27 from webhelpers.html.tools import auto_link, button_to, highlight, \
28 28 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
29 29 from webhelpers.number import format_byte_size, format_bit_size
30 30 from webhelpers.pylonslib import Flash as _Flash
31 31 from webhelpers.pylonslib.secure_form import secure_form
32 32 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
33 33 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
34 34 replace_whitespace, urlify, truncate, wrap_paragraphs
35 35 from webhelpers.date import time_ago_in_words
36 36 from webhelpers.paginate import Page
37 37 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
38 38 convert_boolean_attrs, NotGiven, _make_safe_id_component
39 39
40 40 from rhodecode.lib.annotate import annotate_highlight
41 41 from rhodecode.lib.utils import repo_name_slug
42 42 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
43 43 get_changeset_safe
44 44 from rhodecode.lib.markup_renderer import MarkupRenderer
45 45 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
46 46 from rhodecode.lib.vcs.backends.base import BaseChangeset
47 47 from rhodecode.model.db import URL_SEP
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 def shorter(text, size=20):
53 53 postfix = '...'
54 54 if len(text) > size:
55 55 return text[:size - len(postfix)] + postfix
56 56 return text
57 57
58 58
59 59 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
60 60 """
61 61 Reset button
62 62 """
63 63 _set_input_attrs(attrs, type, name, value)
64 64 _set_id_attr(attrs, id, name)
65 65 convert_boolean_attrs(attrs, ["disabled"])
66 66 return HTML.input(**attrs)
67 67
68 68 reset = _reset
69 69 safeid = _make_safe_id_component
70 70
71 71
72 72 def FID(raw_id, path):
73 73 """
74 74 Creates a uniqe ID for filenode based on it's hash of path and revision
75 75 it's safe to use in urls
76 76
77 77 :param raw_id:
78 78 :param path:
79 79 """
80 80
81 81 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
82 82
83 83
84 84 def get_token():
85 85 """Return the current authentication token, creating one if one doesn't
86 86 already exist.
87 87 """
88 88 token_key = "_authentication_token"
89 89 from pylons import session
90 90 if not token_key in session:
91 91 try:
92 92 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
93 93 except AttributeError: # Python < 2.4
94 94 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
95 95 session[token_key] = token
96 96 if hasattr(session, 'save'):
97 97 session.save()
98 98 return session[token_key]
99 99
100 100
101 101 class _GetError(object):
102 102 """Get error from form_errors, and represent it as span wrapped error
103 103 message
104 104
105 105 :param field_name: field to fetch errors for
106 106 :param form_errors: form errors dict
107 107 """
108 108
109 109 def __call__(self, field_name, form_errors):
110 110 tmpl = """<span class="error_msg">%s</span>"""
111 111 if form_errors and form_errors.has_key(field_name):
112 112 return literal(tmpl % form_errors.get(field_name))
113 113
114 114 get_error = _GetError()
115 115
116 116
117 117 class _ToolTip(object):
118 118
119 119 def __call__(self, tooltip_title, trim_at=50):
120 120 """Special function just to wrap our text into nice formatted
121 121 autowrapped text
122 122
123 123 :param tooltip_title:
124 124 """
125 125 return escape(tooltip_title)
126 126 tooltip = _ToolTip()
127 127
128 128
129 129 class _FilesBreadCrumbs(object):
130 130
131 131 def __call__(self, repo_name, rev, paths):
132 132 if isinstance(paths, str):
133 133 paths = safe_unicode(paths)
134 134 url_l = [link_to(repo_name, url('files_home',
135 135 repo_name=repo_name,
136 136 revision=rev, f_path=''))]
137 137 paths_l = paths.split('/')
138 138 for cnt, p in enumerate(paths_l):
139 139 if p != '':
140 140 url_l.append(link_to(p,
141 141 url('files_home',
142 142 repo_name=repo_name,
143 143 revision=rev,
144 144 f_path='/'.join(paths_l[:cnt + 1])
145 145 )
146 146 )
147 147 )
148 148
149 149 return literal('/'.join(url_l))
150 150
151 151 files_breadcrumbs = _FilesBreadCrumbs()
152 152
153 153
154 154 class CodeHtmlFormatter(HtmlFormatter):
155 155 """
156 156 My code Html Formatter for source codes
157 157 """
158 158
159 159 def wrap(self, source, outfile):
160 160 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
161 161
162 162 def _wrap_code(self, source):
163 163 for cnt, it in enumerate(source):
164 164 i, t = it
165 165 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
166 166 yield i, t
167 167
168 168 def _wrap_tablelinenos(self, inner):
169 169 dummyoutfile = StringIO.StringIO()
170 170 lncount = 0
171 171 for t, line in inner:
172 172 if t:
173 173 lncount += 1
174 174 dummyoutfile.write(line)
175 175
176 176 fl = self.linenostart
177 177 mw = len(str(lncount + fl - 1))
178 178 sp = self.linenospecial
179 179 st = self.linenostep
180 180 la = self.lineanchors
181 181 aln = self.anchorlinenos
182 182 nocls = self.noclasses
183 183 if sp:
184 184 lines = []
185 185
186 186 for i in range(fl, fl + lncount):
187 187 if i % st == 0:
188 188 if i % sp == 0:
189 189 if aln:
190 190 lines.append('<a href="#%s%d" class="special">%*d</a>' %
191 191 (la, i, mw, i))
192 192 else:
193 193 lines.append('<span class="special">%*d</span>' % (mw, i))
194 194 else:
195 195 if aln:
196 196 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
197 197 else:
198 198 lines.append('%*d' % (mw, i))
199 199 else:
200 200 lines.append('')
201 201 ls = '\n'.join(lines)
202 202 else:
203 203 lines = []
204 204 for i in range(fl, fl + lncount):
205 205 if i % st == 0:
206 206 if aln:
207 207 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
208 208 else:
209 209 lines.append('%*d' % (mw, i))
210 210 else:
211 211 lines.append('')
212 212 ls = '\n'.join(lines)
213 213
214 214 # in case you wonder about the seemingly redundant <div> here: since the
215 215 # content in the other cell also is wrapped in a div, some browsers in
216 216 # some configurations seem to mess up the formatting...
217 217 if nocls:
218 218 yield 0, ('<table class="%stable">' % self.cssclass +
219 219 '<tr><td><div class="linenodiv" '
220 220 'style="background-color: #f0f0f0; padding-right: 10px">'
221 221 '<pre style="line-height: 125%">' +
222 222 ls + '</pre></div></td><td id="hlcode" class="code">')
223 223 else:
224 224 yield 0, ('<table class="%stable">' % self.cssclass +
225 225 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
226 226 ls + '</pre></div></td><td id="hlcode" class="code">')
227 227 yield 0, dummyoutfile.getvalue()
228 228 yield 0, '</td></tr></table>'
229 229
230 230
231 231 def pygmentize(filenode, **kwargs):
232 232 """pygmentize function using pygments
233 233
234 234 :param filenode:
235 235 """
236 236
237 237 return literal(code_highlight(filenode.content,
238 238 filenode.lexer, CodeHtmlFormatter(**kwargs)))
239 239
240 240
241 241 def pygmentize_annotation(repo_name, filenode, **kwargs):
242 242 """
243 243 pygmentize function for annotation
244 244
245 245 :param filenode:
246 246 """
247 247
248 248 color_dict = {}
249 249
250 250 def gen_color(n=10000):
251 251 """generator for getting n of evenly distributed colors using
252 252 hsv color and golden ratio. It always return same order of colors
253 253
254 254 :returns: RGB tuple
255 255 """
256 256
257 257 def hsv_to_rgb(h, s, v):
258 258 if s == 0.0:
259 259 return v, v, v
260 260 i = int(h * 6.0) # XXX assume int() truncates!
261 261 f = (h * 6.0) - i
262 262 p = v * (1.0 - s)
263 263 q = v * (1.0 - s * f)
264 264 t = v * (1.0 - s * (1.0 - f))
265 265 i = i % 6
266 266 if i == 0:
267 267 return v, t, p
268 268 if i == 1:
269 269 return q, v, p
270 270 if i == 2:
271 271 return p, v, t
272 272 if i == 3:
273 273 return p, q, v
274 274 if i == 4:
275 275 return t, p, v
276 276 if i == 5:
277 277 return v, p, q
278 278
279 279 golden_ratio = 0.618033988749895
280 280 h = 0.22717784590367374
281 281
282 282 for _ in xrange(n):
283 283 h += golden_ratio
284 284 h %= 1
285 285 HSV_tuple = [h, 0.95, 0.95]
286 286 RGB_tuple = hsv_to_rgb(*HSV_tuple)
287 287 yield map(lambda x: str(int(x * 256)), RGB_tuple)
288 288
289 289 cgenerator = gen_color()
290 290
291 291 def get_color_string(cs):
292 292 if cs in color_dict:
293 293 col = color_dict[cs]
294 294 else:
295 295 col = color_dict[cs] = cgenerator.next()
296 296 return "color: rgb(%s)! important;" % (', '.join(col))
297 297
298 298 def url_func(repo_name):
299 299
300 300 def _url_func(changeset):
301 301 author = changeset.author
302 302 date = changeset.date
303 303 message = tooltip(changeset.message)
304 304
305 305 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
306 306 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
307 307 "</b> %s<br/></div>")
308 308
309 309 tooltip_html = tooltip_html % (author, date, message)
310 310 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
311 311 short_id(changeset.raw_id))
312 312 uri = link_to(
313 313 lnk_format,
314 314 url('changeset_home', repo_name=repo_name,
315 315 revision=changeset.raw_id),
316 316 style=get_color_string(changeset.raw_id),
317 317 class_='tooltip',
318 318 title=tooltip_html
319 319 )
320 320
321 321 uri += '\n'
322 322 return uri
323 323 return _url_func
324 324
325 325 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
326 326
327 327
328 328 def is_following_repo(repo_name, user_id):
329 329 from rhodecode.model.scm import ScmModel
330 330 return ScmModel().is_following_repo(repo_name, user_id)
331 331
332 332 flash = _Flash()
333 333
334 334 #==============================================================================
335 335 # SCM FILTERS available via h.
336 336 #==============================================================================
337 337 from rhodecode.lib.vcs.utils import author_name, author_email
338 338 from rhodecode.lib.utils2 import credentials_filter, age as _age
339 339 from rhodecode.model.db import User
340 340
341 341 age = lambda x: _age(x)
342 342 capitalize = lambda x: x.capitalize()
343 343 email = author_email
344 344 short_id = lambda x: x[:12]
345 345 hide_credentials = lambda x: ''.join(credentials_filter(x))
346 346
347 347
348 348 def is_git(repository):
349 349 if hasattr(repository, 'alias'):
350 350 _type = repository.alias
351 351 elif hasattr(repository, 'repo_type'):
352 352 _type = repository.repo_type
353 353 else:
354 354 _type = repository
355 355 return _type == 'git'
356 356
357 357
358 358 def is_hg(repository):
359 359 if hasattr(repository, 'alias'):
360 360 _type = repository.alias
361 361 elif hasattr(repository, 'repo_type'):
362 362 _type = repository.repo_type
363 363 else:
364 364 _type = repository
365 365 return _type == 'hg'
366 366
367 367
368 368 def email_or_none(author):
369 369 _email = email(author)
370 370 if _email != '':
371 371 return _email
372 372
373 373 # See if it contains a username we can get an email from
374 374 user = User.get_by_username(author_name(author), case_insensitive=True,
375 375 cache=True)
376 376 if user is not None:
377 377 return user.email
378 378
379 379 # No valid email, not a valid user in the system, none!
380 380 return None
381 381
382 382
383 383 def person(author):
384 384 # attr to return from fetched user
385 385 person_getter = lambda usr: usr.username
386 386
387 387 # Valid email in the attribute passed, see if they're in the system
388 388 _email = email(author)
389 389 if _email != '':
390 390 user = User.get_by_email(_email, case_insensitive=True, cache=True)
391 391 if user is not None:
392 392 return person_getter(user)
393 393 return _email
394 394
395 395 # Maybe it's a username?
396 396 _author = author_name(author)
397 397 user = User.get_by_username(_author, case_insensitive=True,
398 398 cache=True)
399 399 if user is not None:
400 400 return person_getter(user)
401 401
402 402 # Still nothing? Just pass back the author name then
403 403 return _author
404 404
405 405
406 406 def bool2icon(value):
407 407 """Returns True/False values represented as small html image of true/false
408 408 icons
409 409
410 410 :param value: bool value
411 411 """
412 412
413 413 if value is True:
414 414 return HTML.tag('img', src=url("/images/icons/accept.png"),
415 415 alt=_('True'))
416 416
417 417 if value is False:
418 418 return HTML.tag('img', src=url("/images/icons/cancel.png"),
419 419 alt=_('False'))
420 420
421 421 return value
422 422
423 423
424 424 def action_parser(user_log, feed=False):
425 425 """
426 426 This helper will action_map the specified string action into translated
427 427 fancy names with icons and links
428 428
429 429 :param user_log: user log instance
430 430 :param feed: use output for feeds (no html and fancy icons)
431 431 """
432 432
433 433 action = user_log.action
434 434 action_params = ' '
435 435
436 436 x = action.split(':')
437 437
438 438 if len(x) > 1:
439 439 action, action_params = x
440 440
441 441 def get_cs_links():
442 442 revs_limit = 3 # display this amount always
443 443 revs_top_limit = 50 # show upto this amount of changesets hidden
444 444 revs_ids = action_params.split(',')
445 445 deleted = user_log.repository is None
446 446 if deleted:
447 447 return ','.join(revs_ids)
448 448
449 449 repo_name = user_log.repository.repo_name
450 450
451 451 repo = user_log.repository.scm_instance
452 452
453 453 def lnk(rev, repo_name):
454 454
455 455 if isinstance(rev, BaseChangeset):
456 456 lbl = 'r%s:%s' % (rev.revision, rev.short_id)
457 _url = url('changeset_home', repo_name=repo_name,
457 _url = url('changeset_home', repo_name=repo_name,
458 458 revision=rev.raw_id)
459 459 title = tooltip(rev.message)
460 460 else:
461 461 lbl = '%s' % rev
462 462 _url = '#'
463 463 title = _('Changeset not found')
464 464
465 465 return link_to(lbl, _url, title=title, class_='tooltip',)
466 466
467 467 revs = []
468 468 if len(filter(lambda v: v != '', revs_ids)) > 0:
469 469 for rev in revs_ids[:revs_top_limit]:
470 470 try:
471 471 rev = repo.get_changeset(rev)
472 472 revs.append(rev)
473 473 except ChangesetDoesNotExistError:
474 474 log.error('cannot find revision %s in this repo' % rev)
475 475 revs.append(rev)
476 476 continue
477 477 cs_links = []
478 478 cs_links.append(" " + ', '.join(
479 479 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
480 480 )
481 481 )
482 482
483 483 compare_view = (
484 484 ' <div class="compare_view tooltip" title="%s">'
485 485 '<a href="%s">%s</a> </div>' % (
486 486 _('Show all combined changesets %s->%s') % (
487 487 revs_ids[0], revs_ids[-1]
488 488 ),
489 489 url('changeset_home', repo_name=repo_name,
490 490 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
491 491 ),
492 492 _('compare view')
493 493 )
494 494 )
495 495
496 496 # if we have exactly one more than normally displayed
497 497 # just display it, takes less space than displaying
498 498 # "and 1 more revisions"
499 499 if len(revs_ids) == revs_limit + 1:
500 500 rev = revs[revs_limit]
501 501 cs_links.append(", " + lnk(rev, repo_name))
502 502
503 503 # hidden-by-default ones
504 504 if len(revs_ids) > revs_limit + 1:
505 505 uniq_id = revs_ids[0]
506 506 html_tmpl = (
507 507 '<span> %s <a class="show_more" id="_%s" '
508 508 'href="#more">%s</a> %s</span>'
509 509 )
510 510 if not feed:
511 511 cs_links.append(html_tmpl % (
512 512 _('and'),
513 513 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
514 514 _('revisions')
515 515 )
516 516 )
517 517
518 518 if not feed:
519 519 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
520 520 else:
521 521 html_tmpl = '<span id="%s"> %s </span>'
522 522
523 523 morelinks = ', '.join(
524 524 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
525 525 )
526 526
527 527 if len(revs_ids) > revs_top_limit:
528 528 morelinks += ', ...'
529 529
530 530 cs_links.append(html_tmpl % (uniq_id, morelinks))
531 531 if len(revs) > 1:
532 532 cs_links.append(compare_view)
533 533 return ''.join(cs_links)
534 534
535 535 def get_fork_name():
536 536 repo_name = action_params
537 537 return _('fork name ') + str(link_to(action_params, url('summary_home',
538 538 repo_name=repo_name,)))
539 539
540 540 action_map = {'user_deleted_repo': (_('[deleted] repository'), None),
541 541 'user_created_repo': (_('[created] repository'), None),
542 542 'user_created_fork': (_('[created] repository as fork'), None),
543 543 'user_forked_repo': (_('[forked] repository'), get_fork_name),
544 544 'user_updated_repo': (_('[updated] repository'), None),
545 545 'admin_deleted_repo': (_('[delete] repository'), None),
546 546 'admin_created_repo': (_('[created] repository'), None),
547 547 'admin_forked_repo': (_('[forked] repository'), None),
548 548 'admin_updated_repo': (_('[updated] repository'), None),
549 549 'push': (_('[pushed] into'), get_cs_links),
550 550 'push_local': (_('[committed via RhodeCode] into'), get_cs_links),
551 551 'push_remote': (_('[pulled from remote] into'), get_cs_links),
552 552 'pull': (_('[pulled] from'), None),
553 553 'started_following_repo': (_('[started following] repository'), None),
554 554 'stopped_following_repo': (_('[stopped following] repository'), None),
555 555 }
556 556
557 557 action_str = action_map.get(action, action)
558 558 if feed:
559 559 action = action_str[0].replace('[', '').replace(']', '')
560 560 else:
561 561 action = action_str[0]\
562 562 .replace('[', '<span class="journal_highlight">')\
563 563 .replace(']', '</span>')
564 564
565 565 action_params_func = lambda: ""
566 566
567 567 if callable(action_str[1]):
568 568 action_params_func = action_str[1]
569 569
570 570 return [literal(action), action_params_func]
571 571
572 572
573 573 def action_parser_icon(user_log):
574 574 action = user_log.action
575 575 action_params = None
576 576 x = action.split(':')
577 577
578 578 if len(x) > 1:
579 579 action, action_params = x
580 580
581 581 tmpl = """<img src="%s%s" alt="%s"/>"""
582 582 map = {'user_deleted_repo':'database_delete.png',
583 583 'user_created_repo':'database_add.png',
584 584 'user_created_fork':'arrow_divide.png',
585 585 'user_forked_repo':'arrow_divide.png',
586 586 'user_updated_repo':'database_edit.png',
587 587 'admin_deleted_repo':'database_delete.png',
588 588 'admin_created_repo':'database_add.png',
589 589 'admin_forked_repo':'arrow_divide.png',
590 590 'admin_updated_repo':'database_edit.png',
591 591 'push':'script_add.png',
592 592 'push_local':'script_edit.png',
593 593 'push_remote':'connect.png',
594 594 'pull':'down_16.png',
595 595 'started_following_repo':'heart_add.png',
596 596 'stopped_following_repo':'heart_delete.png',
597 597 }
598 598 return literal(tmpl % ((url('/images/icons/')),
599 599 map.get(action, action), action))
600 600
601 601
602 602 #==============================================================================
603 603 # PERMS
604 604 #==============================================================================
605 605 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
606 606 HasRepoPermissionAny, HasRepoPermissionAll
607 607
608 608
609 609 #==============================================================================
610 610 # GRAVATAR URL
611 611 #==============================================================================
612 612
613 613 def gravatar_url(email_address, size=30):
614 614 if (not str2bool(config['app_conf'].get('use_gravatar')) or
615 615 not email_address or email_address == 'anonymous@rhodecode.org'):
616 616 f = lambda a, l: min(l, key=lambda x: abs(x - a))
617 617 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
618 618
619 619 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
620 620 default = 'identicon'
621 621 baseurl_nossl = "http://www.gravatar.com/avatar/"
622 622 baseurl_ssl = "https://secure.gravatar.com/avatar/"
623 623 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
624 624
625 625 if isinstance(email_address, unicode):
626 626 #hashlib crashes on unicode items
627 627 email_address = safe_str(email_address)
628 628 # construct the url
629 629 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
630 630 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
631 631
632 632 return gravatar_url
633 633
634 634
635 635 #==============================================================================
636 636 # REPO PAGER, PAGER FOR REPOSITORY
637 637 #==============================================================================
638 638 class RepoPage(Page):
639 639
640 640 def __init__(self, collection, page=1, items_per_page=20,
641 641 item_count=None, url=None, **kwargs):
642 642
643 643 """Create a "RepoPage" instance. special pager for paging
644 644 repository
645 645 """
646 646 self._url_generator = url
647 647
648 648 # Safe the kwargs class-wide so they can be used in the pager() method
649 649 self.kwargs = kwargs
650 650
651 651 # Save a reference to the collection
652 652 self.original_collection = collection
653 653
654 654 self.collection = collection
655 655
656 656 # The self.page is the number of the current page.
657 657 # The first page has the number 1!
658 658 try:
659 659 self.page = int(page) # make it int() if we get it as a string
660 660 except (ValueError, TypeError):
661 661 self.page = 1
662 662
663 663 self.items_per_page = items_per_page
664 664
665 665 # Unless the user tells us how many items the collections has
666 666 # we calculate that ourselves.
667 667 if item_count is not None:
668 668 self.item_count = item_count
669 669 else:
670 670 self.item_count = len(self.collection)
671 671
672 672 # Compute the number of the first and last available page
673 673 if self.item_count > 0:
674 674 self.first_page = 1
675 675 self.page_count = int(math.ceil(float(self.item_count) /
676 676 self.items_per_page))
677 677 self.last_page = self.first_page + self.page_count - 1
678 678
679 679 # Make sure that the requested page number is the range of
680 680 # valid pages
681 681 if self.page > self.last_page:
682 682 self.page = self.last_page
683 683 elif self.page < self.first_page:
684 684 self.page = self.first_page
685 685
686 686 # Note: the number of items on this page can be less than
687 687 # items_per_page if the last page is not full
688 688 self.first_item = max(0, (self.item_count) - (self.page *
689 689 items_per_page))
690 690 self.last_item = ((self.item_count - 1) - items_per_page *
691 691 (self.page - 1))
692 692
693 693 self.items = list(self.collection[self.first_item:self.last_item + 1])
694 694
695 695 # Links to previous and next page
696 696 if self.page > self.first_page:
697 697 self.previous_page = self.page - 1
698 698 else:
699 699 self.previous_page = None
700 700
701 701 if self.page < self.last_page:
702 702 self.next_page = self.page + 1
703 703 else:
704 704 self.next_page = None
705 705
706 706 # No items available
707 707 else:
708 708 self.first_page = None
709 709 self.page_count = 0
710 710 self.last_page = None
711 711 self.first_item = None
712 712 self.last_item = None
713 713 self.previous_page = None
714 714 self.next_page = None
715 715 self.items = []
716 716
717 717 # This is a subclass of the 'list' type. Initialise the list now.
718 718 list.__init__(self, reversed(self.items))
719 719
720 720
721 721 def changed_tooltip(nodes):
722 722 """
723 723 Generates a html string for changed nodes in changeset page.
724 724 It limits the output to 30 entries
725 725
726 726 :param nodes: LazyNodesGenerator
727 727 """
728 728 if nodes:
729 729 pref = ': <br/> '
730 730 suf = ''
731 731 if len(nodes) > 30:
732 732 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
733 733 return literal(pref + '<br/> '.join([safe_unicode(x.path)
734 734 for x in nodes[:30]]) + suf)
735 735 else:
736 736 return ': ' + _('No Files')
737 737
738 738
739 739 def repo_link(groups_and_repos):
740 740 """
741 741 Makes a breadcrumbs link to repo within a group
742 742 joins &raquo; on each group to create a fancy link
743 743
744 744 ex::
745 745 group >> subgroup >> repo
746 746
747 747 :param groups_and_repos:
748 748 """
749 749 groups, repo_name = groups_and_repos
750 750
751 751 if not groups:
752 752 return repo_name
753 753 else:
754 754 def make_link(group):
755 755 return link_to(group.name, url('repos_group_home',
756 756 group_name=group.group_name))
757 757 return literal(' &raquo; '.join(map(make_link, groups)) + \
758 758 " &raquo; " + repo_name)
759 759
760 760
761 761 def fancy_file_stats(stats):
762 762 """
763 763 Displays a fancy two colored bar for number of added/deleted
764 764 lines of code on file
765 765
766 766 :param stats: two element list of added/deleted lines of code
767 767 """
768 768
769 769 a, d, t = stats[0], stats[1], stats[0] + stats[1]
770 770 width = 100
771 771 unit = float(width) / (t or 1)
772 772
773 773 # needs > 9% of width to be visible or 0 to be hidden
774 774 a_p = max(9, unit * a) if a > 0 else 0
775 775 d_p = max(9, unit * d) if d > 0 else 0
776 776 p_sum = a_p + d_p
777 777
778 778 if p_sum > width:
779 779 #adjust the percentage to be == 100% since we adjusted to 9
780 780 if a_p > d_p:
781 781 a_p = a_p - (p_sum - width)
782 782 else:
783 783 d_p = d_p - (p_sum - width)
784 784
785 785 a_v = a if a > 0 else ''
786 786 d_v = d if d > 0 else ''
787 787
788 788 def cgen(l_type):
789 789 mapping = {'tr': 'top-right-rounded-corner-mid',
790 790 'tl': 'top-left-rounded-corner-mid',
791 791 'br': 'bottom-right-rounded-corner-mid',
792 792 'bl': 'bottom-left-rounded-corner-mid'}
793 793 map_getter = lambda x: mapping[x]
794 794
795 795 if l_type == 'a' and d_v:
796 796 #case when added and deleted are present
797 797 return ' '.join(map(map_getter, ['tl', 'bl']))
798 798
799 799 if l_type == 'a' and not d_v:
800 800 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
801 801
802 802 if l_type == 'd' and a_v:
803 803 return ' '.join(map(map_getter, ['tr', 'br']))
804 804
805 805 if l_type == 'd' and not a_v:
806 806 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
807 807
808 808 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
809 809 cgen('a'), a_p, a_v
810 810 )
811 811 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
812 812 cgen('d'), d_p, d_v
813 813 )
814 814 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
815 815
816 816
817 817 def urlify_text(text_):
818 818 import re
819 819
820 820 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
821 821 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
822 822
823 823 def url_func(match_obj):
824 824 url_full = match_obj.groups()[0]
825 825 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
826 826
827 827 return literal(url_pat.sub(url_func, text_))
828 828
829 829
830 830 def urlify_changesets(text_, repository):
831 831 """
832 832 Extract revision ids from changeset and make link from them
833 833
834 834 :param text_:
835 835 :param repository:
836 836 """
837 837 import re
838 838 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
839 839
840 840 def url_func(match_obj):
841 841 rev = match_obj.groups()[0]
842 842 pref = ''
843 843 if match_obj.group().startswith(' '):
844 844 pref = ' '
845 845 tmpl = (
846 846 '%(pref)s<a class="%(cls)s" href="%(url)s">'
847 847 '%(rev)s'
848 848 '</a>'
849 849 )
850 850 return tmpl % {
851 851 'pref': pref,
852 852 'cls': 'revision-link',
853 853 'url': url('changeset_home', repo_name=repository, revision=rev),
854 854 'rev': rev,
855 855 }
856 856
857 857 newtext = URL_PAT.sub(url_func, text_)
858 858
859 859 return newtext
860 860
861 861
862 862 def urlify_commit(text_, repository=None, link_=None):
863 863 """
864 864 Parses given text message and makes proper links.
865 865 issues are linked to given issue-server, and rest is a changeset link
866 866 if link_ is given, in other case it's a plain text
867 867
868 868 :param text_:
869 869 :param repository:
870 870 :param link_: changeset link
871 871 """
872 872 import re
873 873 import traceback
874 874
875 875 def escaper(string):
876 876 return string.replace('<', '&lt;').replace('>', '&gt;')
877 877
878 878 def linkify_others(t, l):
879 879 urls = re.compile(r'(\<a.*?\<\/a\>)',)
880 880 links = []
881 881 for e in urls.split(t):
882 882 if not urls.match(e):
883 883 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
884 884 else:
885 885 links.append(e)
886 886
887 887 return ''.join(links)
888 888
889 889 # urlify changesets - extrac revisions and make link out of them
890 890 text_ = urlify_changesets(escaper(text_), repository)
891 891
892 892 try:
893 893 conf = config['app_conf']
894 894
895 895 URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
896 896
897 897 if URL_PAT:
898 898 ISSUE_SERVER_LNK = conf.get('issue_server_link')
899 899 ISSUE_PREFIX = conf.get('issue_prefix')
900 900
901 901 def url_func(match_obj):
902 902 pref = ''
903 903 if match_obj.group().startswith(' '):
904 904 pref = ' '
905 905
906 906 issue_id = ''.join(match_obj.groups())
907 907 tmpl = (
908 908 '%(pref)s<a class="%(cls)s" href="%(url)s">'
909 909 '%(issue-prefix)s%(id-repr)s'
910 910 '</a>'
911 911 )
912 912 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
913 913 if repository:
914 914 url = url.replace('{repo}', repository)
915 915 repo_name = repository.split(URL_SEP)[-1]
916 916 url = url.replace('{repo_name}', repo_name)
917 917 return tmpl % {
918 918 'pref': pref,
919 919 'cls': 'issue-tracker-link',
920 920 'url': url,
921 921 'id-repr': issue_id,
922 922 'issue-prefix': ISSUE_PREFIX,
923 923 'serv': ISSUE_SERVER_LNK,
924 924 }
925 925
926 926 newtext = URL_PAT.sub(url_func, text_)
927 927
928 928 if link_:
929 929 # wrap not links into final link => link_
930 930 newtext = linkify_others(newtext, link_)
931 931
932 932 return literal(newtext)
933 933 except:
934 934 log.error(traceback.format_exc())
935 935 pass
936 936
937 937 return text_
938 938
939 939
940 940 def rst(source):
941 941 return literal('<div class="rst-block">%s</div>' %
942 942 MarkupRenderer.rst(source))
943 943
944 944
945 945 def rst_w_mentions(source):
946 946 """
947 947 Wrapped rst renderer with @mention highlighting
948 948
949 949 :param source:
950 950 """
951 951 return literal('<div class="rst-block">%s</div>' %
952 952 MarkupRenderer.rst_with_mentions(source))
@@ -1,445 +1,445 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import re
27 27 from datetime import datetime
28 28 from pylons.i18n.translation import _, ungettext
29 29 from rhodecode.lib.vcs.utils.lazy import LazyProperty
30 30
31 31
32 32 def __get_lem():
33 33 """
34 34 Get language extension map based on what's inside pygments lexers
35 35 """
36 36 from pygments import lexers
37 37 from string import lower
38 38 from collections import defaultdict
39 39
40 40 d = defaultdict(lambda: [])
41 41
42 42 def __clean(s):
43 43 s = s.lstrip('*')
44 44 s = s.lstrip('.')
45 45
46 46 if s.find('[') != -1:
47 47 exts = []
48 48 start, stop = s.find('['), s.find(']')
49 49
50 50 for suffix in s[start + 1:stop]:
51 51 exts.append(s[:s.find('[')] + suffix)
52 52 return map(lower, exts)
53 53 else:
54 54 return map(lower, [s])
55 55
56 56 for lx, t in sorted(lexers.LEXERS.items()):
57 57 m = map(__clean, t[-2])
58 58 if m:
59 59 m = reduce(lambda x, y: x + y, m)
60 60 for ext in m:
61 61 desc = lx.replace('Lexer', '')
62 62 d[ext].append(desc)
63 63
64 64 return dict(d)
65 65
66 66 def str2bool(_str):
67 67 """
68 68 returs True/False value from given string, it tries to translate the
69 69 string into boolean
70 70
71 71 :param _str: string value to translate into boolean
72 72 :rtype: boolean
73 73 :returns: boolean from given string
74 74 """
75 75 if _str is None:
76 76 return False
77 77 if _str in (True, False):
78 78 return _str
79 79 _str = str(_str).strip().lower()
80 80 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
81 81
82 82
83 83 def convert_line_endings(line, mode):
84 84 """
85 85 Converts a given line "line end" accordingly to given mode
86 86
87 87 Available modes are::
88 88 0 - Unix
89 89 1 - Mac
90 90 2 - DOS
91 91
92 92 :param line: given line to convert
93 93 :param mode: mode to convert to
94 94 :rtype: str
95 95 :return: converted line according to mode
96 96 """
97 97 from string import replace
98 98
99 99 if mode == 0:
100 100 line = replace(line, '\r\n', '\n')
101 101 line = replace(line, '\r', '\n')
102 102 elif mode == 1:
103 103 line = replace(line, '\r\n', '\r')
104 104 line = replace(line, '\n', '\r')
105 105 elif mode == 2:
106 106 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
107 107 return line
108 108
109 109
110 110 def detect_mode(line, default):
111 111 """
112 112 Detects line break for given line, if line break couldn't be found
113 113 given default value is returned
114 114
115 115 :param line: str line
116 116 :param default: default
117 117 :rtype: int
118 118 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
119 119 """
120 120 if line.endswith('\r\n'):
121 121 return 2
122 122 elif line.endswith('\n'):
123 123 return 0
124 124 elif line.endswith('\r'):
125 125 return 1
126 126 else:
127 127 return default
128 128
129 129
130 130 def generate_api_key(username, salt=None):
131 131 """
132 132 Generates unique API key for given username, if salt is not given
133 133 it'll be generated from some random string
134 134
135 135 :param username: username as string
136 136 :param salt: salt to hash generate KEY
137 137 :rtype: str
138 138 :returns: sha1 hash from username+salt
139 139 """
140 140 from tempfile import _RandomNameSequence
141 141 import hashlib
142 142
143 143 if salt is None:
144 144 salt = _RandomNameSequence().next()
145 145
146 146 return hashlib.sha1(username + salt).hexdigest()
147 147
148 148
149 149 def safe_unicode(str_, from_encoding=None):
150 150 """
151 151 safe unicode function. Does few trick to turn str_ into unicode
152 152
153 153 In case of UnicodeDecode error we try to return it with encoding detected
154 154 by chardet library if it fails fallback to unicode with errors replaced
155 155
156 156 :param str_: string to decode
157 157 :rtype: unicode
158 158 :returns: unicode object
159 159 """
160 160 if isinstance(str_, unicode):
161 161 return str_
162 162
163 163 if not from_encoding:
164 164 import rhodecode
165 165 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
166 166 from_encoding = DEFAULT_ENCODING
167 167
168 168 try:
169 169 return unicode(str_)
170 170 except UnicodeDecodeError:
171 171 pass
172 172
173 173 try:
174 174 return unicode(str_, from_encoding)
175 175 except UnicodeDecodeError:
176 176 pass
177 177
178 178 try:
179 179 import chardet
180 180 encoding = chardet.detect(str_)['encoding']
181 181 if encoding is None:
182 182 raise Exception()
183 183 return str_.decode(encoding)
184 184 except (ImportError, UnicodeDecodeError, Exception):
185 185 return unicode(str_, from_encoding, 'replace')
186 186
187 187
188 188 def safe_str(unicode_, to_encoding=None):
189 189 """
190 190 safe str function. Does few trick to turn unicode_ into string
191 191
192 192 In case of UnicodeEncodeError we try to return it with encoding detected
193 193 by chardet library if it fails fallback to string with errors replaced
194 194
195 195 :param unicode_: unicode to encode
196 196 :rtype: str
197 197 :returns: str object
198 198 """
199 199
200 200 # if it's not basestr cast to str
201 201 if not isinstance(unicode_, basestring):
202 202 return str(unicode_)
203 203
204 204 if isinstance(unicode_, str):
205 205 return unicode_
206 206
207 207 if not to_encoding:
208 208 import rhodecode
209 209 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
210 210 to_encoding = DEFAULT_ENCODING
211 211
212 212 try:
213 213 return unicode_.encode(to_encoding)
214 214 except UnicodeEncodeError:
215 215 pass
216 216
217 217 try:
218 218 import chardet
219 219 encoding = chardet.detect(unicode_)['encoding']
220 220 if encoding is None:
221 221 raise UnicodeEncodeError()
222 222
223 223 return unicode_.encode(encoding)
224 224 except (ImportError, UnicodeEncodeError):
225 225 return unicode_.encode(to_encoding, 'replace')
226 226
227 227 return safe_str
228 228
229 229
230 230 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
231 231 """
232 232 Custom engine_from_config functions that makes sure we use NullPool for
233 233 file based sqlite databases. This prevents errors on sqlite. This only
234 234 applies to sqlalchemy versions < 0.7.0
235 235
236 236 """
237 237 import sqlalchemy
238 238 from sqlalchemy import engine_from_config as efc
239 239 import logging
240 240
241 241 if int(sqlalchemy.__version__.split('.')[1]) < 7:
242 242
243 243 # This solution should work for sqlalchemy < 0.7.0, and should use
244 244 # proxy=TimerProxy() for execution time profiling
245 245
246 246 from sqlalchemy.pool import NullPool
247 247 url = configuration[prefix + 'url']
248 248
249 249 if url.startswith('sqlite'):
250 250 kwargs.update({'poolclass': NullPool})
251 251 return efc(configuration, prefix, **kwargs)
252 252 else:
253 253 import time
254 254 from sqlalchemy import event
255 255 from sqlalchemy.engine import Engine
256 256
257 257 log = logging.getLogger('sqlalchemy.engine')
258 258 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
259 259 engine = efc(configuration, prefix, **kwargs)
260 260
261 261 def color_sql(sql):
262 262 COLOR_SEQ = "\033[1;%dm"
263 263 COLOR_SQL = YELLOW
264 264 normal = '\x1b[0m'
265 265 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
266 266
267 267 if configuration['debug']:
268 268 #attach events only for debug configuration
269 269
270 270 def before_cursor_execute(conn, cursor, statement,
271 271 parameters, context, executemany):
272 272 context._query_start_time = time.time()
273 273 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
274 274
275 275
276 276 def after_cursor_execute(conn, cursor, statement,
277 277 parameters, context, executemany):
278 278 total = time.time() - context._query_start_time
279 279 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
280 280
281 281 event.listen(engine, "before_cursor_execute",
282 282 before_cursor_execute)
283 283 event.listen(engine, "after_cursor_execute",
284 284 after_cursor_execute)
285 285
286 286 return engine
287 287
288 288
289 289 def age(prevdate):
290 290 """
291 291 turns a datetime into an age string.
292 292
293 293 :param prevdate: datetime object
294 294 :rtype: unicode
295 295 :returns: unicode words describing age
296 296 """
297 297
298 298 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
299 299 deltas = {}
300 300
301 301 # Get date parts deltas
302 302 now = datetime.now()
303 303 for part in order:
304 304 deltas[part] = getattr(now, part) - getattr(prevdate, part)
305 305
306 306 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
307 307 # not 1 hour, -59 minutes and -59 seconds)
308 308
309 309 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
310 310 part = order[num]
311 311 carry_part = order[num - 1]
312
312
313 313 if deltas[part] < 0:
314 314 deltas[part] += length
315 315 deltas[carry_part] -= 1
316 316
317 317 # Same thing for days except that the increment depends on the (variable)
318 318 # number of days in the month
319 319 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
320 320 if deltas['day'] < 0:
321 321 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
322 322 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
323 323 deltas['day'] += 29
324 324 else:
325 325 deltas['day'] += month_lengths[prevdate.month - 1]
326
326
327 327 deltas['month'] -= 1
328
328
329 329 if deltas['month'] < 0:
330 330 deltas['month'] += 12
331 331 deltas['year'] -= 1
332
332
333 333 # Format the result
334 334 fmt_funcs = {
335 335 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
336 336 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
337 337 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
338 338 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
339 339 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
340 340 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
341 341 }
342
342
343 343 for i, part in enumerate(order):
344 344 value = deltas[part]
345 345 if value == 0:
346 346 continue
347
347
348 348 if i < 5:
349 349 sub_part = order[i + 1]
350 350 sub_value = deltas[sub_part]
351 351 else:
352 352 sub_value = 0
353
353
354 354 if sub_value == 0:
355 355 return _(u'%s ago') % fmt_funcs[part](value)
356
356
357 357 return _(u'%s and %s ago') % (fmt_funcs[part](value),
358 358 fmt_funcs[sub_part](sub_value))
359 359
360 360 return _(u'just now')
361 361
362 362
363 363 def uri_filter(uri):
364 364 """
365 365 Removes user:password from given url string
366 366
367 367 :param uri:
368 368 :rtype: unicode
369 369 :returns: filtered list of strings
370 370 """
371 371 if not uri:
372 372 return ''
373 373
374 374 proto = ''
375 375
376 376 for pat in ('https://', 'http://'):
377 377 if uri.startswith(pat):
378 378 uri = uri[len(pat):]
379 379 proto = pat
380 380 break
381 381
382 382 # remove passwords and username
383 383 uri = uri[uri.find('@') + 1:]
384 384
385 385 # get the port
386 386 cred_pos = uri.find(':')
387 387 if cred_pos == -1:
388 388 host, port = uri, None
389 389 else:
390 390 host, port = uri[:cred_pos], uri[cred_pos + 1:]
391 391
392 392 return filter(None, [proto, host, port])
393 393
394 394
395 395 def credentials_filter(uri):
396 396 """
397 397 Returns a url with removed credentials
398 398
399 399 :param uri:
400 400 """
401 401
402 402 uri = uri_filter(uri)
403 403 #check if we have port
404 404 if len(uri) > 2 and uri[2]:
405 405 uri[2] = ':' + uri[2]
406 406
407 407 return ''.join(uri)
408 408
409 409
410 410 def get_changeset_safe(repo, rev):
411 411 """
412 412 Safe version of get_changeset if this changeset doesn't exists for a
413 413 repo it returns a Dummy one instead
414 414
415 415 :param repo:
416 416 :param rev:
417 417 """
418 418 from rhodecode.lib.vcs.backends.base import BaseRepository
419 419 from rhodecode.lib.vcs.exceptions import RepositoryError
420 420 if not isinstance(repo, BaseRepository):
421 421 raise Exception('You must pass an Repository '
422 422 'object as first argument got %s', type(repo))
423 423
424 424 try:
425 425 cs = repo.get_changeset(rev)
426 426 except RepositoryError:
427 427 from rhodecode.lib.utils import EmptyChangeset
428 428 cs = EmptyChangeset(requested_revision=rev)
429 429 return cs
430 430
431 431
432 432 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
433 433
434 434
435 435 def extract_mentioned_users(s):
436 436 """
437 437 Returns unique usernames from given string s that have @mention
438 438
439 439 :param s: string to get mentions
440 440 """
441 441 usrs = set()
442 442 for username in re.findall(MENTIONS_REGEX, s):
443 443 usrs.add(username)
444 444
445 445 return sorted(list(usrs), key=lambda k: k.lower())
@@ -1,554 +1,554 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.git
4 4 ~~~~~~~~~~~~~~~~
5 5
6 6 Git backend implementation.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import os
13 13 import re
14 14 import time
15 15 import posixpath
16 16 from dulwich.repo import Repo, NotGitRepository
17 17 #from dulwich.config import ConfigFile
18 18 from string import Template
19 19 from subprocess import Popen, PIPE
20 20 from rhodecode.lib.vcs.backends.base import BaseRepository
21 21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
22 22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
23 23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
24 24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
26 26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
27 27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
28 28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
30 30 from rhodecode.lib.vcs.utils.paths import abspath
31 31 from rhodecode.lib.vcs.utils.paths import get_user_home
32 32 from .workdir import GitWorkdir
33 33 from .changeset import GitChangeset
34 34 from .inmemory import GitInMemoryChangeset
35 35 from .config import ConfigFile
36 36
37 37
38 38 class GitRepository(BaseRepository):
39 39 """
40 40 Git repository backend.
41 41 """
42 42 DEFAULT_BRANCH_NAME = 'master'
43 43 scm = 'git'
44 44
45 45 def __init__(self, repo_path, create=False, src_url=None,
46 46 update_after_clone=False, bare=False):
47 47
48 48 self.path = abspath(repo_path)
49 49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
50 50 #temporary set that to now at later we will move it to constructor
51 51 baseui = None
52 52 if baseui is None:
53 53 from mercurial.ui import ui
54 54 baseui = ui()
55 55 # patch the instance of GitRepo with an "FAKE" ui object to add
56 56 # compatibility layer with Mercurial
57 57 setattr(self._repo, 'ui', baseui)
58 58
59 59 try:
60 60 self.head = self._repo.head()
61 61 except KeyError:
62 62 self.head = None
63 63
64 64 self._config_files = [
65 65 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
66 66 'config'),
67 67 abspath(get_user_home(), '.gitconfig'),
68 68 ]
69 69 self.bare = self._repo.bare
70 70
71 71 @LazyProperty
72 72 def revisions(self):
73 73 """
74 74 Returns list of revisions' ids, in ascending order. Being lazy
75 75 attribute allows external tools to inject shas from cache.
76 76 """
77 77 return self._get_all_revisions()
78 78
79 79 def run_git_command(self, cmd):
80 80 """
81 81 Runs given ``cmd`` as git command and returns tuple
82 82 (returncode, stdout, stderr).
83 83
84 84 .. note::
85 85 This method exists only until log/blame functionality is implemented
86 86 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
87 87 os command's output is road to hell...
88 88
89 89 :param cmd: git command to be executed
90 90 """
91 91
92 92 _copts = ['-c', 'core.quotepath=false', ]
93 93 _str_cmd = False
94 94 if isinstance(cmd, basestring):
95 95 cmd = [cmd]
96 96 _str_cmd = True
97
97
98 98 gitenv = os.environ
99 99 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
100 100
101 101 cmd = ['git'] + _copts + cmd
102 102 if _str_cmd:
103 103 cmd = ' '.join(cmd)
104 104 try:
105 105 opts = dict(
106 106 shell=isinstance(cmd, basestring),
107 107 stdout=PIPE,
108 108 stderr=PIPE,
109 109 env=gitenv,
110 110 )
111 111 if os.path.isdir(self.path):
112 112 opts['cwd'] = self.path
113 113 p = Popen(cmd, **opts)
114 114 except OSError, err:
115 115 raise RepositoryError("Couldn't run git command (%s).\n"
116 116 "Original error was:%s" % (cmd, err))
117 117 so, se = p.communicate()
118 118 if not se.startswith("fatal: bad default revision 'HEAD'") and \
119 119 p.returncode != 0:
120 120 raise RepositoryError("Couldn't run git command (%s).\n"
121 121 "stderr:\n%s" % (cmd, se))
122 122 return so, se
123 123
124 124 def _check_url(self, url):
125 125 """
126 126 Functon will check given url and try to verify if it's a valid
127 127 link. Sometimes it may happened that mercurial will issue basic
128 128 auth request that can cause whole API to hang when used from python
129 129 or other external calls.
130 130
131 131 On failures it'll raise urllib2.HTTPError
132 132 """
133 133
134 134 #TODO: implement this
135 135 pass
136 136
137 137 def _get_repo(self, create, src_url=None, update_after_clone=False,
138 138 bare=False):
139 139 if create and os.path.exists(self.path):
140 140 raise RepositoryError("Location already exist")
141 141 if src_url and not create:
142 142 raise RepositoryError("Create should be set to True if src_url is "
143 143 "given (clone operation creates repository)")
144 144 try:
145 145 if create and src_url:
146 146 self._check_url(src_url)
147 147 self.clone(src_url, update_after_clone, bare)
148 148 return Repo(self.path)
149 149 elif create:
150 150 os.mkdir(self.path)
151 151 if bare:
152 152 return Repo.init_bare(self.path)
153 153 else:
154 154 return Repo.init(self.path)
155 155 else:
156 156 return Repo(self.path)
157 157 except (NotGitRepository, OSError), err:
158 158 raise RepositoryError(err)
159 159
160 160 def _get_all_revisions(self):
161 161 cmd = 'rev-list --all --date-order'
162 162 try:
163 163 so, se = self.run_git_command(cmd)
164 164 except RepositoryError:
165 165 # Can be raised for empty repositories
166 166 return []
167 167 revisions = so.splitlines()
168 168 revisions.reverse()
169 169 return revisions
170 170
171 171 def _get_revision(self, revision):
172 172 """
173 173 For git backend we always return integer here. This way we ensure
174 174 that changset's revision attribute would become integer.
175 175 """
176 176 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
177 177 is_bstr = lambda o: isinstance(o, (str, unicode))
178 178 is_null = lambda o: len(o) == revision.count('0')
179 179
180 180 if len(self.revisions) == 0:
181 181 raise EmptyRepositoryError("There are no changesets yet")
182 182
183 183 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
184 184 revision = self.revisions[-1]
185 185
186 186 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
187 187 or isinstance(revision, int) or is_null(revision)):
188 188 try:
189 189 revision = self.revisions[int(revision)]
190 190 except:
191 191 raise ChangesetDoesNotExistError("Revision %r does not exist "
192 192 "for this repository %s" % (revision, self))
193 193
194 194 elif is_bstr(revision):
195 195 if not pattern.match(revision) or revision not in self.revisions:
196 196 raise ChangesetDoesNotExistError("Revision %r does not exist "
197 197 "for this repository %s" % (revision, self))
198 198
199 199 # Ensure we return full id
200 200 if not pattern.match(str(revision)):
201 201 raise ChangesetDoesNotExistError("Given revision %r not recognized"
202 202 % revision)
203 203 return revision
204 204
205 205 def _get_archives(self, archive_name='tip'):
206 206
207 207 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
208 208 yield {"type": i[0], "extension": i[1], "node": archive_name}
209 209
210 210 def _get_url(self, url):
211 211 """
212 212 Returns normalized url. If schema is not given, would fall to
213 213 filesystem (``file:///``) schema.
214 214 """
215 215 url = str(url)
216 216 if url != 'default' and not '://' in url:
217 217 url = ':///'.join(('file', url))
218 218 return url
219 219
220 220 @LazyProperty
221 221 def name(self):
222 222 return os.path.basename(self.path)
223 223
224 224 @LazyProperty
225 225 def last_change(self):
226 226 """
227 227 Returns last change made on this repository as datetime object
228 228 """
229 229 return date_fromtimestamp(self._get_mtime(), makedate()[1])
230 230
231 231 def _get_mtime(self):
232 232 try:
233 233 return time.mktime(self.get_changeset().date.timetuple())
234 234 except RepositoryError:
235 235 idx_loc = '' if self.bare else '.git'
236 236 # fallback to filesystem
237 237 in_path = os.path.join(self.path, idx_loc, "index")
238 238 he_path = os.path.join(self.path, idx_loc, "HEAD")
239 239 if os.path.exists(in_path):
240 240 return os.stat(in_path).st_mtime
241 241 else:
242 242 return os.stat(he_path).st_mtime
243 243
244 244 @LazyProperty
245 245 def description(self):
246 246 idx_loc = '' if self.bare else '.git'
247 247 undefined_description = u'unknown'
248 248 description_path = os.path.join(self.path, idx_loc, 'description')
249 249 if os.path.isfile(description_path):
250 250 return safe_unicode(open(description_path).read())
251 251 else:
252 252 return undefined_description
253 253
254 254 @LazyProperty
255 255 def contact(self):
256 256 undefined_contact = u'Unknown'
257 257 return undefined_contact
258 258
259 259 @property
260 260 def branches(self):
261 261 if not self.revisions:
262 262 return {}
263 263 refs = self._repo.refs.as_dict()
264 264 sortkey = lambda ctx: ctx[0]
265 265 _branches = [('/'.join(ref.split('/')[2:]), head)
266 266 for ref, head in refs.items()
267 267 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
268 268 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
269 269
270 270 def _heads(self, reverse=False):
271 271 refs = self._repo.get_refs()
272 272 heads = {}
273 273
274 274 for key, val in refs.items():
275 275 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
276 276 if key.startswith(ref_key):
277 277 n = key[len(ref_key):]
278 278 if n not in ['HEAD']:
279 279 heads[n] = val
280 280
281 281 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
282 282
283 283 def _get_tags(self):
284 284 if not self.revisions:
285 285 return {}
286 286 sortkey = lambda ctx: ctx[0]
287 287 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
288 288 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
289 289 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
290 290
291 291 @LazyProperty
292 292 def tags(self):
293 293 return self._get_tags()
294 294
295 295 def tag(self, name, user, revision=None, message=None, date=None,
296 296 **kwargs):
297 297 """
298 298 Creates and returns a tag for the given ``revision``.
299 299
300 300 :param name: name for new tag
301 301 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
302 302 :param revision: changeset id for which new tag would be created
303 303 :param message: message of the tag's commit
304 304 :param date: date of tag's commit
305 305
306 306 :raises TagAlreadyExistError: if tag with same name already exists
307 307 """
308 308 if name in self.tags:
309 309 raise TagAlreadyExistError("Tag %s already exists" % name)
310 310 changeset = self.get_changeset(revision)
311 311 message = message or "Added tag %s for commit %s" % (name,
312 312 changeset.raw_id)
313 313 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
314 314
315 315 self.tags = self._get_tags()
316 316 return changeset
317 317
318 318 def remove_tag(self, name, user, message=None, date=None):
319 319 """
320 320 Removes tag with the given ``name``.
321 321
322 322 :param name: name of the tag to be removed
323 323 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
324 324 :param message: message of the tag's removal commit
325 325 :param date: date of tag's removal commit
326 326
327 327 :raises TagDoesNotExistError: if tag with given name does not exists
328 328 """
329 329 if name not in self.tags:
330 330 raise TagDoesNotExistError("Tag %s does not exist" % name)
331 331 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
332 332 try:
333 333 os.remove(tagpath)
334 334 self.tags = self._get_tags()
335 335 except OSError, e:
336 336 raise RepositoryError(e.strerror)
337 337
338 338 def get_changeset(self, revision=None):
339 339 """
340 340 Returns ``GitChangeset`` object representing commit from git repository
341 341 at the given revision or head (most recent commit) if None given.
342 342 """
343 343 if isinstance(revision, GitChangeset):
344 344 return revision
345 345 revision = self._get_revision(revision)
346 346 changeset = GitChangeset(repository=self, revision=revision)
347 347 return changeset
348 348
349 349 def get_changesets(self, start=None, end=None, start_date=None,
350 350 end_date=None, branch_name=None, reverse=False):
351 351 """
352 352 Returns iterator of ``GitChangeset`` objects from start to end (both
353 353 are inclusive), in ascending date order (unless ``reverse`` is set).
354 354
355 355 :param start: changeset ID, as str; first returned changeset
356 356 :param end: changeset ID, as str; last returned changeset
357 357 :param start_date: if specified, changesets with commit date less than
358 358 ``start_date`` would be filtered out from returned set
359 359 :param end_date: if specified, changesets with commit date greater than
360 360 ``end_date`` would be filtered out from returned set
361 361 :param branch_name: if specified, changesets not reachable from given
362 362 branch would be filtered out from returned set
363 363 :param reverse: if ``True``, returned generator would be reversed
364 364 (meaning that returned changesets would have descending date order)
365 365
366 366 :raise BranchDoesNotExistError: If given ``branch_name`` does not
367 367 exist.
368 368 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
369 369 ``end`` could not be found.
370 370
371 371 """
372 372 if branch_name and branch_name not in self.branches:
373 373 raise BranchDoesNotExistError("Branch '%s' not found" \
374 374 % branch_name)
375 375 # %H at format means (full) commit hash, initial hashes are retrieved
376 376 # in ascending date order
377 377 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
378 378 cmd_params = {}
379 379 if start_date:
380 380 cmd_template += ' --since "$since"'
381 381 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
382 382 if end_date:
383 383 cmd_template += ' --until "$until"'
384 384 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
385 385 if branch_name:
386 386 cmd_template += ' $branch_name'
387 387 cmd_params['branch_name'] = branch_name
388 388 else:
389 389 cmd_template += ' --all'
390 390
391 391 cmd = Template(cmd_template).safe_substitute(**cmd_params)
392 392 revs = self.run_git_command(cmd)[0].splitlines()
393 393 start_pos = 0
394 394 end_pos = len(revs)
395 395 if start:
396 396 _start = self._get_revision(start)
397 397 try:
398 398 start_pos = revs.index(_start)
399 399 except ValueError:
400 400 pass
401 401
402 402 if end is not None:
403 403 _end = self._get_revision(end)
404 404 try:
405 405 end_pos = revs.index(_end)
406 406 except ValueError:
407 407 pass
408 408
409 409 if None not in [start, end] and start_pos > end_pos:
410 410 raise RepositoryError('start cannot be after end')
411 411
412 412 if end_pos is not None:
413 413 end_pos += 1
414 414
415 415 revs = revs[start_pos:end_pos]
416 416 if reverse:
417 417 revs = reversed(revs)
418 418 for rev in revs:
419 419 yield self.get_changeset(rev)
420 420
421 421 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
422 422 context=3):
423 423 """
424 424 Returns (git like) *diff*, as plain text. Shows changes introduced by
425 425 ``rev2`` since ``rev1``.
426 426
427 427 :param rev1: Entry point from which diff is shown. Can be
428 428 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
429 429 the changes since empty state of the repository until ``rev2``
430 430 :param rev2: Until which revision changes should be shown.
431 431 :param ignore_whitespace: If set to ``True``, would not show whitespace
432 432 changes. Defaults to ``False``.
433 433 :param context: How many lines before/after changed lines should be
434 434 shown. Defaults to ``3``.
435 435 """
436 436 flags = ['-U%s' % context]
437 437 if ignore_whitespace:
438 438 flags.append('-w')
439 439
440 440 if rev1 == self.EMPTY_CHANGESET:
441 441 rev2 = self.get_changeset(rev2).raw_id
442 442 cmd = ' '.join(['show'] + flags + [rev2])
443 443 else:
444 444 rev1 = self.get_changeset(rev1).raw_id
445 445 rev2 = self.get_changeset(rev2).raw_id
446 446 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
447 447
448 448 if path:
449 449 cmd += ' -- "%s"' % path
450 450 stdout, stderr = self.run_git_command(cmd)
451 451 # If we used 'show' command, strip first few lines (until actual diff
452 452 # starts)
453 453 if rev1 == self.EMPTY_CHANGESET:
454 454 lines = stdout.splitlines()
455 455 x = 0
456 456 for line in lines:
457 457 if line.startswith('diff'):
458 458 break
459 459 x += 1
460 460 # Append new line just like 'diff' command do
461 461 stdout = '\n'.join(lines[x:]) + '\n'
462 462 return stdout
463 463
464 464 @LazyProperty
465 465 def in_memory_changeset(self):
466 466 """
467 467 Returns ``GitInMemoryChangeset`` object for this repository.
468 468 """
469 469 return GitInMemoryChangeset(self)
470 470
471 471 def clone(self, url, update_after_clone=True, bare=False):
472 472 """
473 473 Tries to clone changes from external location.
474 474
475 475 :param update_after_clone: If set to ``False``, git won't checkout
476 476 working directory
477 477 :param bare: If set to ``True``, repository would be cloned into
478 478 *bare* git repository (no working directory at all).
479 479 """
480 480 url = self._get_url(url)
481 481 cmd = ['clone']
482 482 if bare:
483 483 cmd.append('--bare')
484 484 elif not update_after_clone:
485 485 cmd.append('--no-checkout')
486 486 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
487 487 cmd = ' '.join(cmd)
488 488 # If error occurs run_git_command raises RepositoryError already
489 489 self.run_git_command(cmd)
490 490
491 491 def pull(self, url):
492 492 """
493 493 Tries to pull changes from external location.
494 494 """
495 495 url = self._get_url(url)
496 496 cmd = ['pull']
497 497 cmd.append("--ff-only")
498 498 cmd.append(url)
499 499 cmd = ' '.join(cmd)
500 500 # If error occurs run_git_command raises RepositoryError already
501 501 self.run_git_command(cmd)
502 502
503 503 @LazyProperty
504 504 def workdir(self):
505 505 """
506 506 Returns ``Workdir`` instance for this repository.
507 507 """
508 508 return GitWorkdir(self)
509 509
510 510 def get_config_value(self, section, name, config_file=None):
511 511 """
512 512 Returns configuration value for a given [``section``] and ``name``.
513 513
514 514 :param section: Section we want to retrieve value from
515 515 :param name: Name of configuration we want to retrieve
516 516 :param config_file: A path to file which should be used to retrieve
517 517 configuration from (might also be a list of file paths)
518 518 """
519 519 if config_file is None:
520 520 config_file = []
521 521 elif isinstance(config_file, basestring):
522 522 config_file = [config_file]
523 523
524 524 def gen_configs():
525 525 for path in config_file + self._config_files:
526 526 try:
527 527 yield ConfigFile.from_path(path)
528 528 except (IOError, OSError, ValueError):
529 529 continue
530 530
531 531 for config in gen_configs():
532 532 try:
533 533 return config.get(section, name)
534 534 except KeyError:
535 535 continue
536 536 return None
537 537
538 538 def get_user_name(self, config_file=None):
539 539 """
540 540 Returns user's name from global configuration file.
541 541
542 542 :param config_file: A path to file which should be used to retrieve
543 543 configuration from (might also be a list of file paths)
544 544 """
545 545 return self.get_config_value('user', 'name', config_file)
546 546
547 547 def get_user_email(self, config_file=None):
548 548 """
549 549 Returns user's email from global configuration file.
550 550
551 551 :param config_file: A path to file which should be used to retrieve
552 552 configuration from (might also be a list of file paths)
553 553 """
554 554 return self.get_config_value('user', 'email', config_file)
@@ -1,85 +1,85 b''
1 1 <div>
2 2 ${h.form(url('admin_settings_my_account_update'),method='put')}
3 3 <div class="form">
4 4
5 5 <div class="field">
6 6 <div class="gravatar_box">
7 7 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(c.user.email)}"/></div>
8 8 <p>
9 9 %if c.use_gravatar:
10 10 <strong>${_('Change your avatar at')} <a href="http://gravatar.com">gravatar.com</a></strong>
11 11 <br/>${_('Using')} ${c.user.email}
12 12 %else:
13 13 <br/>${c.user.email}
14 14 %endif
15 15 </p>
16 16 </div>
17 17 </div>
18 18 <div class="field">
19 19 <div class="label">
20 20 <label>${_('API key')}</label> ${c.user.api_key}
21 21 </div>
22 22 </div>
23 23 <div class="fields">
24 24 <div class="field">
25 25 <div class="label">
26 26 <label for="username">${_('Username')}:</label>
27 27 </div>
28 28 <div class="input">
29 29 ${h.text('username',class_="medium")}
30 30 </div>
31 31 </div>
32 32
33 33 <div class="field">
34 34 <div class="label">
35 35 <label for="new_password">${_('New password')}:</label>
36 36 </div>
37 37 <div class="input">
38 38 ${h.password('new_password',class_="medium",autocomplete="off")}
39 39 </div>
40 40 </div>
41 41
42 42 <div class="field">
43 43 <div class="label">
44 44 <label for="password_confirmation">${_('New password confirmation')}:</label>
45 45 </div>
46 46 <div class="input">
47 47 ${h.password('password_confirmation',class_="medium",autocomplete="off")}
48 48 </div>
49 49 </div>
50 50
51 51 <div class="field">
52 52 <div class="label">
53 53 <label for="name">${_('First Name')}:</label>
54 54 </div>
55 55 <div class="input">
56 56 ${h.text('name',class_="medium")}
57 57 </div>
58 58 </div>
59 59
60 60 <div class="field">
61 61 <div class="label">
62 62 <label for="lastname">${_('Last Name')}:</label>
63 63 </div>
64 64 <div class="input">
65 65 ${h.text('lastname',class_="medium")}
66 66 </div>
67 67 </div>
68 68
69 69 <div class="field">
70 70 <div class="label">
71 71 <label for="email">${_('Email')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 ${h.text('email',class_="medium")}
75 75 </div>
76 76 </div>
77 77
78 78 <div class="buttons">
79 79 ${h.submit('save',_('Save'),class_="ui-button")}
80 80 ${h.reset('reset',_('Reset'),class_="ui-button")}
81 81 </div>
82 82 </div>
83 83 </div>
84 84 ${h.end_form()}
85 </div> No newline at end of file
85 </div>
@@ -1,320 +1,320 b''
1 1 from rhodecode.tests import *
2 2
3 3 ARCHIVE_SPECS = {
4 4 '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
5 5 '.tar.gz': ('application/x-gzip', 'tgz', ''),
6 6 '.zip': ('application/zip', 'zip', ''),
7 7 }
8 8
9 9
10 10 class TestFilesController(TestController):
11 11
12 12 def test_index(self):
13 13 self.log_user()
14 14 response = self.app.get(url(controller='files', action='index',
15 15 repo_name=HG_REPO,
16 16 revision='tip',
17 17 f_path='/'))
18 18 # Test response...
19 19 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
20 20 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
21 21 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
22 22 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
23 23 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
24 24
25 25 def test_index_revision(self):
26 26 self.log_user()
27 27
28 28 response = self.app.get(
29 29 url(controller='files', action='index',
30 30 repo_name=HG_REPO,
31 31 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
32 32 f_path='/')
33 33 )
34 34
35 35 #Test response...
36 36
37 37 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
38 38 response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
39 39 response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
40 40 response.mustcontain('1.1 KiB')
41 41 response.mustcontain('text/x-python')
42 42
43 43 def test_index_different_branch(self):
44 44 self.log_user()
45 45
46 46 response = self.app.get(url(controller='files', action='index',
47 47 repo_name=HG_REPO,
48 48 revision='97e8b885c04894463c51898e14387d80c30ed1ee',
49 49 f_path='/'))
50 50
51 51 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
52 52
53 53 def test_index_paging(self):
54 54 self.log_user()
55 55
56 56 for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
57 57 (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
58 58 (109, '75feb4c33e81186c87eac740cee2447330288412'),
59 59 (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
60 60 (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
61 61
62 62 response = self.app.get(url(controller='files', action='index',
63 63 repo_name=HG_REPO,
64 64 revision=r[1],
65 65 f_path='/'))
66 66
67 67 response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
68 68
69 69 def test_file_source(self):
70 70 self.log_user()
71 71 response = self.app.get(url(controller='files', action='index',
72 72 repo_name=HG_REPO,
73 73 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
74 74 f_path='vcs/nodes.py'))
75 75
76 76 #test or history
77 77 response.mustcontain("""<optgroup label="Changesets">
78 78 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
79 79 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
80 80 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
81 81 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
82 82 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
83 83 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
84 84 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
85 85 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
86 86 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
87 87 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
88 88 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
89 89 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
90 90 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
91 91 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
92 92 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
93 93 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
94 94 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
95 95 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
96 96 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
97 97 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
98 98 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
99 99 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
100 100 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
101 101 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
102 102 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
103 103 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
104 104 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
105 105 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
106 106 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
107 107 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
108 108 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
109 109 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
110 110 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
111 111 </optgroup>
112 112 <optgroup label="Branches">
113 113 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
114 114 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
115 115 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
116 116 </optgroup>
117 117 <optgroup label="Tags">
118 118 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
119 119 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
120 120 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
121 121 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
122 122 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
123 123 </optgroup>
124 124 """)
125 125
126 126 response.mustcontain("""<div class="commit">merge</div>""")
127 127
128 128 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
129 129
130 130 def test_file_annotation(self):
131 131 self.log_user()
132 132 response = self.app.get(url(controller='files', action='index',
133 133 repo_name=HG_REPO,
134 134 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
135 135 f_path='vcs/nodes.py',
136 136 annotate=True))
137 137
138 138
139 139 response.mustcontain("""<optgroup label="Changesets">
140 140 <option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
141 141 <option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
142 142 <option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
143 143 <option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
144 144 <option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
145 145 <option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
146 146 <option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
147 147 <option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
148 148 <option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
149 149 <option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
150 150 <option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
151 151 <option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
152 152 <option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
153 153 <option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
154 154 <option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
155 155 <option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
156 156 <option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
157 157 <option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
158 158 <option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
159 159 <option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
160 160 <option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
161 161 <option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
162 162 <option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
163 163 <option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
164 164 <option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
165 165 <option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
166 166 <option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
167 167 <option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
168 168 <option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
169 169 <option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
170 170 <option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
171 171 <option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
172 172 <option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
173 173 </optgroup>
174 174 <optgroup label="Branches">
175 175 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
176 176 <option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
177 177 <option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
178 178 </optgroup>
179 179 <optgroup label="Tags">
180 180 <option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
181 181 <option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
182 182 <option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
183 183 <option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
184 184 <option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
185 185 </optgroup>""")
186 186
187 187 response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
188 188
189 189 def test_archival(self):
190 190 self.log_user()
191 191
192 192 for arch_ext, info in ARCHIVE_SPECS.items():
193 193 short = '27cd5cce30c9%s' % arch_ext
194 194 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
195 195 filename = '%s-%s' % (HG_REPO, short)
196 196 response = self.app.get(url(controller='files',
197 197 action='archivefile',
198 198 repo_name=HG_REPO,
199 199 fname=fname))
200 200
201 201 self.assertEqual(response.status, '200 OK')
202 202 heads = [
203 ('Pragma', 'no-cache'),
204 ('Cache-Control', 'no-cache'),
203 ('Pragma', 'no-cache'),
204 ('Cache-Control', 'no-cache'),
205 205 ('Content-Disposition', 'attachment; filename=%s' % filename),
206 206 ('Content-Type', '%s; charset=utf-8' % info[0]),
207 207 ]
208 208 self.assertEqual(response.response._headers.items(), heads)
209 209
210 210 def test_archival_wrong_ext(self):
211 211 self.log_user()
212 212
213 213 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
214 214 fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
215 215
216 response = self.app.get(url(controller='files',
216 response = self.app.get(url(controller='files',
217 217 action='archivefile',
218 218 repo_name=HG_REPO,
219 219 fname=fname))
220 220 response.mustcontain('Unknown archive type')
221 221
222 222 def test_archival_wrong_revision(self):
223 223 self.log_user()
224 224
225 225 for rev in ['00x000000', 'tar', 'wrong', '@##$@$42413232', '232dffcd']:
226 226 fname = '%s.zip' % rev
227 227
228 228 response = self.app.get(url(controller='files',
229 229 action='archivefile',
230 230 repo_name=HG_REPO,
231 231 fname=fname))
232 232 response.mustcontain('Unknown revision')
233 233
234 234 #==========================================================================
235 235 # RAW FILE
236 236 #==========================================================================
237 237 def test_raw_file_ok(self):
238 238 self.log_user()
239 239 response = self.app.get(url(controller='files', action='rawfile',
240 240 repo_name=HG_REPO,
241 241 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
242 242 f_path='vcs/nodes.py'))
243 243
244 244 self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
245 245 self.assertEqual(response.content_type, "text/x-python")
246 246
247 247 def test_raw_file_wrong_cs(self):
248 248 self.log_user()
249 249 rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
250 250 f_path = 'vcs/nodes.py'
251 251
252 252 response = self.app.get(url(controller='files', action='rawfile',
253 253 repo_name=HG_REPO,
254 254 revision=rev,
255 255 f_path=f_path))
256 256
257 257 msg = """Revision %r does not exist for this repository""" % (rev)
258 258 self.checkSessionFlash(response, msg)
259 259
260 260 msg = """%s""" % (HG_REPO)
261 261 self.checkSessionFlash(response, msg)
262 262
263 263 def test_raw_file_wrong_f_path(self):
264 264 self.log_user()
265 265 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
266 266 f_path = 'vcs/ERRORnodes.py'
267 267 response = self.app.get(url(controller='files', action='rawfile',
268 268 repo_name=HG_REPO,
269 269 revision=rev,
270 270 f_path=f_path))
271 271
272 272 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
273 273 self.checkSessionFlash(response, msg)
274 274
275 275 #==========================================================================
276 276 # RAW RESPONSE - PLAIN
277 277 #==========================================================================
278 278 def test_raw_ok(self):
279 279 self.log_user()
280 280 response = self.app.get(url(controller='files', action='raw',
281 281 repo_name=HG_REPO,
282 282 revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
283 283 f_path='vcs/nodes.py'))
284 284
285 285 self.assertEqual(response.content_type, "text/plain")
286 286
287 287 def test_raw_wrong_cs(self):
288 288 self.log_user()
289 289 rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
290 290 f_path = 'vcs/nodes.py'
291 291
292 292 response = self.app.get(url(controller='files', action='raw',
293 293 repo_name=HG_REPO,
294 294 revision=rev,
295 295 f_path=f_path))
296 296 msg = """Revision %r does not exist for this repository""" % (rev)
297 297 self.checkSessionFlash(response, msg)
298 298
299 299 msg = """%s""" % (HG_REPO)
300 300 self.checkSessionFlash(response, msg)
301 301
302 302 def test_raw_wrong_f_path(self):
303 303 self.log_user()
304 304 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
305 305 f_path = 'vcs/ERRORnodes.py'
306 306 response = self.app.get(url(controller='files', action='raw',
307 307 repo_name=HG_REPO,
308 308 revision=rev,
309 309 f_path=f_path))
310 310 msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
311 311 self.checkSessionFlash(response, msg)
312 312
313 313 def test_ajaxed_files_list(self):
314 314 self.log_user()
315 315 rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
316 316 response = self.app.get(
317 317 url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
318 318 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
319 319 )
320 320 response.mustcontain("vcs/web/simplevcs/views/repository.py")
General Comments 0
You need to be logged in to leave comments. Login now