##// END OF EJS Templates
added emulation of pull hook for git-backend, and dummy git-push hook
marcink -
r2203:d9972f76 beta
parent child Browse files
Show More
@@ -1,938 +1,941 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
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 def shorter(text, size=20):
50 50 postfix = '...'
51 51 if len(text) > size:
52 52 return text[:size - len(postfix)] + postfix
53 53 return text
54 54
55 55
56 56 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
57 57 """
58 58 Reset button
59 59 """
60 60 _set_input_attrs(attrs, type, name, value)
61 61 _set_id_attr(attrs, id, name)
62 62 convert_boolean_attrs(attrs, ["disabled"])
63 63 return HTML.input(**attrs)
64 64
65 65 reset = _reset
66 66 safeid = _make_safe_id_component
67 67
68 68
69 69 def FID(raw_id, path):
70 70 """
71 71 Creates a uniqe ID for filenode based on it's hash of path and revision
72 72 it's safe to use in urls
73 73
74 74 :param raw_id:
75 75 :param path:
76 76 """
77 77
78 78 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
79 79
80 80
81 81 def get_token():
82 82 """Return the current authentication token, creating one if one doesn't
83 83 already exist.
84 84 """
85 85 token_key = "_authentication_token"
86 86 from pylons import session
87 87 if not token_key in session:
88 88 try:
89 89 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
90 except AttributeError: # Python < 2.4
90 except AttributeError: # Python < 2.4
91 91 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
92 92 session[token_key] = token
93 93 if hasattr(session, 'save'):
94 94 session.save()
95 95 return session[token_key]
96 96
97 97
98 98 class _GetError(object):
99 99 """Get error from form_errors, and represent it as span wrapped error
100 100 message
101 101
102 102 :param field_name: field to fetch errors for
103 103 :param form_errors: form errors dict
104 104 """
105 105
106 106 def __call__(self, field_name, form_errors):
107 107 tmpl = """<span class="error_msg">%s</span>"""
108 108 if form_errors and form_errors.has_key(field_name):
109 109 return literal(tmpl % form_errors.get(field_name))
110 110
111 111 get_error = _GetError()
112 112
113 113
114 114 class _ToolTip(object):
115 115
116 116 def __call__(self, tooltip_title, trim_at=50):
117 117 """Special function just to wrap our text into nice formatted
118 118 autowrapped text
119 119
120 120 :param tooltip_title:
121 121 """
122 122 return escape(tooltip_title)
123 123 tooltip = _ToolTip()
124 124
125 125
126 126 class _FilesBreadCrumbs(object):
127 127
128 128 def __call__(self, repo_name, rev, paths):
129 129 if isinstance(paths, str):
130 130 paths = safe_unicode(paths)
131 131 url_l = [link_to(repo_name, url('files_home',
132 132 repo_name=repo_name,
133 133 revision=rev, f_path=''))]
134 134 paths_l = paths.split('/')
135 135 for cnt, p in enumerate(paths_l):
136 136 if p != '':
137 137 url_l.append(link_to(p,
138 138 url('files_home',
139 139 repo_name=repo_name,
140 140 revision=rev,
141 141 f_path='/'.join(paths_l[:cnt + 1])
142 142 )
143 143 )
144 144 )
145 145
146 146 return literal('/'.join(url_l))
147 147
148 148 files_breadcrumbs = _FilesBreadCrumbs()
149 149
150 150
151 151 class CodeHtmlFormatter(HtmlFormatter):
152 152 """
153 153 My code Html Formatter for source codes
154 154 """
155 155
156 156 def wrap(self, source, outfile):
157 157 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
158 158
159 159 def _wrap_code(self, source):
160 160 for cnt, it in enumerate(source):
161 161 i, t = it
162 162 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
163 163 yield i, t
164 164
165 165 def _wrap_tablelinenos(self, inner):
166 166 dummyoutfile = StringIO.StringIO()
167 167 lncount = 0
168 168 for t, line in inner:
169 169 if t:
170 170 lncount += 1
171 171 dummyoutfile.write(line)
172 172
173 173 fl = self.linenostart
174 174 mw = len(str(lncount + fl - 1))
175 175 sp = self.linenospecial
176 176 st = self.linenostep
177 177 la = self.lineanchors
178 178 aln = self.anchorlinenos
179 179 nocls = self.noclasses
180 180 if sp:
181 181 lines = []
182 182
183 183 for i in range(fl, fl + lncount):
184 184 if i % st == 0:
185 185 if i % sp == 0:
186 186 if aln:
187 187 lines.append('<a href="#%s%d" class="special">%*d</a>' %
188 188 (la, i, mw, i))
189 189 else:
190 190 lines.append('<span class="special">%*d</span>' % (mw, i))
191 191 else:
192 192 if aln:
193 193 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
194 194 else:
195 195 lines.append('%*d' % (mw, i))
196 196 else:
197 197 lines.append('')
198 198 ls = '\n'.join(lines)
199 199 else:
200 200 lines = []
201 201 for i in range(fl, fl + lncount):
202 202 if i % st == 0:
203 203 if aln:
204 204 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
205 205 else:
206 206 lines.append('%*d' % (mw, i))
207 207 else:
208 208 lines.append('')
209 209 ls = '\n'.join(lines)
210 210
211 211 # in case you wonder about the seemingly redundant <div> here: since the
212 212 # content in the other cell also is wrapped in a div, some browsers in
213 213 # some configurations seem to mess up the formatting...
214 214 if nocls:
215 215 yield 0, ('<table class="%stable">' % self.cssclass +
216 216 '<tr><td><div class="linenodiv" '
217 217 'style="background-color: #f0f0f0; padding-right: 10px">'
218 218 '<pre style="line-height: 125%">' +
219 219 ls + '</pre></div></td><td id="hlcode" class="code">')
220 220 else:
221 221 yield 0, ('<table class="%stable">' % self.cssclass +
222 222 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
223 223 ls + '</pre></div></td><td id="hlcode" class="code">')
224 224 yield 0, dummyoutfile.getvalue()
225 225 yield 0, '</td></tr></table>'
226 226
227 227
228 228 def pygmentize(filenode, **kwargs):
229 229 """pygmentize function using pygments
230 230
231 231 :param filenode:
232 232 """
233 233
234 234 return literal(code_highlight(filenode.content,
235 235 filenode.lexer, CodeHtmlFormatter(**kwargs)))
236 236
237 237
238 238 def pygmentize_annotation(repo_name, filenode, **kwargs):
239 239 """
240 240 pygmentize function for annotation
241 241
242 242 :param filenode:
243 243 """
244 244
245 245 color_dict = {}
246 246
247 247 def gen_color(n=10000):
248 248 """generator for getting n of evenly distributed colors using
249 249 hsv color and golden ratio. It always return same order of colors
250 250
251 251 :returns: RGB tuple
252 252 """
253 253
254 254 def hsv_to_rgb(h, s, v):
255 255 if s == 0.0:
256 256 return v, v, v
257 257 i = int(h * 6.0) # XXX assume int() truncates!
258 258 f = (h * 6.0) - i
259 259 p = v * (1.0 - s)
260 260 q = v * (1.0 - s * f)
261 261 t = v * (1.0 - s * (1.0 - f))
262 262 i = i % 6
263 263 if i == 0:
264 264 return v, t, p
265 265 if i == 1:
266 266 return q, v, p
267 267 if i == 2:
268 268 return p, v, t
269 269 if i == 3:
270 270 return p, q, v
271 271 if i == 4:
272 272 return t, p, v
273 273 if i == 5:
274 274 return v, p, q
275 275
276 276 golden_ratio = 0.618033988749895
277 277 h = 0.22717784590367374
278 278
279 279 for _ in xrange(n):
280 280 h += golden_ratio
281 281 h %= 1
282 282 HSV_tuple = [h, 0.95, 0.95]
283 283 RGB_tuple = hsv_to_rgb(*HSV_tuple)
284 284 yield map(lambda x: str(int(x * 256)), RGB_tuple)
285 285
286 286 cgenerator = gen_color()
287 287
288 288 def get_color_string(cs):
289 289 if cs in color_dict:
290 290 col = color_dict[cs]
291 291 else:
292 292 col = color_dict[cs] = cgenerator.next()
293 293 return "color: rgb(%s)! important;" % (', '.join(col))
294 294
295 295 def url_func(repo_name):
296 296
297 297 def _url_func(changeset):
298 298 author = changeset.author
299 299 date = changeset.date
300 300 message = tooltip(changeset.message)
301 301
302 302 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
303 303 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
304 304 "</b> %s<br/></div>")
305 305
306 306 tooltip_html = tooltip_html % (author, date, message)
307 307 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
308 308 short_id(changeset.raw_id))
309 309 uri = link_to(
310 310 lnk_format,
311 311 url('changeset_home', repo_name=repo_name,
312 312 revision=changeset.raw_id),
313 313 style=get_color_string(changeset.raw_id),
314 314 class_='tooltip',
315 315 title=tooltip_html
316 316 )
317 317
318 318 uri += '\n'
319 319 return uri
320 320 return _url_func
321 321
322 322 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
323 323
324 324
325 325 def is_following_repo(repo_name, user_id):
326 326 from rhodecode.model.scm import ScmModel
327 327 return ScmModel().is_following_repo(repo_name, user_id)
328 328
329 329 flash = _Flash()
330 330
331 331 #==============================================================================
332 332 # SCM FILTERS available via h.
333 333 #==============================================================================
334 334 from rhodecode.lib.vcs.utils import author_name, author_email
335 335 from rhodecode.lib.utils2 import credentials_filter, age as _age
336 336 from rhodecode.model.db import User
337 337
338 338 age = lambda x: _age(x)
339 339 capitalize = lambda x: x.capitalize()
340 340 email = author_email
341 341 short_id = lambda x: x[:12]
342 342 hide_credentials = lambda x: ''.join(credentials_filter(x))
343 343
344 344
345 345 def is_git(repository):
346 346 if hasattr(repository, 'alias'):
347 347 _type = repository.alias
348 348 elif hasattr(repository, 'repo_type'):
349 349 _type = repository.repo_type
350 350 else:
351 351 _type = repository
352 352 return _type == 'git'
353 353
354 354
355 355 def is_hg(repository):
356 356 if hasattr(repository, 'alias'):
357 357 _type = repository.alias
358 358 elif hasattr(repository, 'repo_type'):
359 359 _type = repository.repo_type
360 360 else:
361 361 _type = repository
362 362 return _type == 'hg'
363 363
364 364
365 365 def email_or_none(author):
366 366 _email = email(author)
367 367 if _email != '':
368 368 return _email
369 369
370 370 # See if it contains a username we can get an email from
371 371 user = User.get_by_username(author_name(author), case_insensitive=True,
372 372 cache=True)
373 373 if user is not None:
374 374 return user.email
375 375
376 376 # No valid email, not a valid user in the system, none!
377 377 return None
378 378
379 379
380 380 def person(author):
381 381 # attr to return from fetched user
382 382 person_getter = lambda usr: usr.username
383 383
384 384 # Valid email in the attribute passed, see if they're in the system
385 385 _email = email(author)
386 386 if _email != '':
387 387 user = User.get_by_email(_email, case_insensitive=True, cache=True)
388 388 if user is not None:
389 389 return person_getter(user)
390 390 return _email
391 391
392 392 # Maybe it's a username?
393 393 _author = author_name(author)
394 394 user = User.get_by_username(_author, case_insensitive=True,
395 395 cache=True)
396 396 if user is not None:
397 397 return person_getter(user)
398 398
399 399 # Still nothing? Just pass back the author name then
400 400 return _author
401 401
402 402
403 403 def bool2icon(value):
404 404 """Returns True/False values represented as small html image of true/false
405 405 icons
406 406
407 407 :param value: bool value
408 408 """
409 409
410 410 if value is True:
411 411 return HTML.tag('img', src=url("/images/icons/accept.png"),
412 412 alt=_('True'))
413 413
414 414 if value is False:
415 415 return HTML.tag('img', src=url("/images/icons/cancel.png"),
416 416 alt=_('False'))
417 417
418 418 return value
419 419
420 420
421 421 def action_parser(user_log, feed=False):
422 422 """
423 423 This helper will action_map the specified string action into translated
424 424 fancy names with icons and links
425 425
426 426 :param user_log: user log instance
427 427 :param feed: use output for feeds (no html and fancy icons)
428 428 """
429 429
430 430 action = user_log.action
431 431 action_params = ' '
432 432
433 433 x = action.split(':')
434 434
435 435 if len(x) > 1:
436 436 action, action_params = x
437 437
438 438 def get_cs_links():
439 439 revs_limit = 3 # display this amount always
440 440 revs_top_limit = 50 # show upto this amount of changesets hidden
441 441 revs_ids = action_params.split(',')
442 442 deleted = user_log.repository is None
443 443 if deleted:
444 444 return ','.join(revs_ids)
445 445
446 446 repo_name = user_log.repository.repo_name
447 447
448 448 repo = user_log.repository.scm_instance
449 449
450 450 message = lambda rev: rev.message
451 451 lnk = lambda rev, repo_name: (
452 452 link_to('r%s:%s' % (rev.revision, rev.short_id),
453 453 url('changeset_home', repo_name=repo_name,
454 454 revision=rev.raw_id),
455 455 title=tooltip(message(rev)), class_='tooltip')
456 456 )
457 # get only max revs_top_limit of changeset for performance/ui reasons
458 revs = [
459 x for x in repo.get_changesets(revs_ids[0],
460 revs_ids[:revs_top_limit][-1])
461 ]
457
458 revs = []
459 if len(filter(lambda v: v != '', revs_ids)) > 0:
460 # get only max revs_top_limit of changeset for performance/ui reasons
461 revs = [
462 x for x in repo.get_changesets(revs_ids[0],
463 revs_ids[:revs_top_limit][-1])
464 ]
462 465
463 466 cs_links = []
464 467 cs_links.append(" " + ', '.join(
465 468 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
466 469 )
467 470 )
468 471
469 472 compare_view = (
470 473 ' <div class="compare_view tooltip" title="%s">'
471 474 '<a href="%s">%s</a> </div>' % (
472 475 _('Show all combined changesets %s->%s') % (
473 476 revs_ids[0], revs_ids[-1]
474 477 ),
475 478 url('changeset_home', repo_name=repo_name,
476 479 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
477 480 ),
478 481 _('compare view')
479 482 )
480 483 )
481 484
482 485 # if we have exactly one more than normally displayed
483 486 # just display it, takes less space than displaying
484 487 # "and 1 more revisions"
485 488 if len(revs_ids) == revs_limit + 1:
486 489 rev = revs[revs_limit]
487 490 cs_links.append(", " + lnk(rev, repo_name))
488 491
489 492 # hidden-by-default ones
490 493 if len(revs_ids) > revs_limit + 1:
491 494 uniq_id = revs_ids[0]
492 495 html_tmpl = (
493 496 '<span> %s <a class="show_more" id="_%s" '
494 497 'href="#more">%s</a> %s</span>'
495 498 )
496 499 if not feed:
497 500 cs_links.append(html_tmpl % (
498 501 _('and'),
499 502 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
500 503 _('revisions')
501 504 )
502 505 )
503 506
504 507 if not feed:
505 508 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
506 509 else:
507 510 html_tmpl = '<span id="%s"> %s </span>'
508 511
509 512 morelinks = ', '.join(
510 513 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
511 514 )
512 515
513 516 if len(revs_ids) > revs_top_limit:
514 517 morelinks += ', ...'
515 518
516 519 cs_links.append(html_tmpl % (uniq_id, morelinks))
517 520 if len(revs) > 1:
518 521 cs_links.append(compare_view)
519 522 return ''.join(cs_links)
520 523
521 524 def get_fork_name():
522 525 repo_name = action_params
523 526 return _('fork name ') + str(link_to(action_params, url('summary_home',
524 527 repo_name=repo_name,)))
525 528
526 529 action_map = {'user_deleted_repo': (_('[deleted] repository'), None),
527 530 'user_created_repo': (_('[created] repository'), None),
528 531 'user_created_fork': (_('[created] repository as fork'), None),
529 532 'user_forked_repo': (_('[forked] repository'), get_fork_name),
530 533 'user_updated_repo': (_('[updated] repository'), None),
531 534 'admin_deleted_repo': (_('[delete] repository'), None),
532 535 'admin_created_repo': (_('[created] repository'), None),
533 536 'admin_forked_repo': (_('[forked] repository'), None),
534 537 'admin_updated_repo': (_('[updated] repository'), None),
535 538 'push': (_('[pushed] into'), get_cs_links),
536 539 'push_local': (_('[committed via RhodeCode] into'), get_cs_links),
537 540 'push_remote': (_('[pulled from remote] into'), get_cs_links),
538 541 'pull': (_('[pulled] from'), None),
539 542 'started_following_repo': (_('[started following] repository'), None),
540 543 'stopped_following_repo': (_('[stopped following] repository'), None),
541 544 }
542 545
543 546 action_str = action_map.get(action, action)
544 547 if feed:
545 548 action = action_str[0].replace('[', '').replace(']', '')
546 549 else:
547 550 action = action_str[0]\
548 551 .replace('[', '<span class="journal_highlight">')\
549 552 .replace(']', '</span>')
550 553
551 554 action_params_func = lambda: ""
552 555
553 556 if callable(action_str[1]):
554 557 action_params_func = action_str[1]
555 558
556 559 return [literal(action), action_params_func]
557 560
558 561
559 562 def action_parser_icon(user_log):
560 563 action = user_log.action
561 564 action_params = None
562 565 x = action.split(':')
563 566
564 567 if len(x) > 1:
565 568 action, action_params = x
566 569
567 570 tmpl = """<img src="%s%s" alt="%s"/>"""
568 571 map = {'user_deleted_repo':'database_delete.png',
569 572 'user_created_repo':'database_add.png',
570 573 'user_created_fork':'arrow_divide.png',
571 574 'user_forked_repo':'arrow_divide.png',
572 575 'user_updated_repo':'database_edit.png',
573 576 'admin_deleted_repo':'database_delete.png',
574 577 'admin_created_repo':'database_add.png',
575 578 'admin_forked_repo':'arrow_divide.png',
576 579 'admin_updated_repo':'database_edit.png',
577 580 'push':'script_add.png',
578 581 'push_local':'script_edit.png',
579 582 'push_remote':'connect.png',
580 583 'pull':'down_16.png',
581 584 'started_following_repo':'heart_add.png',
582 585 'stopped_following_repo':'heart_delete.png',
583 586 }
584 587 return literal(tmpl % ((url('/images/icons/')),
585 588 map.get(action, action), action))
586 589
587 590
588 591 #==============================================================================
589 592 # PERMS
590 593 #==============================================================================
591 594 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
592 595 HasRepoPermissionAny, HasRepoPermissionAll
593 596
594 597
595 598 #==============================================================================
596 599 # GRAVATAR URL
597 600 #==============================================================================
598 601
599 602 def gravatar_url(email_address, size=30):
600 603 if (not str2bool(config['app_conf'].get('use_gravatar')) or
601 604 not email_address or email_address == 'anonymous@rhodecode.org'):
602 605 f = lambda a, l: min(l, key=lambda x: abs(x - a))
603 606 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
604 607
605 608 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
606 609 default = 'identicon'
607 610 baseurl_nossl = "http://www.gravatar.com/avatar/"
608 611 baseurl_ssl = "https://secure.gravatar.com/avatar/"
609 612 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
610 613
611 614 if isinstance(email_address, unicode):
612 615 #hashlib crashes on unicode items
613 616 email_address = safe_str(email_address)
614 617 # construct the url
615 618 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
616 619 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
617 620
618 621 return gravatar_url
619 622
620 623
621 624 #==============================================================================
622 625 # REPO PAGER, PAGER FOR REPOSITORY
623 626 #==============================================================================
624 627 class RepoPage(Page):
625 628
626 629 def __init__(self, collection, page=1, items_per_page=20,
627 630 item_count=None, url=None, **kwargs):
628 631
629 632 """Create a "RepoPage" instance. special pager for paging
630 633 repository
631 634 """
632 635 self._url_generator = url
633 636
634 637 # Safe the kwargs class-wide so they can be used in the pager() method
635 638 self.kwargs = kwargs
636 639
637 640 # Save a reference to the collection
638 641 self.original_collection = collection
639 642
640 643 self.collection = collection
641 644
642 645 # The self.page is the number of the current page.
643 646 # The first page has the number 1!
644 647 try:
645 648 self.page = int(page) # make it int() if we get it as a string
646 649 except (ValueError, TypeError):
647 650 self.page = 1
648 651
649 652 self.items_per_page = items_per_page
650 653
651 654 # Unless the user tells us how many items the collections has
652 655 # we calculate that ourselves.
653 656 if item_count is not None:
654 657 self.item_count = item_count
655 658 else:
656 659 self.item_count = len(self.collection)
657 660
658 661 # Compute the number of the first and last available page
659 662 if self.item_count > 0:
660 663 self.first_page = 1
661 664 self.page_count = int(math.ceil(float(self.item_count) /
662 665 self.items_per_page))
663 666 self.last_page = self.first_page + self.page_count - 1
664 667
665 668 # Make sure that the requested page number is the range of
666 669 # valid pages
667 670 if self.page > self.last_page:
668 671 self.page = self.last_page
669 672 elif self.page < self.first_page:
670 673 self.page = self.first_page
671 674
672 675 # Note: the number of items on this page can be less than
673 676 # items_per_page if the last page is not full
674 677 self.first_item = max(0, (self.item_count) - (self.page *
675 678 items_per_page))
676 679 self.last_item = ((self.item_count - 1) - items_per_page *
677 680 (self.page - 1))
678 681
679 682 self.items = list(self.collection[self.first_item:self.last_item + 1])
680 683
681 684 # Links to previous and next page
682 685 if self.page > self.first_page:
683 686 self.previous_page = self.page - 1
684 687 else:
685 688 self.previous_page = None
686 689
687 690 if self.page < self.last_page:
688 691 self.next_page = self.page + 1
689 692 else:
690 693 self.next_page = None
691 694
692 695 # No items available
693 696 else:
694 697 self.first_page = None
695 698 self.page_count = 0
696 699 self.last_page = None
697 700 self.first_item = None
698 701 self.last_item = None
699 702 self.previous_page = None
700 703 self.next_page = None
701 704 self.items = []
702 705
703 706 # This is a subclass of the 'list' type. Initialise the list now.
704 707 list.__init__(self, reversed(self.items))
705 708
706 709
707 710 def changed_tooltip(nodes):
708 711 """
709 712 Generates a html string for changed nodes in changeset page.
710 713 It limits the output to 30 entries
711 714
712 715 :param nodes: LazyNodesGenerator
713 716 """
714 717 if nodes:
715 718 pref = ': <br/> '
716 719 suf = ''
717 720 if len(nodes) > 30:
718 721 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
719 722 return literal(pref + '<br/> '.join([safe_unicode(x.path)
720 723 for x in nodes[:30]]) + suf)
721 724 else:
722 725 return ': ' + _('No Files')
723 726
724 727
725 728 def repo_link(groups_and_repos):
726 729 """
727 730 Makes a breadcrumbs link to repo within a group
728 731 joins &raquo; on each group to create a fancy link
729 732
730 733 ex::
731 734 group >> subgroup >> repo
732 735
733 736 :param groups_and_repos:
734 737 """
735 738 groups, repo_name = groups_and_repos
736 739
737 740 if not groups:
738 741 return repo_name
739 742 else:
740 743 def make_link(group):
741 744 return link_to(group.name, url('repos_group_home',
742 745 group_name=group.group_name))
743 746 return literal(' &raquo; '.join(map(make_link, groups)) + \
744 747 " &raquo; " + repo_name)
745 748
746 749
747 750 def fancy_file_stats(stats):
748 751 """
749 752 Displays a fancy two colored bar for number of added/deleted
750 753 lines of code on file
751 754
752 755 :param stats: two element list of added/deleted lines of code
753 756 """
754 757
755 758 a, d, t = stats[0], stats[1], stats[0] + stats[1]
756 759 width = 100
757 760 unit = float(width) / (t or 1)
758 761
759 762 # needs > 9% of width to be visible or 0 to be hidden
760 763 a_p = max(9, unit * a) if a > 0 else 0
761 764 d_p = max(9, unit * d) if d > 0 else 0
762 765 p_sum = a_p + d_p
763 766
764 767 if p_sum > width:
765 768 #adjust the percentage to be == 100% since we adjusted to 9
766 769 if a_p > d_p:
767 770 a_p = a_p - (p_sum - width)
768 771 else:
769 772 d_p = d_p - (p_sum - width)
770 773
771 774 a_v = a if a > 0 else ''
772 775 d_v = d if d > 0 else ''
773 776
774 777 def cgen(l_type):
775 778 mapping = {'tr': 'top-right-rounded-corner-mid',
776 779 'tl': 'top-left-rounded-corner-mid',
777 780 'br': 'bottom-right-rounded-corner-mid',
778 781 'bl': 'bottom-left-rounded-corner-mid'}
779 782 map_getter = lambda x: mapping[x]
780 783
781 784 if l_type == 'a' and d_v:
782 785 #case when added and deleted are present
783 786 return ' '.join(map(map_getter, ['tl', 'bl']))
784 787
785 788 if l_type == 'a' and not d_v:
786 789 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
787 790
788 791 if l_type == 'd' and a_v:
789 792 return ' '.join(map(map_getter, ['tr', 'br']))
790 793
791 794 if l_type == 'd' and not a_v:
792 795 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
793 796
794 797 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
795 798 cgen('a'), a_p, a_v
796 799 )
797 800 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
798 801 cgen('d'), d_p, d_v
799 802 )
800 803 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
801 804
802 805
803 806 def urlify_text(text_):
804 807 import re
805 808
806 809 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
807 810 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
808 811
809 812 def url_func(match_obj):
810 813 url_full = match_obj.groups()[0]
811 814 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
812 815
813 816 return literal(url_pat.sub(url_func, text_))
814 817
815 818
816 819 def urlify_changesets(text_, repository):
817 820 """
818 821 Extract revision ids from changeset and make link from them
819 822
820 823 :param text_:
821 824 :param repository:
822 825 """
823 826 import re
824 827 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
825 828
826 829 def url_func(match_obj):
827 830 rev = match_obj.groups()[0]
828 831 pref = ''
829 832 if match_obj.group().startswith(' '):
830 833 pref = ' '
831 834 tmpl = (
832 835 '%(pref)s<a class="%(cls)s" href="%(url)s">'
833 836 '%(rev)s'
834 837 '</a>'
835 838 )
836 839 return tmpl % {
837 840 'pref': pref,
838 841 'cls': 'revision-link',
839 842 'url': url('changeset_home', repo_name=repository, revision=rev),
840 843 'rev': rev,
841 844 }
842 845
843 846 newtext = URL_PAT.sub(url_func, text_)
844 847
845 848 return newtext
846 849
847 850
848 851 def urlify_commit(text_, repository=None, link_=None):
849 852 """
850 853 Parses given text message and makes proper links.
851 854 issues are linked to given issue-server, and rest is a changeset link
852 855 if link_ is given, in other case it's a plain text
853 856
854 857 :param text_:
855 858 :param repository:
856 859 :param link_: changeset link
857 860 """
858 861 import re
859 862 import traceback
860 863
861 864 def escaper(string):
862 865 return string.replace('<', '&lt;').replace('>', '&gt;')
863 866
864 867 def linkify_others(t, l):
865 868 urls = re.compile(r'(\<a.*?\<\/a\>)',)
866 869 links = []
867 870 for e in urls.split(t):
868 871 if not urls.match(e):
869 872 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
870 873 else:
871 874 links.append(e)
872 875
873 876 return ''.join(links)
874 877
875 878
876 879 # urlify changesets - extrac revisions and make link out of them
877 880 text_ = urlify_changesets(escaper(text_), repository)
878 881
879 882 try:
880 883 conf = config['app_conf']
881 884
882 885 URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
883 886
884 887 if URL_PAT:
885 888 ISSUE_SERVER_LNK = conf.get('issue_server_link')
886 889 ISSUE_PREFIX = conf.get('issue_prefix')
887 890
888 891 def url_func(match_obj):
889 892 pref = ''
890 893 if match_obj.group().startswith(' '):
891 894 pref = ' '
892 895
893 896 issue_id = ''.join(match_obj.groups())
894 897 tmpl = (
895 898 '%(pref)s<a class="%(cls)s" href="%(url)s">'
896 899 '%(issue-prefix)s%(id-repr)s'
897 900 '</a>'
898 901 )
899 902 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
900 903 if repository:
901 904 url = url.replace('{repo}', repository)
902 905
903 906 return tmpl % {
904 907 'pref': pref,
905 908 'cls': 'issue-tracker-link',
906 909 'url': url,
907 910 'id-repr': issue_id,
908 911 'issue-prefix': ISSUE_PREFIX,
909 912 'serv': ISSUE_SERVER_LNK,
910 913 }
911 914
912 915 newtext = URL_PAT.sub(url_func, text_)
913 916
914 917 if link_:
915 918 # wrap not links into final link => link_
916 919 newtext = linkify_others(newtext, link_)
917 920
918 921 return literal(newtext)
919 922 except:
920 923 log.error(traceback.format_exc())
921 924 pass
922 925
923 926 return text_
924 927
925 928
926 929 def rst(source):
927 930 return literal('<div class="rst-block">%s</div>' %
928 931 MarkupRenderer.rst(source))
929 932
930 933
931 934 def rst_w_mentions(source):
932 935 """
933 936 Wrapped rst renderer with @mention highlighting
934 937
935 938 :param source:
936 939 """
937 940 return literal('<div class="rst-block">%s</div>' %
938 941 MarkupRenderer.rst_with_mentions(source))
@@ -1,184 +1,190 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.hooks
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Hooks runned by rhodecode
7 7
8 8 :created_on: Aug 6, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-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 import os
26 26 import sys
27 27
28 28 from mercurial.scmutil import revrange
29 29 from mercurial.node import nullrev
30 30 from rhodecode import EXTENSIONS
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.utils import action_logger
33 33 from inspect import isfunction
34 34
35 35
36 36 def _get_scm_size(alias, root_path):
37 37
38 38 if not alias.startswith('.'):
39 39 alias += '.'
40 40
41 41 size_scm, size_root = 0, 0
42 42 for path, dirs, files in os.walk(root_path):
43 43 if path.find(alias) != -1:
44 44 for f in files:
45 45 try:
46 46 size_scm += os.path.getsize(os.path.join(path, f))
47 47 except OSError:
48 48 pass
49 49 else:
50 50 for f in files:
51 51 try:
52 52 size_root += os.path.getsize(os.path.join(path, f))
53 53 except OSError:
54 54 pass
55 55
56 56 size_scm_f = h.format_byte_size(size_scm)
57 57 size_root_f = h.format_byte_size(size_root)
58 58 size_total_f = h.format_byte_size(size_root + size_scm)
59 59
60 60 return size_scm_f, size_root_f, size_total_f
61 61
62 62
63 63 def repo_size(ui, repo, hooktype=None, **kwargs):
64 64 """
65 65 Presents size of repository after push
66 66
67 67 :param ui:
68 68 :param repo:
69 69 :param hooktype:
70 70 """
71 71
72 72 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
73 73
74 74 last_cs = repo[len(repo) - 1]
75 75
76 76 msg = ('Repository size .hg:%s repo:%s total:%s\n'
77 77 'Last revision is now r%s:%s\n') % (
78 78 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
79 79 )
80 80
81 81 sys.stdout.write(msg)
82 82
83 83
84 84 def log_pull_action(ui, repo, **kwargs):
85 85 """
86 86 Logs user last pull action
87 87
88 88 :param ui:
89 89 :param repo:
90 90 """
91 91
92 92 extras = dict(repo.ui.configitems('rhodecode_extras'))
93 93 username = extras['username']
94 94 repository = extras['repository']
95 scm = extras['scm']
95 96 action = 'pull'
96 97
97 98 action_logger(username, action, repository, extras['ip'], commit=True)
98 99 # extension hook call
99 100 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
100 101
101 102 if isfunction(callback):
102 103 kw = {}
103 104 kw.update(extras)
104 105 callback(**kw)
105 106 return 0
106 107
107 108
108 109 def log_push_action(ui, repo, **kwargs):
109 110 """
110 111 Maps user last push action to new changeset id, from mercurial
111 112
112 113 :param ui:
113 114 :param repo:
114 115 """
115 116
116 117 extras = dict(repo.ui.configitems('rhodecode_extras'))
117 118 username = extras['username']
118 119 repository = extras['repository']
119 120 action = extras['action'] + ':%s'
120 node = kwargs['node']
121 scm = extras['scm']
121 122
122 def get_revs(repo, rev_opt):
123 if rev_opt:
124 revs = revrange(repo, rev_opt)
123 if scm == 'hg':
124 node = kwargs['node']
125
126 def get_revs(repo, rev_opt):
127 if rev_opt:
128 revs = revrange(repo, rev_opt)
125 129
126 if len(revs) == 0:
127 return (nullrev, nullrev)
128 return (max(revs), min(revs))
129 else:
130 return (len(repo) - 1, 0)
130 if len(revs) == 0:
131 return (nullrev, nullrev)
132 return (max(revs), min(revs))
133 else:
134 return (len(repo) - 1, 0)
131 135
132 stop, start = get_revs(repo, [node + ':'])
136 stop, start = get_revs(repo, [node + ':'])
133 137
134 revs = (str(repo[r]) for r in xrange(start, stop + 1))
138 revs = (str(repo[r]) for r in xrange(start, stop + 1))
139 elif scm == 'git':
140 revs = []
135 141
136 142 action = action % ','.join(revs)
137 143
138 144 action_logger(username, action, repository, extras['ip'], commit=True)
139 145
140 146 # extension hook call
141 147 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
142 148 if isfunction(callback):
143 149 kw = {'pushed_revs': revs}
144 150 kw.update(extras)
145 151 callback(**kw)
146 152 return 0
147 153
148 154
149 155 def log_create_repository(repository_dict, created_by, **kwargs):
150 156 """
151 157 Post create repository Hook. This is a dummy function for admins to re-use
152 158 if needed. It's taken from rhodecode-extensions module and executed
153 159 if present
154 160
155 161 :param repository: dict dump of repository object
156 162 :param created_by: username who created repository
157 163 :param created_date: date of creation
158 164
159 165 available keys of repository_dict:
160 166
161 167 'repo_type',
162 168 'description',
163 169 'private',
164 170 'created_on',
165 171 'enable_downloads',
166 172 'repo_id',
167 173 'user_id',
168 174 'enable_statistics',
169 175 'clone_uri',
170 176 'fork_id',
171 177 'group_id',
172 178 'repo_name'
173 179
174 180 """
175 181
176 182 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
177 183 if isfunction(callback):
178 184 kw = {}
179 185 kw.update(repository_dict)
180 186 kw.update({'created_by': created_by})
181 187 kw.update(kwargs)
182 188 return callback(**kw)
183 189
184 190 return 0
@@ -1,251 +1,287 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import re
29 29 import logging
30 30 import traceback
31 31
32 32 from dulwich import server as dulserver
33 33
34 34
35 35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36 36
37 37 def handle(self):
38 38 write = lambda x: self.proto.write_sideband(1, x)
39 39
40 40 graph_walker = dulserver.ProtocolGraphWalker(self,
41 41 self.repo.object_store,
42 42 self.repo.get_peeled)
43 43 objects_iter = self.repo.fetch_objects(
44 44 graph_walker.determine_wants, graph_walker, self.progress,
45 45 get_tagged=self.get_tagged)
46 46
47 47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
48 48 # that the client still expects a 0-object pack in most cases.
49 49 if objects_iter is None:
50 50 return
51 51
52 52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
53 53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
54 54 objects_iter)
55 55 messages = []
56 56 messages.append('thank you for using rhodecode')
57 57
58 58 for msg in messages:
59 59 self.progress(msg + "\n")
60 60 # we are done
61 61 self.proto.write("0000")
62 62
63 63
64 64 dulserver.DEFAULT_HANDLERS = {
65 65 'git-upload-pack': SimpleGitUploadPackHandler,
66 66 'git-receive-pack': dulserver.ReceivePackHandler,
67 67 }
68 68
69 69 from dulwich.repo import Repo
70 70 from dulwich.web import make_wsgi_chain
71 71
72 72 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
73 73
74 74 from rhodecode.lib.utils2 import safe_str
75 75 from rhodecode.lib.base import BaseVCSController
76 76 from rhodecode.lib.auth import get_container_username
77 from rhodecode.lib.utils import is_valid_repo
77 from rhodecode.lib.utils import is_valid_repo, make_ui
78 78 from rhodecode.model.db import User
79 79
80 80 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
81 81
82 82 log = logging.getLogger(__name__)
83 83
84 84
85 85 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
86 86
87 87
88 88 def is_git(environ):
89 89 path_info = environ['PATH_INFO']
90 90 isgit_path = GIT_PROTO_PAT.match(path_info)
91 91 log.debug('pathinfo: %s detected as GIT %s' % (
92 92 path_info, isgit_path != None)
93 93 )
94 94 return isgit_path
95 95
96 96
97 97 class SimpleGit(BaseVCSController):
98 98
99 99 def _handle_request(self, environ, start_response):
100 100
101 101 if not is_git(environ):
102 102 return self.application(environ, start_response)
103 103
104 104 ipaddr = self._get_ip_addr(environ)
105 105 username = None
106 self._git_first_op = False
106 107 # skip passing error to error controller
107 108 environ['pylons.status_code_redirect'] = True
108 109
109 110 #======================================================================
110 111 # EXTRACT REPOSITORY NAME FROM ENV
111 112 #======================================================================
112 113 try:
113 114 repo_name = self.__get_repository(environ)
114 115 log.debug('Extracted repo name is %s' % repo_name)
115 116 except:
116 117 return HTTPInternalServerError()(environ, start_response)
117 118
118 119 # quick check if that dir exists...
119 120 if is_valid_repo(repo_name, self.basepath) is False:
120 121 return HTTPNotFound()(environ, start_response)
121 122
122 123 #======================================================================
123 124 # GET ACTION PULL or PUSH
124 125 #======================================================================
125 126 action = self.__get_action(environ)
126 127
127 128 #======================================================================
128 129 # CHECK ANONYMOUS PERMISSION
129 130 #======================================================================
130 131 if action in ['pull', 'push']:
131 132 anonymous_user = self.__get_user('default')
132 133 username = anonymous_user.username
133 134 anonymous_perm = self._check_permission(action, anonymous_user,
134 135 repo_name)
135 136
136 137 if anonymous_perm is not True or anonymous_user.active is False:
137 138 if anonymous_perm is not True:
138 139 log.debug('Not enough credentials to access this '
139 140 'repository as anonymous user')
140 141 if anonymous_user.active is False:
141 142 log.debug('Anonymous access is disabled, running '
142 143 'authentication')
143 144 #==============================================================
144 145 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
145 146 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
146 147 #==============================================================
147 148
148 149 # Attempting to retrieve username from the container
149 150 username = get_container_username(environ, self.config)
150 151
151 152 # If not authenticated by the container, running basic auth
152 153 if not username:
153 154 self.authenticate.realm = \
154 155 safe_str(self.config['rhodecode_realm'])
155 156 result = self.authenticate(environ)
156 157 if isinstance(result, str):
157 158 AUTH_TYPE.update(environ, 'basic')
158 159 REMOTE_USER.update(environ, result)
159 160 username = result
160 161 else:
161 162 return result.wsgi_application(environ, start_response)
162 163
163 164 #==============================================================
164 165 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
165 166 #==============================================================
166 167 if action in ['pull', 'push']:
167 168 try:
168 169 user = self.__get_user(username)
169 170 if user is None or not user.active:
170 171 return HTTPForbidden()(environ, start_response)
171 172 username = user.username
172 173 except:
173 174 log.error(traceback.format_exc())
174 175 return HTTPInternalServerError()(environ,
175 176 start_response)
176 177
177 178 #check permissions for this repository
178 179 perm = self._check_permission(action, user, repo_name)
179 180 if perm is not True:
180 181 return HTTPForbidden()(environ, start_response)
182 extras = {
183 'ip': ipaddr,
184 'username': username,
185 'action': action,
186 'repository': repo_name,
187 'scm': 'git',
188 }
181 189
182 190 #===================================================================
183 191 # GIT REQUEST HANDLING
184 192 #===================================================================
185 193 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
186 194 log.debug('Repository path is %s' % repo_path)
187 195
196 baseui = make_ui('db')
197 for k, v in extras.items():
198 baseui.setconfig('rhodecode_extras', k, v)
199
188 200 try:
189 #invalidate cache on push
201 # invalidate cache on push
190 202 if action == 'push':
191 203 self._invalidate_cache(repo_name)
204 self._handle_githooks(action, baseui, environ)
205
192 206 log.info('%s action on GIT repo "%s"' % (action, repo_name))
193 207 app = self.__make_app(repo_name, repo_path)
194 208 return app(environ, start_response)
195 209 except Exception:
196 210 log.error(traceback.format_exc())
197 211 return HTTPInternalServerError()(environ, start_response)
198 212
199 213 def __make_app(self, repo_name, repo_path):
200 214 """
201 215 Make an wsgi application using dulserver
202 216
203 217 :param repo_name: name of the repository
204 218 :param repo_path: full path to the repository
205 219 """
206 220 _d = {'/' + repo_name: Repo(repo_path)}
207 221 backend = dulserver.DictBackend(_d)
208 222 gitserve = make_wsgi_chain(backend)
209 223
210 224 return gitserve
211 225
212 226 def __get_repository(self, environ):
213 227 """
214 228 Get's repository name out of PATH_INFO header
215 229
216 230 :param environ: environ where PATH_INFO is stored
217 231 """
218 232 try:
219 233 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
220 234 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
221 235 except:
222 236 log.error(traceback.format_exc())
223 237 raise
224 238
225 239 return repo_name
226 240
227 241 def __get_user(self, username):
228 242 return User.get_by_username(username)
229 243
230 244 def __get_action(self, environ):
231 245 """
232 246 Maps git request commands into a pull or push command.
233 247
234 248 :param environ:
235 249 """
236 250 service = environ['QUERY_STRING'].split('=')
237 251
238 252 if len(service) > 1:
239 253 service_cmd = service[1]
240 254 mapping = {
241 255 'git-receive-pack': 'push',
242 256 'git-upload-pack': 'pull',
243 257 }
244 258 op = mapping[service_cmd]
245 259 self._git_stored_op = op
246 260 return op
247 261 else:
248 262 # try to fallback to stored variable as we don't know if the last
249 263 # operation is pull/push
250 264 op = getattr(self, '_git_stored_op', 'pull')
251 265 return op
266
267 def _handle_githooks(self, action, baseui, environ):
268
269 from rhodecode.lib.hooks import log_pull_action, log_push_action
270 service = environ['QUERY_STRING'].split('=')
271 if len(service) < 2:
272 return
273
274 class cont(object):
275 pass
276
277 repo = cont()
278 setattr(repo, 'ui', baseui)
279
280 push_hook = 'pretxnchangegroup.push_logger'
281 pull_hook = 'preoutgoing.pull_logger'
282 _hooks = dict(baseui.configitems('hooks')) or {}
283 if action == 'push' and _hooks.get(push_hook):
284 log_push_action(ui=baseui, repo=repo)
285 elif action == 'pull' and _hooks.get(pull_hook):
286 log_pull_action(ui=baseui, repo=repo)
287
@@ -1,256 +1,257 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30 import urllib
31 31
32 32 from mercurial.error import RepoError
33 33 from mercurial.hgweb import hgweb_mod
34 34
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 37 from rhodecode.lib.utils2 import safe_str
38 38 from rhodecode.lib.base import BaseVCSController
39 39 from rhodecode.lib.auth import get_container_username
40 40 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
41 41 from rhodecode.model.db import User
42 42
43 43 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 def is_mercurial(environ):
49 49 """
50 50 Returns True if request's target is mercurial server - header
51 51 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
52 52 """
53 53 http_accept = environ.get('HTTP_ACCEPT')
54 54 path_info = environ['PATH_INFO']
55 55 if http_accept and http_accept.startswith('application/mercurial'):
56 56 ishg_path = True
57 57 else:
58 58 ishg_path = False
59 59
60 60 log.debug('pathinfo: %s detected as HG %s' % (
61 61 path_info, ishg_path)
62 62 )
63 63 return ishg_path
64 64
65 65
66 66 class SimpleHg(BaseVCSController):
67 67
68 68 def _handle_request(self, environ, start_response):
69 69 if not is_mercurial(environ):
70 70 return self.application(environ, start_response)
71 71
72 72 ipaddr = self._get_ip_addr(environ)
73 73
74 74 # skip passing error to error controller
75 75 environ['pylons.status_code_redirect'] = True
76 76
77 77 #======================================================================
78 78 # EXTRACT REPOSITORY NAME FROM ENV
79 79 #======================================================================
80 80 try:
81 81 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
82 82 log.debug('Extracted repo name is %s' % repo_name)
83 83 except:
84 84 return HTTPInternalServerError()(environ, start_response)
85 85
86 86 # quick check if that dir exists...
87 87 if is_valid_repo(repo_name, self.basepath) is False:
88 88 return HTTPNotFound()(environ, start_response)
89 89
90 90 #======================================================================
91 91 # GET ACTION PULL or PUSH
92 92 #======================================================================
93 93 action = self.__get_action(environ)
94 94
95 95 #======================================================================
96 96 # CHECK ANONYMOUS PERMISSION
97 97 #======================================================================
98 98 if action in ['pull', 'push']:
99 99 anonymous_user = self.__get_user('default')
100 100 username = anonymous_user.username
101 101 anonymous_perm = self._check_permission(action, anonymous_user,
102 102 repo_name)
103 103
104 104 if anonymous_perm is not True or anonymous_user.active is False:
105 105 if anonymous_perm is not True:
106 106 log.debug('Not enough credentials to access this '
107 107 'repository as anonymous user')
108 108 if anonymous_user.active is False:
109 109 log.debug('Anonymous access is disabled, running '
110 110 'authentication')
111 111 #==============================================================
112 112 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
113 113 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
114 114 #==============================================================
115 115
116 116 # Attempting to retrieve username from the container
117 117 username = get_container_username(environ, self.config)
118 118
119 119 # If not authenticated by the container, running basic auth
120 120 if not username:
121 121 self.authenticate.realm = \
122 122 safe_str(self.config['rhodecode_realm'])
123 123 result = self.authenticate(environ)
124 124 if isinstance(result, str):
125 125 AUTH_TYPE.update(environ, 'basic')
126 126 REMOTE_USER.update(environ, result)
127 127 username = result
128 128 else:
129 129 return result.wsgi_application(environ, start_response)
130 130
131 131 #==============================================================
132 132 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
133 133 #==============================================================
134 134 if action in ['pull', 'push']:
135 135 try:
136 136 user = self.__get_user(username)
137 137 if user is None or not user.active:
138 138 return HTTPForbidden()(environ, start_response)
139 139 username = user.username
140 140 except:
141 141 log.error(traceback.format_exc())
142 142 return HTTPInternalServerError()(environ,
143 143 start_response)
144 144
145 145 #check permissions for this repository
146 146 perm = self._check_permission(action, user, repo_name)
147 147 if perm is not True:
148 148 return HTTPForbidden()(environ, start_response)
149 149
150 150 # extras are injected into mercurial UI object and later available
151 151 # in hg hooks executed by rhodecode
152 152 extras = {
153 153 'ip': ipaddr,
154 154 'username': username,
155 155 'action': action,
156 'repository': repo_name
156 'repository': repo_name,
157 'scm': 'hg',
157 158 }
158 159
159 160 #======================================================================
160 161 # MERCURIAL REQUEST HANDLING
161 162 #======================================================================
162 163 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
163 164 log.debug('Repository path is %s' % repo_path)
164 165
165 166 baseui = make_ui('db')
166 167 self.__inject_extras(repo_path, baseui, extras)
167 168
168 169 try:
169 170 # invalidate cache on push
170 171 if action == 'push':
171 172 self._invalidate_cache(repo_name)
172 173 log.info('%s action on HG repo "%s"' % (action, repo_name))
173 174 app = self.__make_app(repo_path, baseui, extras)
174 175 return app(environ, start_response)
175 176 except RepoError, e:
176 177 if str(e).find('not found') != -1:
177 178 return HTTPNotFound()(environ, start_response)
178 179 except Exception:
179 180 log.error(traceback.format_exc())
180 181 return HTTPInternalServerError()(environ, start_response)
181 182
182 183 def __make_app(self, repo_name, baseui, extras):
183 184 """
184 185 Make an wsgi application using hgweb, and inject generated baseui
185 186 instance, additionally inject some extras into ui object
186 187 """
187 188 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
188 189
189 190 def __get_repository(self, environ):
190 191 """
191 192 Get's repository name out of PATH_INFO header
192 193
193 194 :param environ: environ where PATH_INFO is stored
194 195 """
195 196 try:
196 197 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
197 198 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
198 199 if repo_name.endswith('/'):
199 200 repo_name = repo_name.rstrip('/')
200 201 except:
201 202 log.error(traceback.format_exc())
202 203 raise
203 204
204 205 return repo_name
205 206
206 207 def __get_user(self, username):
207 208 return User.get_by_username(username)
208 209
209 210 def __get_action(self, environ):
210 211 """
211 212 Maps mercurial request commands into a clone,pull or push command.
212 213 This should always return a valid command string
213 214
214 215 :param environ:
215 216 """
216 217 mapping = {'changegroup': 'pull',
217 218 'changegroupsubset': 'pull',
218 219 'stream_out': 'pull',
219 220 'listkeys': 'pull',
220 221 'unbundle': 'push',
221 222 'pushkey': 'push', }
222 223 for qry in environ['QUERY_STRING'].split('&'):
223 224 if qry.startswith('cmd'):
224 225 cmd = qry.split('=')[-1]
225 226 if cmd in mapping:
226 227 return mapping[cmd]
227 228 else:
228 229 return 'pull'
229 230
230 231 def __inject_extras(self, repo_path, baseui, extras={}):
231 232 """
232 233 Injects some extra params into baseui instance
233 234
234 235 also overwrites global settings with those takes from local hgrc file
235 236
236 237 :param baseui: baseui instance
237 238 :param extras: dict with extra params to put into baseui
238 239 """
239 240
240 241 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
241 242
242 243 # make our hgweb quiet so it doesn't print output
243 244 baseui.setconfig('ui', 'quiet', 'true')
244 245
245 246 #inject some additional parameters that will be available in ui
246 247 #for hooks
247 248 for k, v in extras.items():
248 249 baseui.setconfig('rhodecode_extras', k, v)
249 250
250 251 repoui = make_ui('file', hgrc, False)
251 252
252 253 if repoui:
253 254 #overwrite our ui instance with the section from hgrc file
254 255 for section in ui_sections:
255 256 for k, v in repoui.configitems(section):
256 257 baseui.setconfig(section, k, v)
General Comments 0
You need to be logged in to leave comments. Login now