##// END OF EJS Templates
Add option to define custom lexers for custom extensions for code highlight in rcextension module
marcink -
r3375:7000fc4a beta
parent child Browse files
Show More
@@ -1,118 +1,126 b''
1 1 # Additional mappings that are not present in the pygments lexers
2 2 # used for building stats
3 3 # format is {'ext':['Names']} eg. {'py':['Python']} note: there can be
4 4 # more than one name for extension
5 5 # NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP
6 6 # build by pygments
7 7 EXTRA_MAPPINGS = {}
8 8
9 # additional lexer definitions for custom files
10 # it's overrides pygments lexers, and uses defined name of lexer to colorize the
11 # files. Format is {'ext': 'lexer_name'}
12 # List of lexers can be printed running:
13 # python -c "import pprint;from pygments import lexers;pprint.pprint([(x[0], x[1]) for x in lexers.get_all_lexers()]);"
14
15 EXTRA_LEXERS = {}
16
9 17 #==============================================================================
10 18 # WHOOSH INDEX EXTENSIONS
11 19 #==============================================================================
12 20 # if INDEX_EXTENSIONS is [] it'll use pygments lexers extensions by default.
13 21 # To set your own just add to this list extensions to index with content
14 22 INDEX_EXTENSIONS = []
15 23
16 24 # additional extensions for indexing besides the default from pygments
17 25 # those get's added to INDEX_EXTENSIONS
18 26 EXTRA_INDEX_EXTENSIONS = []
19 27
20 28
21 29 #==============================================================================
22 30 # POST CREATE REPOSITORY HOOK
23 31 #==============================================================================
24 32 # this function will be executed after each repository is created
25 33 def _crhook(*args, **kwargs):
26 34 """
27 35 Post create repository HOOK
28 36 kwargs available:
29 37 :param repo_name:
30 38 :param repo_type:
31 39 :param description:
32 40 :param private:
33 41 :param created_on:
34 42 :param enable_downloads:
35 43 :param repo_id:
36 44 :param user_id:
37 45 :param enable_statistics:
38 46 :param clone_uri:
39 47 :param fork_id:
40 48 :param group_id:
41 49 :param created_by:
42 50 """
43 51 return 0
44 52 CREATE_REPO_HOOK = _crhook
45 53
46 54
47 55 #==============================================================================
48 56 # POST DELETE REPOSITORY HOOK
49 57 #==============================================================================
50 58 # this function will be executed after each repository deletion
51 59 def _dlhook(*args, **kwargs):
52 60 """
53 61 Post create repository HOOK
54 62 kwargs available:
55 63 :param repo_name:
56 64 :param repo_type:
57 65 :param description:
58 66 :param private:
59 67 :param created_on:
60 68 :param enable_downloads:
61 69 :param repo_id:
62 70 :param user_id:
63 71 :param enable_statistics:
64 72 :param clone_uri:
65 73 :param fork_id:
66 74 :param group_id:
67 75 :param deleted_by:
68 76 :param deleted_on:
69 77 """
70 78 return 0
71 79 DELETE_REPO_HOOK = _dlhook
72 80
73 81
74 82 #==============================================================================
75 83 # POST PUSH HOOK
76 84 #==============================================================================
77 85
78 86 # this function will be executed after each push it's executed after the
79 87 # build-in hook that RhodeCode uses for logging pushes
80 88 def _pushhook(*args, **kwargs):
81 89 """
82 90 Post push hook
83 91 kwargs available:
84 92
85 93 :param server_url: url of instance that triggered this hook
86 94 :param config: path to .ini config used
87 95 :param scm: type of VS 'git' or 'hg'
88 96 :param username: name of user who pushed
89 97 :param ip: ip of who pushed
90 98 :param action: push
91 99 :param repository: repository name
92 100 :param pushed_revs: list of pushed revisions
93 101 """
94 102 return 0
95 103 PUSH_HOOK = _pushhook
96 104
97 105
98 106 #==============================================================================
99 107 # POST PULL HOOK
100 108 #==============================================================================
101 109
102 110 # this function will be executed after each push it's executed after the
103 111 # build-in hook that RhodeCode uses for logging pulls
104 112 def _pullhook(*args, **kwargs):
105 113 """
106 114 Post pull hook
107 115 kwargs available::
108 116
109 117 :param server_url: url of instance that triggered this hook
110 118 :param config: path to .ini config used
111 119 :param scm: type of VS 'git' or 'hg'
112 120 :param username: name of user who pulled
113 121 :param ip: ip of who pulled
114 122 :param action: pull
115 123 :param repository: repository name
116 124 """
117 125 return 0
118 126 PULL_HOOK = _pullhook
@@ -1,191 +1,191 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.annotate
4 4 ~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Anontation library for usage in rhodecode, previously part of vcs
7 7
8 8 :created_on: Dec 4, 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 import StringIO
13 14
14 15 from rhodecode.lib.vcs.exceptions import VCSError
15 16 from rhodecode.lib.vcs.nodes import FileNode
16 17 from pygments.formatters import HtmlFormatter
17 18 from pygments import highlight
18 19
19 import StringIO
20
21 20
22 21 def annotate_highlight(filenode, annotate_from_changeset_func=None,
23 22 order=None, headers=None, **options):
24 23 """
25 24 Returns html portion containing annotated table with 3 columns: line
26 25 numbers, changeset information and pygmentized line of code.
27 26
28 27 :param filenode: FileNode object
29 28 :param annotate_from_changeset_func: function taking changeset and
30 29 returning single annotate cell; needs break line at the end
31 30 :param order: ordered sequence of ``ls`` (line numbers column),
32 31 ``annotate`` (annotate column), ``code`` (code column); Default is
33 32 ``['ls', 'annotate', 'code']``
34 33 :param headers: dictionary with headers (keys are whats in ``order``
35 34 parameter)
36 35 """
36 from rhodecode.lib.utils import get_custom_lexer
37 37 options['linenos'] = True
38 38 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
39 39 headers=headers,
40 40 annotate_from_changeset_func=annotate_from_changeset_func, **options)
41 lexer = filenode.lexer
41 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
42 42 highlighted = highlight(filenode.content, lexer, formatter)
43 43 return highlighted
44 44
45 45
46 46 class AnnotateHtmlFormatter(HtmlFormatter):
47 47
48 48 def __init__(self, filenode, annotate_from_changeset_func=None,
49 49 order=None, **options):
50 50 """
51 51 If ``annotate_from_changeset_func`` is passed it should be a function
52 52 which returns string from the given changeset. For example, we may pass
53 53 following function as ``annotate_from_changeset_func``::
54 54
55 55 def changeset_to_anchor(changeset):
56 56 return '<a href="/changesets/%s/">%s</a>\n' %\
57 57 (changeset.id, changeset.id)
58 58
59 59 :param annotate_from_changeset_func: see above
60 60 :param order: (default: ``['ls', 'annotate', 'code']``); order of
61 61 columns;
62 62 :param options: standard pygment's HtmlFormatter options, there is
63 63 extra option tough, ``headers``. For instance we can pass::
64 64
65 65 formatter = AnnotateHtmlFormatter(filenode, headers={
66 66 'ls': '#',
67 67 'annotate': 'Annotate',
68 68 'code': 'Code',
69 69 })
70 70
71 71 """
72 72 super(AnnotateHtmlFormatter, self).__init__(**options)
73 73 self.annotate_from_changeset_func = annotate_from_changeset_func
74 74 self.order = order or ('ls', 'annotate', 'code')
75 75 headers = options.pop('headers', None)
76 76 if headers and not ('ls' in headers and 'annotate' in headers and
77 77 'code' in headers):
78 78 raise ValueError("If headers option dict is specified it must "
79 79 "all 'ls', 'annotate' and 'code' keys")
80 80 self.headers = headers
81 81 if isinstance(filenode, FileNode):
82 82 self.filenode = filenode
83 83 else:
84 84 raise VCSError("This formatter expect FileNode parameter, not %r"
85 85 % type(filenode))
86 86
87 87 def annotate_from_changeset(self, changeset):
88 88 """
89 89 Returns full html line for single changeset per annotated line.
90 90 """
91 91 if self.annotate_from_changeset_func:
92 92 return self.annotate_from_changeset_func(changeset)
93 93 else:
94 94 return ''.join((changeset.id, '\n'))
95 95
96 96 def _wrap_tablelinenos(self, inner):
97 97 dummyoutfile = StringIO.StringIO()
98 98 lncount = 0
99 99 for t, line in inner:
100 100 if t:
101 101 lncount += 1
102 102 dummyoutfile.write(line)
103 103
104 104 fl = self.linenostart
105 105 mw = len(str(lncount + fl - 1))
106 106 sp = self.linenospecial
107 107 st = self.linenostep
108 108 la = self.lineanchors
109 109 aln = self.anchorlinenos
110 110 if sp:
111 111 lines = []
112 112
113 113 for i in range(fl, fl + lncount):
114 114 if i % st == 0:
115 115 if i % sp == 0:
116 116 if aln:
117 117 lines.append('<a href="#%s-%d" class="special">'
118 118 '%*d</a>' %
119 119 (la, i, mw, i))
120 120 else:
121 121 lines.append('<span class="special">'
122 122 '%*d</span>' % (mw, i))
123 123 else:
124 124 if aln:
125 125 lines.append('<a href="#%s-%d">'
126 126 '%*d</a>' % (la, i, mw, i))
127 127 else:
128 128 lines.append('%*d' % (mw, i))
129 129 else:
130 130 lines.append('')
131 131 ls = '\n'.join(lines)
132 132 else:
133 133 lines = []
134 134 for i in range(fl, fl + lncount):
135 135 if i % st == 0:
136 136 if aln:
137 137 lines.append('<a href="#%s-%d">%*d</a>' \
138 138 % (la, i, mw, i))
139 139 else:
140 140 lines.append('%*d' % (mw, i))
141 141 else:
142 142 lines.append('')
143 143 ls = '\n'.join(lines)
144 144
145 145 # annotate_changesets = [tup[1] for tup in self.filenode.annotate]
146 146 ## TODO: not sure what that fixes
147 147 # # If pygments cropped last lines break we need do that too
148 148 # ln_cs = len(annotate_changesets)
149 149 # ln_ = len(ls.splitlines())
150 150 # if ln_cs > ln_:
151 151 # annotate_changesets = annotate_changesets[:ln_ - ln_cs]
152 152 annotate = ''.join((self.annotate_from_changeset(el[2]())
153 153 for el in self.filenode.annotate))
154 154 # in case you wonder about the seemingly redundant <div> here:
155 155 # since the content in the other cell also is wrapped in a div,
156 156 # some browsers in some configurations seem to mess up the formatting.
157 157 '''
158 158 yield 0, ('<table class="%stable">' % self.cssclass +
159 159 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
160 160 ls + '</pre></div></td>' +
161 161 '<td class="code">')
162 162 yield 0, dummyoutfile.getvalue()
163 163 yield 0, '</td></tr></table>'
164 164
165 165 '''
166 166 headers_row = []
167 167 if self.headers:
168 168 headers_row = ['<tr class="annotate-header">']
169 169 for key in self.order:
170 170 td = ''.join(('<td>', self.headers[key], '</td>'))
171 171 headers_row.append(td)
172 172 headers_row.append('</tr>')
173 173
174 174 body_row_start = ['<tr>']
175 175 for key in self.order:
176 176 if key == 'ls':
177 177 body_row_start.append(
178 178 '<td class="linenos"><div class="linenodiv"><pre>' +
179 179 ls + '</pre></div></td>')
180 180 elif key == 'annotate':
181 181 body_row_start.append(
182 182 '<td class="annotate"><div class="annotatediv"><pre>' +
183 183 annotate + '</pre></div></td>')
184 184 elif key == 'code':
185 185 body_row_start.append('<td class="code">')
186 186 yield 0, ('<table class="%stable">' % self.cssclass +
187 187 ''.join(headers_row) +
188 188 ''.join(body_row_start)
189 189 )
190 190 yield 0, dummyoutfile.getvalue()
191 191 yield 0, '</td></tr></table>'
@@ -1,1173 +1,1174 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 import re
13 13 import urlparse
14 14 import textwrap
15 15
16 16 from datetime import datetime
17 17 from pygments.formatters.html import HtmlFormatter
18 18 from pygments import highlight as code_highlight
19 19 from pylons import url, request, config
20 20 from pylons.i18n.translation import _, ungettext
21 21 from hashlib import md5
22 22
23 23 from webhelpers.html import literal, HTML, escape
24 24 from webhelpers.html.tools import *
25 25 from webhelpers.html.builder import make_tag
26 26 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
27 27 end_form, file, form, hidden, image, javascript_link, link_to, \
28 28 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
29 29 submit, text, password, textarea, title, ul, xml_declaration, radio
30 30 from webhelpers.html.tools import auto_link, button_to, highlight, \
31 31 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
32 32 from webhelpers.number import format_byte_size, format_bit_size
33 33 from webhelpers.pylonslib import Flash as _Flash
34 34 from webhelpers.pylonslib.secure_form import secure_form
35 35 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
36 36 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
37 37 replace_whitespace, urlify, truncate, wrap_paragraphs
38 38 from webhelpers.date import time_ago_in_words
39 39 from webhelpers.paginate import Page
40 40 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
41 41 convert_boolean_attrs, NotGiven, _make_safe_id_component
42 42
43 43 from rhodecode.lib.annotate import annotate_highlight
44 from rhodecode.lib.utils import repo_name_slug
44 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
45 45 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
46 46 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict
47 47 from rhodecode.lib.markup_renderer import MarkupRenderer
48 48 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
49 49 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
50 50 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
51 51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 52 from rhodecode.model.db import URL_SEP, Permission
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 html_escape_table = {
58 58 "&": "&amp;",
59 59 '"': "&quot;",
60 60 "'": "&apos;",
61 61 ">": "&gt;",
62 62 "<": "&lt;",
63 63 }
64 64
65 65
66 66 def html_escape(text):
67 67 """Produce entities within text."""
68 68 return "".join(html_escape_table.get(c, c) for c in text)
69 69
70 70
71 71 def shorter(text, size=20):
72 72 postfix = '...'
73 73 if len(text) > size:
74 74 return text[:size - len(postfix)] + postfix
75 75 return text
76 76
77 77
78 78 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
79 79 """
80 80 Reset button
81 81 """
82 82 _set_input_attrs(attrs, type, name, value)
83 83 _set_id_attr(attrs, id, name)
84 84 convert_boolean_attrs(attrs, ["disabled"])
85 85 return HTML.input(**attrs)
86 86
87 87 reset = _reset
88 88 safeid = _make_safe_id_component
89 89
90 90
91 91 def FID(raw_id, path):
92 92 """
93 93 Creates a uniqe ID for filenode based on it's hash of path and revision
94 94 it's safe to use in urls
95 95
96 96 :param raw_id:
97 97 :param path:
98 98 """
99 99
100 100 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
101 101
102 102
103 103 def get_token():
104 104 """Return the current authentication token, creating one if one doesn't
105 105 already exist.
106 106 """
107 107 token_key = "_authentication_token"
108 108 from pylons import session
109 109 if not token_key in session:
110 110 try:
111 111 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
112 112 except AttributeError: # Python < 2.4
113 113 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
114 114 session[token_key] = token
115 115 if hasattr(session, 'save'):
116 116 session.save()
117 117 return session[token_key]
118 118
119 119
120 120 class _GetError(object):
121 121 """Get error from form_errors, and represent it as span wrapped error
122 122 message
123 123
124 124 :param field_name: field to fetch errors for
125 125 :param form_errors: form errors dict
126 126 """
127 127
128 128 def __call__(self, field_name, form_errors):
129 129 tmpl = """<span class="error_msg">%s</span>"""
130 130 if form_errors and field_name in form_errors:
131 131 return literal(tmpl % form_errors.get(field_name))
132 132
133 133 get_error = _GetError()
134 134
135 135
136 136 class _ToolTip(object):
137 137
138 138 def __call__(self, tooltip_title, trim_at=50):
139 139 """
140 140 Special function just to wrap our text into nice formatted
141 141 autowrapped text
142 142
143 143 :param tooltip_title:
144 144 """
145 145 tooltip_title = escape(tooltip_title)
146 146 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
147 147 return tooltip_title
148 148 tooltip = _ToolTip()
149 149
150 150
151 151 class _FilesBreadCrumbs(object):
152 152
153 153 def __call__(self, repo_name, rev, paths):
154 154 if isinstance(paths, str):
155 155 paths = safe_unicode(paths)
156 156 url_l = [link_to(repo_name, url('files_home',
157 157 repo_name=repo_name,
158 158 revision=rev, f_path=''),
159 159 class_='ypjax-link')]
160 160 paths_l = paths.split('/')
161 161 for cnt, p in enumerate(paths_l):
162 162 if p != '':
163 163 url_l.append(link_to(p,
164 164 url('files_home',
165 165 repo_name=repo_name,
166 166 revision=rev,
167 167 f_path='/'.join(paths_l[:cnt + 1])
168 168 ),
169 169 class_='ypjax-link'
170 170 )
171 171 )
172 172
173 173 return literal('/'.join(url_l))
174 174
175 175 files_breadcrumbs = _FilesBreadCrumbs()
176 176
177 177
178 178 class CodeHtmlFormatter(HtmlFormatter):
179 179 """
180 180 My code Html Formatter for source codes
181 181 """
182 182
183 183 def wrap(self, source, outfile):
184 184 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
185 185
186 186 def _wrap_code(self, source):
187 187 for cnt, it in enumerate(source):
188 188 i, t = it
189 189 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
190 190 yield i, t
191 191
192 192 def _wrap_tablelinenos(self, inner):
193 193 dummyoutfile = StringIO.StringIO()
194 194 lncount = 0
195 195 for t, line in inner:
196 196 if t:
197 197 lncount += 1
198 198 dummyoutfile.write(line)
199 199
200 200 fl = self.linenostart
201 201 mw = len(str(lncount + fl - 1))
202 202 sp = self.linenospecial
203 203 st = self.linenostep
204 204 la = self.lineanchors
205 205 aln = self.anchorlinenos
206 206 nocls = self.noclasses
207 207 if sp:
208 208 lines = []
209 209
210 210 for i in range(fl, fl + lncount):
211 211 if i % st == 0:
212 212 if i % sp == 0:
213 213 if aln:
214 214 lines.append('<a href="#%s%d" class="special">%*d</a>' %
215 215 (la, i, mw, i))
216 216 else:
217 217 lines.append('<span class="special">%*d</span>' % (mw, i))
218 218 else:
219 219 if aln:
220 220 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
221 221 else:
222 222 lines.append('%*d' % (mw, i))
223 223 else:
224 224 lines.append('')
225 225 ls = '\n'.join(lines)
226 226 else:
227 227 lines = []
228 228 for i in range(fl, fl + lncount):
229 229 if i % st == 0:
230 230 if aln:
231 231 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
232 232 else:
233 233 lines.append('%*d' % (mw, i))
234 234 else:
235 235 lines.append('')
236 236 ls = '\n'.join(lines)
237 237
238 238 # in case you wonder about the seemingly redundant <div> here: since the
239 239 # content in the other cell also is wrapped in a div, some browsers in
240 240 # some configurations seem to mess up the formatting...
241 241 if nocls:
242 242 yield 0, ('<table class="%stable">' % self.cssclass +
243 243 '<tr><td><div class="linenodiv" '
244 244 'style="background-color: #f0f0f0; padding-right: 10px">'
245 245 '<pre style="line-height: 125%">' +
246 246 ls + '</pre></div></td><td id="hlcode" class="code">')
247 247 else:
248 248 yield 0, ('<table class="%stable">' % self.cssclass +
249 249 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
250 250 ls + '</pre></div></td><td id="hlcode" class="code">')
251 251 yield 0, dummyoutfile.getvalue()
252 252 yield 0, '</td></tr></table>'
253 253
254 254
255 255 def pygmentize(filenode, **kwargs):
256 """pygmentize function using pygments
256 """
257 pygmentize function using pygments
257 258
258 259 :param filenode:
259 260 """
260
261 return literal(code_highlight(filenode.content,
262 filenode.lexer, CodeHtmlFormatter(**kwargs)))
261 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
262 return literal(code_highlight(filenode.content, lexer,
263 CodeHtmlFormatter(**kwargs)))
263 264
264 265
265 266 def pygmentize_annotation(repo_name, filenode, **kwargs):
266 267 """
267 268 pygmentize function for annotation
268 269
269 270 :param filenode:
270 271 """
271 272
272 273 color_dict = {}
273 274
274 275 def gen_color(n=10000):
275 276 """generator for getting n of evenly distributed colors using
276 277 hsv color and golden ratio. It always return same order of colors
277 278
278 279 :returns: RGB tuple
279 280 """
280 281
281 282 def hsv_to_rgb(h, s, v):
282 283 if s == 0.0:
283 284 return v, v, v
284 285 i = int(h * 6.0) # XXX assume int() truncates!
285 286 f = (h * 6.0) - i
286 287 p = v * (1.0 - s)
287 288 q = v * (1.0 - s * f)
288 289 t = v * (1.0 - s * (1.0 - f))
289 290 i = i % 6
290 291 if i == 0:
291 292 return v, t, p
292 293 if i == 1:
293 294 return q, v, p
294 295 if i == 2:
295 296 return p, v, t
296 297 if i == 3:
297 298 return p, q, v
298 299 if i == 4:
299 300 return t, p, v
300 301 if i == 5:
301 302 return v, p, q
302 303
303 304 golden_ratio = 0.618033988749895
304 305 h = 0.22717784590367374
305 306
306 307 for _ in xrange(n):
307 308 h += golden_ratio
308 309 h %= 1
309 310 HSV_tuple = [h, 0.95, 0.95]
310 311 RGB_tuple = hsv_to_rgb(*HSV_tuple)
311 312 yield map(lambda x: str(int(x * 256)), RGB_tuple)
312 313
313 314 cgenerator = gen_color()
314 315
315 316 def get_color_string(cs):
316 317 if cs in color_dict:
317 318 col = color_dict[cs]
318 319 else:
319 320 col = color_dict[cs] = cgenerator.next()
320 321 return "color: rgb(%s)! important;" % (', '.join(col))
321 322
322 323 def url_func(repo_name):
323 324
324 325 def _url_func(changeset):
325 326 author = changeset.author
326 327 date = changeset.date
327 328 message = tooltip(changeset.message)
328 329
329 330 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
330 331 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
331 332 "</b> %s<br/></div>")
332 333
333 334 tooltip_html = tooltip_html % (author, date, message)
334 335 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
335 336 short_id(changeset.raw_id))
336 337 uri = link_to(
337 338 lnk_format,
338 339 url('changeset_home', repo_name=repo_name,
339 340 revision=changeset.raw_id),
340 341 style=get_color_string(changeset.raw_id),
341 342 class_='tooltip',
342 343 title=tooltip_html
343 344 )
344 345
345 346 uri += '\n'
346 347 return uri
347 348 return _url_func
348 349
349 350 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
350 351
351 352
352 353 def is_following_repo(repo_name, user_id):
353 354 from rhodecode.model.scm import ScmModel
354 355 return ScmModel().is_following_repo(repo_name, user_id)
355 356
356 357 flash = _Flash()
357 358
358 359 #==============================================================================
359 360 # SCM FILTERS available via h.
360 361 #==============================================================================
361 362 from rhodecode.lib.vcs.utils import author_name, author_email
362 363 from rhodecode.lib.utils2 import credentials_filter, age as _age
363 364 from rhodecode.model.db import User, ChangesetStatus
364 365
365 366 age = lambda x: _age(x)
366 367 capitalize = lambda x: x.capitalize()
367 368 email = author_email
368 369 short_id = lambda x: x[:12]
369 370 hide_credentials = lambda x: ''.join(credentials_filter(x))
370 371
371 372
372 373 def fmt_date(date):
373 374 if date:
374 375 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
375 376 return date.strftime(_fmt).decode('utf8')
376 377
377 378 return ""
378 379
379 380
380 381 def is_git(repository):
381 382 if hasattr(repository, 'alias'):
382 383 _type = repository.alias
383 384 elif hasattr(repository, 'repo_type'):
384 385 _type = repository.repo_type
385 386 else:
386 387 _type = repository
387 388 return _type == 'git'
388 389
389 390
390 391 def is_hg(repository):
391 392 if hasattr(repository, 'alias'):
392 393 _type = repository.alias
393 394 elif hasattr(repository, 'repo_type'):
394 395 _type = repository.repo_type
395 396 else:
396 397 _type = repository
397 398 return _type == 'hg'
398 399
399 400
400 401 def email_or_none(author):
401 402 # extract email from the commit string
402 403 _email = email(author)
403 404 if _email != '':
404 405 # check it against RhodeCode database, and use the MAIN email for this
405 406 # user
406 407 user = User.get_by_email(_email, case_insensitive=True, cache=True)
407 408 if user is not None:
408 409 return user.email
409 410 return _email
410 411
411 412 # See if it contains a username we can get an email from
412 413 user = User.get_by_username(author_name(author), case_insensitive=True,
413 414 cache=True)
414 415 if user is not None:
415 416 return user.email
416 417
417 418 # No valid email, not a valid user in the system, none!
418 419 return None
419 420
420 421
421 422 def person(author, show_attr="username_and_name"):
422 423 # attr to return from fetched user
423 424 person_getter = lambda usr: getattr(usr, show_attr)
424 425
425 426 # Valid email in the attribute passed, see if they're in the system
426 427 _email = email(author)
427 428 if _email != '':
428 429 user = User.get_by_email(_email, case_insensitive=True, cache=True)
429 430 if user is not None:
430 431 return person_getter(user)
431 432 return _email
432 433
433 434 # Maybe it's a username?
434 435 _author = author_name(author)
435 436 user = User.get_by_username(_author, case_insensitive=True,
436 437 cache=True)
437 438 if user is not None:
438 439 return person_getter(user)
439 440
440 441 # Still nothing? Just pass back the author name then
441 442 return _author
442 443
443 444
444 445 def person_by_id(id_, show_attr="username_and_name"):
445 446 # attr to return from fetched user
446 447 person_getter = lambda usr: getattr(usr, show_attr)
447 448
448 449 #maybe it's an ID ?
449 450 if str(id_).isdigit() or isinstance(id_, int):
450 451 id_ = int(id_)
451 452 user = User.get(id_)
452 453 if user is not None:
453 454 return person_getter(user)
454 455 return id_
455 456
456 457
457 458 def desc_stylize(value):
458 459 """
459 460 converts tags from value into html equivalent
460 461
461 462 :param value:
462 463 """
463 464 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
464 465 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
465 466 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
466 467 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
467 468 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
468 469 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
469 470 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
470 471 '<div class="metatag" tag="lang">\\2</div>', value)
471 472 value = re.sub(r'\[([a-z]+)\]',
472 473 '<div class="metatag" tag="\\1">\\1</div>', value)
473 474
474 475 return value
475 476
476 477
477 478 def bool2icon(value):
478 479 """Returns True/False values represented as small html image of true/false
479 480 icons
480 481
481 482 :param value: bool value
482 483 """
483 484
484 485 if value is True:
485 486 return HTML.tag('img', src=url("/images/icons/accept.png"),
486 487 alt=_('True'))
487 488
488 489 if value is False:
489 490 return HTML.tag('img', src=url("/images/icons/cancel.png"),
490 491 alt=_('False'))
491 492
492 493 return value
493 494
494 495
495 496 def action_parser(user_log, feed=False, parse_cs=False):
496 497 """
497 498 This helper will action_map the specified string action into translated
498 499 fancy names with icons and links
499 500
500 501 :param user_log: user log instance
501 502 :param feed: use output for feeds (no html and fancy icons)
502 503 :param parse_cs: parse Changesets into VCS instances
503 504 """
504 505
505 506 action = user_log.action
506 507 action_params = ' '
507 508
508 509 x = action.split(':')
509 510
510 511 if len(x) > 1:
511 512 action, action_params = x
512 513
513 514 def get_cs_links():
514 515 revs_limit = 3 # display this amount always
515 516 revs_top_limit = 50 # show upto this amount of changesets hidden
516 517 revs_ids = action_params.split(',')
517 518 deleted = user_log.repository is None
518 519 if deleted:
519 520 return ','.join(revs_ids)
520 521
521 522 repo_name = user_log.repository.repo_name
522 523
523 524 def lnk(rev, repo_name):
524 525 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
525 526 lazy_cs = True
526 527 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
527 528 lazy_cs = False
528 529 lbl = '?'
529 530 if rev.op == 'delete_branch':
530 531 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
531 532 title = ''
532 533 elif rev.op == 'tag':
533 534 lbl = '%s' % _('Created tag: %s') % rev.ref_name
534 535 title = ''
535 536 _url = '#'
536 537
537 538 else:
538 539 lbl = '%s' % (rev.short_id[:8])
539 540 _url = url('changeset_home', repo_name=repo_name,
540 541 revision=rev.raw_id)
541 542 title = tooltip(rev.message)
542 543 else:
543 544 ## changeset cannot be found/striped/removed etc.
544 545 lbl = ('%s' % rev)[:12]
545 546 _url = '#'
546 547 title = _('Changeset not found')
547 548 if parse_cs:
548 549 return link_to(lbl, _url, title=title, class_='tooltip')
549 550 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
550 551 class_='lazy-cs' if lazy_cs else '')
551 552
552 553 revs = []
553 554 if len(filter(lambda v: v != '', revs_ids)) > 0:
554 555 repo = None
555 556 for rev in revs_ids[:revs_top_limit]:
556 557 _op = _name = None
557 558 if len(rev.split('=>')) == 2:
558 559 _op, _name = rev.split('=>')
559 560
560 561 # we want parsed changesets, or new log store format is bad
561 562 if parse_cs:
562 563 try:
563 564 if repo is None:
564 565 repo = user_log.repository.scm_instance
565 566 _rev = repo.get_changeset(rev)
566 567 revs.append(_rev)
567 568 except ChangesetDoesNotExistError:
568 569 log.error('cannot find revision %s in this repo' % rev)
569 570 revs.append(rev)
570 571 continue
571 572 else:
572 573 _rev = AttributeDict({
573 574 'short_id': rev[:12],
574 575 'raw_id': rev,
575 576 'message': '',
576 577 'op': _op,
577 578 'ref_name': _name
578 579 })
579 580 revs.append(_rev)
580 581 cs_links = []
581 582 cs_links.append(" " + ', '.join(
582 583 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
583 584 )
584 585 )
585 586
586 587 compare_view = (
587 588 ' <div class="compare_view tooltip" title="%s">'
588 589 '<a href="%s">%s</a> </div>' % (
589 590 _('Show all combined changesets %s->%s') % (
590 591 revs_ids[0][:12], revs_ids[-1][:12]
591 592 ),
592 593 url('changeset_home', repo_name=repo_name,
593 594 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
594 595 ),
595 596 _('compare view')
596 597 )
597 598 )
598 599
599 600 # if we have exactly one more than normally displayed
600 601 # just display it, takes less space than displaying
601 602 # "and 1 more revisions"
602 603 if len(revs_ids) == revs_limit + 1:
603 604 rev = revs[revs_limit]
604 605 cs_links.append(", " + lnk(rev, repo_name))
605 606
606 607 # hidden-by-default ones
607 608 if len(revs_ids) > revs_limit + 1:
608 609 uniq_id = revs_ids[0]
609 610 html_tmpl = (
610 611 '<span> %s <a class="show_more" id="_%s" '
611 612 'href="#more">%s</a> %s</span>'
612 613 )
613 614 if not feed:
614 615 cs_links.append(html_tmpl % (
615 616 _('and'),
616 617 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
617 618 _('revisions')
618 619 )
619 620 )
620 621
621 622 if not feed:
622 623 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
623 624 else:
624 625 html_tmpl = '<span id="%s"> %s </span>'
625 626
626 627 morelinks = ', '.join(
627 628 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
628 629 )
629 630
630 631 if len(revs_ids) > revs_top_limit:
631 632 morelinks += ', ...'
632 633
633 634 cs_links.append(html_tmpl % (uniq_id, morelinks))
634 635 if len(revs) > 1:
635 636 cs_links.append(compare_view)
636 637 return ''.join(cs_links)
637 638
638 639 def get_fork_name():
639 640 repo_name = action_params
640 641 _url = url('summary_home', repo_name=repo_name)
641 642 return _('fork name %s') % link_to(action_params, _url)
642 643
643 644 def get_user_name():
644 645 user_name = action_params
645 646 return user_name
646 647
647 648 def get_users_group():
648 649 group_name = action_params
649 650 return group_name
650 651
651 652 def get_pull_request():
652 653 pull_request_id = action_params
653 654 deleted = user_log.repository is None
654 655 if deleted:
655 656 repo_name = user_log.repository_name
656 657 else:
657 658 repo_name = user_log.repository.repo_name
658 659 return link_to(_('Pull request #%s') % pull_request_id,
659 660 url('pullrequest_show', repo_name=repo_name,
660 661 pull_request_id=pull_request_id))
661 662
662 663 # action : translated str, callback(extractor), icon
663 664 action_map = {
664 665 'user_deleted_repo': (_('[deleted] repository'),
665 666 None, 'database_delete.png'),
666 667 'user_created_repo': (_('[created] repository'),
667 668 None, 'database_add.png'),
668 669 'user_created_fork': (_('[created] repository as fork'),
669 670 None, 'arrow_divide.png'),
670 671 'user_forked_repo': (_('[forked] repository'),
671 672 get_fork_name, 'arrow_divide.png'),
672 673 'user_updated_repo': (_('[updated] repository'),
673 674 None, 'database_edit.png'),
674 675 'admin_deleted_repo': (_('[delete] repository'),
675 676 None, 'database_delete.png'),
676 677 'admin_created_repo': (_('[created] repository'),
677 678 None, 'database_add.png'),
678 679 'admin_forked_repo': (_('[forked] repository'),
679 680 None, 'arrow_divide.png'),
680 681 'admin_updated_repo': (_('[updated] repository'),
681 682 None, 'database_edit.png'),
682 683 'admin_created_user': (_('[created] user'),
683 684 get_user_name, 'user_add.png'),
684 685 'admin_updated_user': (_('[updated] user'),
685 686 get_user_name, 'user_edit.png'),
686 687 'admin_created_users_group': (_('[created] users group'),
687 688 get_users_group, 'group_add.png'),
688 689 'admin_updated_users_group': (_('[updated] users group'),
689 690 get_users_group, 'group_edit.png'),
690 691 'user_commented_revision': (_('[commented] on revision in repository'),
691 692 get_cs_links, 'comment_add.png'),
692 693 'user_commented_pull_request': (_('[commented] on pull request for'),
693 694 get_pull_request, 'comment_add.png'),
694 695 'user_closed_pull_request': (_('[closed] pull request for'),
695 696 get_pull_request, 'tick.png'),
696 697 'push': (_('[pushed] into'),
697 698 get_cs_links, 'script_add.png'),
698 699 'push_local': (_('[committed via RhodeCode] into repository'),
699 700 get_cs_links, 'script_edit.png'),
700 701 'push_remote': (_('[pulled from remote] into repository'),
701 702 get_cs_links, 'connect.png'),
702 703 'pull': (_('[pulled] from'),
703 704 None, 'down_16.png'),
704 705 'started_following_repo': (_('[started following] repository'),
705 706 None, 'heart_add.png'),
706 707 'stopped_following_repo': (_('[stopped following] repository'),
707 708 None, 'heart_delete.png'),
708 709 }
709 710
710 711 action_str = action_map.get(action, action)
711 712 if feed:
712 713 action = action_str[0].replace('[', '').replace(']', '')
713 714 else:
714 715 action = action_str[0]\
715 716 .replace('[', '<span class="journal_highlight">')\
716 717 .replace(']', '</span>')
717 718
718 719 action_params_func = lambda: ""
719 720
720 721 if callable(action_str[1]):
721 722 action_params_func = action_str[1]
722 723
723 724 def action_parser_icon():
724 725 action = user_log.action
725 726 action_params = None
726 727 x = action.split(':')
727 728
728 729 if len(x) > 1:
729 730 action, action_params = x
730 731
731 732 tmpl = """<img src="%s%s" alt="%s"/>"""
732 733 ico = action_map.get(action, ['', '', ''])[2]
733 734 return literal(tmpl % ((url('/images/icons/')), ico, action))
734 735
735 736 # returned callbacks we need to call to get
736 737 return [lambda: literal(action), action_params_func, action_parser_icon]
737 738
738 739
739 740
740 741 #==============================================================================
741 742 # PERMS
742 743 #==============================================================================
743 744 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
744 745 HasRepoPermissionAny, HasRepoPermissionAll, HasReposGroupPermissionAll, \
745 746 HasReposGroupPermissionAny
746 747
747 748
748 749 #==============================================================================
749 750 # GRAVATAR URL
750 751 #==============================================================================
751 752
752 753 def gravatar_url(email_address, size=30):
753 754 from pylons import url # doh, we need to re-import url to mock it later
754 755 _def = 'anonymous@rhodecode.org'
755 756 use_gravatar = str2bool(config['app_conf'].get('use_gravatar'))
756 757 email_address = email_address or _def
757 758 if (not use_gravatar or not email_address or email_address == _def):
758 759 f = lambda a, l: min(l, key=lambda x: abs(x - a))
759 760 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
760 761
761 762 if use_gravatar and config['app_conf'].get('alternative_gravatar_url'):
762 763 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
763 764 parsed_url = urlparse.urlparse(url.current(qualified=True))
764 765 tmpl = tmpl.replace('{email}', email_address)\
765 766 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
766 767 .replace('{netloc}', parsed_url.netloc)\
767 768 .replace('{scheme}', parsed_url.scheme)\
768 769 .replace('{size}', str(size))
769 770 return tmpl
770 771
771 772 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
772 773 default = 'identicon'
773 774 baseurl_nossl = "http://www.gravatar.com/avatar/"
774 775 baseurl_ssl = "https://secure.gravatar.com/avatar/"
775 776 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
776 777
777 778 if isinstance(email_address, unicode):
778 779 #hashlib crashes on unicode items
779 780 email_address = safe_str(email_address)
780 781 # construct the url
781 782 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
782 783 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
783 784
784 785 return gravatar_url
785 786
786 787
787 788 #==============================================================================
788 789 # REPO PAGER, PAGER FOR REPOSITORY
789 790 #==============================================================================
790 791 class RepoPage(Page):
791 792
792 793 def __init__(self, collection, page=1, items_per_page=20,
793 794 item_count=None, url=None, **kwargs):
794 795
795 796 """Create a "RepoPage" instance. special pager for paging
796 797 repository
797 798 """
798 799 self._url_generator = url
799 800
800 801 # Safe the kwargs class-wide so they can be used in the pager() method
801 802 self.kwargs = kwargs
802 803
803 804 # Save a reference to the collection
804 805 self.original_collection = collection
805 806
806 807 self.collection = collection
807 808
808 809 # The self.page is the number of the current page.
809 810 # The first page has the number 1!
810 811 try:
811 812 self.page = int(page) # make it int() if we get it as a string
812 813 except (ValueError, TypeError):
813 814 self.page = 1
814 815
815 816 self.items_per_page = items_per_page
816 817
817 818 # Unless the user tells us how many items the collections has
818 819 # we calculate that ourselves.
819 820 if item_count is not None:
820 821 self.item_count = item_count
821 822 else:
822 823 self.item_count = len(self.collection)
823 824
824 825 # Compute the number of the first and last available page
825 826 if self.item_count > 0:
826 827 self.first_page = 1
827 828 self.page_count = int(math.ceil(float(self.item_count) /
828 829 self.items_per_page))
829 830 self.last_page = self.first_page + self.page_count - 1
830 831
831 832 # Make sure that the requested page number is the range of
832 833 # valid pages
833 834 if self.page > self.last_page:
834 835 self.page = self.last_page
835 836 elif self.page < self.first_page:
836 837 self.page = self.first_page
837 838
838 839 # Note: the number of items on this page can be less than
839 840 # items_per_page if the last page is not full
840 841 self.first_item = max(0, (self.item_count) - (self.page *
841 842 items_per_page))
842 843 self.last_item = ((self.item_count - 1) - items_per_page *
843 844 (self.page - 1))
844 845
845 846 self.items = list(self.collection[self.first_item:self.last_item + 1])
846 847
847 848 # Links to previous and next page
848 849 if self.page > self.first_page:
849 850 self.previous_page = self.page - 1
850 851 else:
851 852 self.previous_page = None
852 853
853 854 if self.page < self.last_page:
854 855 self.next_page = self.page + 1
855 856 else:
856 857 self.next_page = None
857 858
858 859 # No items available
859 860 else:
860 861 self.first_page = None
861 862 self.page_count = 0
862 863 self.last_page = None
863 864 self.first_item = None
864 865 self.last_item = None
865 866 self.previous_page = None
866 867 self.next_page = None
867 868 self.items = []
868 869
869 870 # This is a subclass of the 'list' type. Initialise the list now.
870 871 list.__init__(self, reversed(self.items))
871 872
872 873
873 874 def changed_tooltip(nodes):
874 875 """
875 876 Generates a html string for changed nodes in changeset page.
876 877 It limits the output to 30 entries
877 878
878 879 :param nodes: LazyNodesGenerator
879 880 """
880 881 if nodes:
881 882 pref = ': <br/> '
882 883 suf = ''
883 884 if len(nodes) > 30:
884 885 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
885 886 return literal(pref + '<br/> '.join([safe_unicode(x.path)
886 887 for x in nodes[:30]]) + suf)
887 888 else:
888 889 return ': ' + _('No Files')
889 890
890 891
891 892 def repo_link(groups_and_repos, last_url=None):
892 893 """
893 894 Makes a breadcrumbs link to repo within a group
894 895 joins &raquo; on each group to create a fancy link
895 896
896 897 ex::
897 898 group >> subgroup >> repo
898 899
899 900 :param groups_and_repos:
900 901 :param last_url:
901 902 """
902 903 groups, repo_name = groups_and_repos
903 904 last_link = link_to(repo_name, last_url) if last_url else repo_name
904 905
905 906 if not groups:
906 907 if last_url:
907 908 return last_link
908 909 return repo_name
909 910 else:
910 911 def make_link(group):
911 912 return link_to(group.name,
912 913 url('repos_group_home', group_name=group.group_name))
913 914 return literal(' &raquo; '.join(map(make_link, groups) + [last_link]))
914 915
915 916
916 917 def fancy_file_stats(stats):
917 918 """
918 919 Displays a fancy two colored bar for number of added/deleted
919 920 lines of code on file
920 921
921 922 :param stats: two element list of added/deleted lines of code
922 923 """
923 924 def cgen(l_type, a_v, d_v):
924 925 mapping = {'tr': 'top-right-rounded-corner-mid',
925 926 'tl': 'top-left-rounded-corner-mid',
926 927 'br': 'bottom-right-rounded-corner-mid',
927 928 'bl': 'bottom-left-rounded-corner-mid'}
928 929 map_getter = lambda x: mapping[x]
929 930
930 931 if l_type == 'a' and d_v:
931 932 #case when added and deleted are present
932 933 return ' '.join(map(map_getter, ['tl', 'bl']))
933 934
934 935 if l_type == 'a' and not d_v:
935 936 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
936 937
937 938 if l_type == 'd' and a_v:
938 939 return ' '.join(map(map_getter, ['tr', 'br']))
939 940
940 941 if l_type == 'd' and not a_v:
941 942 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
942 943
943 944 a, d = stats[0], stats[1]
944 945 width = 100
945 946
946 947 if a == 'b':
947 948 #binary mode
948 949 b_d = '<div class="bin%s %s" style="width:100%%">%s</div>' % (d, cgen('a', a_v='', d_v=0), 'bin')
949 950 b_a = '<div class="bin1" style="width:0%%">%s</div>' % ('bin')
950 951 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
951 952
952 953 t = stats[0] + stats[1]
953 954 unit = float(width) / (t or 1)
954 955
955 956 # needs > 9% of width to be visible or 0 to be hidden
956 957 a_p = max(9, unit * a) if a > 0 else 0
957 958 d_p = max(9, unit * d) if d > 0 else 0
958 959 p_sum = a_p + d_p
959 960
960 961 if p_sum > width:
961 962 #adjust the percentage to be == 100% since we adjusted to 9
962 963 if a_p > d_p:
963 964 a_p = a_p - (p_sum - width)
964 965 else:
965 966 d_p = d_p - (p_sum - width)
966 967
967 968 a_v = a if a > 0 else ''
968 969 d_v = d if d > 0 else ''
969 970
970 971 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
971 972 cgen('a', a_v, d_v), a_p, a_v
972 973 )
973 974 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
974 975 cgen('d', a_v, d_v), d_p, d_v
975 976 )
976 977 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
977 978
978 979
979 980 def urlify_text(text_):
980 981
981 982 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
982 983 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
983 984
984 985 def url_func(match_obj):
985 986 url_full = match_obj.groups()[0]
986 987 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
987 988
988 989 return literal(url_pat.sub(url_func, text_))
989 990
990 991
991 992 def urlify_changesets(text_, repository):
992 993 """
993 994 Extract revision ids from changeset and make link from them
994 995
995 996 :param text_:
996 997 :param repository:
997 998 """
998 999
999 1000 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
1000 1001
1001 1002 def url_func(match_obj):
1002 1003 rev = match_obj.groups()[0]
1003 1004 pref = ''
1004 1005 if match_obj.group().startswith(' '):
1005 1006 pref = ' '
1006 1007 tmpl = (
1007 1008 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1008 1009 '%(rev)s'
1009 1010 '</a>'
1010 1011 )
1011 1012 return tmpl % {
1012 1013 'pref': pref,
1013 1014 'cls': 'revision-link',
1014 1015 'url': url('changeset_home', repo_name=repository, revision=rev),
1015 1016 'rev': rev,
1016 1017 }
1017 1018
1018 1019 newtext = URL_PAT.sub(url_func, text_)
1019 1020
1020 1021 return newtext
1021 1022
1022 1023
1023 1024 def urlify_commit(text_, repository=None, link_=None):
1024 1025 """
1025 1026 Parses given text message and makes proper links.
1026 1027 issues are linked to given issue-server, and rest is a changeset link
1027 1028 if link_ is given, in other case it's a plain text
1028 1029
1029 1030 :param text_:
1030 1031 :param repository:
1031 1032 :param link_: changeset link
1032 1033 """
1033 1034 import traceback
1034 1035
1035 1036 def escaper(string):
1036 1037 return string.replace('<', '&lt;').replace('>', '&gt;')
1037 1038
1038 1039 def linkify_others(t, l):
1039 1040 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1040 1041 links = []
1041 1042 for e in urls.split(t):
1042 1043 if not urls.match(e):
1043 1044 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1044 1045 else:
1045 1046 links.append(e)
1046 1047
1047 1048 return ''.join(links)
1048 1049
1049 1050 # urlify changesets - extrac revisions and make link out of them
1050 1051 newtext = urlify_changesets(escaper(text_), repository)
1051 1052
1052 1053 try:
1053 1054 conf = config['app_conf']
1054 1055
1055 1056 # allow multiple issue servers to be used
1056 1057 valid_indices = [
1057 1058 x.group(1)
1058 1059 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1059 1060 if x and 'issue_server_link%s' % x.group(1) in conf
1060 1061 and 'issue_prefix%s' % x.group(1) in conf
1061 1062 ]
1062 1063
1063 1064 log.debug('found issue server suffixes `%s` during valuation of: %s'
1064 1065 % (','.join(valid_indices), newtext))
1065 1066
1066 1067 for pattern_index in valid_indices:
1067 1068 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1068 1069 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1069 1070 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1070 1071
1071 1072 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1072 1073 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1073 1074 ISSUE_PREFIX))
1074 1075
1075 1076 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1076 1077
1077 1078 def url_func(match_obj):
1078 1079 pref = ''
1079 1080 if match_obj.group().startswith(' '):
1080 1081 pref = ' '
1081 1082
1082 1083 issue_id = ''.join(match_obj.groups())
1083 1084 tmpl = (
1084 1085 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1085 1086 '%(issue-prefix)s%(id-repr)s'
1086 1087 '</a>'
1087 1088 )
1088 1089 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1089 1090 if repository:
1090 1091 url = url.replace('{repo}', repository)
1091 1092 repo_name = repository.split(URL_SEP)[-1]
1092 1093 url = url.replace('{repo_name}', repo_name)
1093 1094
1094 1095 return tmpl % {
1095 1096 'pref': pref,
1096 1097 'cls': 'issue-tracker-link',
1097 1098 'url': url,
1098 1099 'id-repr': issue_id,
1099 1100 'issue-prefix': ISSUE_PREFIX,
1100 1101 'serv': ISSUE_SERVER_LNK,
1101 1102 }
1102 1103 newtext = URL_PAT.sub(url_func, newtext)
1103 1104 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1104 1105
1105 1106 # if we actually did something above
1106 1107 if link_:
1107 1108 # wrap not links into final link => link_
1108 1109 newtext = linkify_others(newtext, link_)
1109 1110 except:
1110 1111 log.error(traceback.format_exc())
1111 1112 pass
1112 1113
1113 1114 return literal(newtext)
1114 1115
1115 1116
1116 1117 def rst(source):
1117 1118 return literal('<div class="rst-block">%s</div>' %
1118 1119 MarkupRenderer.rst(source))
1119 1120
1120 1121
1121 1122 def rst_w_mentions(source):
1122 1123 """
1123 1124 Wrapped rst renderer with @mention highlighting
1124 1125
1125 1126 :param source:
1126 1127 """
1127 1128 return literal('<div class="rst-block">%s</div>' %
1128 1129 MarkupRenderer.rst_with_mentions(source))
1129 1130
1130 1131
1131 1132 def changeset_status(repo, revision):
1132 1133 return ChangesetStatusModel().get_status(repo, revision)
1133 1134
1134 1135
1135 1136 def changeset_status_lbl(changeset_status):
1136 1137 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1137 1138
1138 1139
1139 1140 def get_permission_name(key):
1140 1141 return dict(Permission.PERMS).get(key)
1141 1142
1142 1143
1143 1144 def journal_filter_help():
1144 1145 return _(textwrap.dedent('''
1145 1146 Example filter terms:
1146 1147 repository:vcs
1147 1148 username:marcin
1148 1149 action:*push*
1149 1150 ip:127.0.0.1
1150 1151 date:20120101
1151 1152 date:[20120101100000 TO 20120102]
1152 1153
1153 1154 Generate wildcards using '*' character:
1154 1155 "repositroy:vcs*" - search everything starting with 'vcs'
1155 1156 "repository:*vcs*" - search for repository containing 'vcs'
1156 1157
1157 1158 Optional AND / OR operators in queries
1158 1159 "repository:vcs OR repository:test"
1159 1160 "username:test AND repository:test*"
1160 1161 '''))
1161 1162
1162 1163
1163 1164 def not_mapped_error(repo_name):
1164 1165 flash(_('%s repository is not mapped to db perhaps'
1165 1166 ' it was created or renamed from the filesystem'
1166 1167 ' please run the application again'
1167 1168 ' in order to rescan repositories') % repo_name, category='error')
1168 1169
1169 1170
1170 1171 def ip_range(ip_addr):
1171 1172 from rhodecode.model.db import UserIpMap
1172 1173 s, e = UserIpMap._get_ip_range(ip_addr)
1173 1174 return '%s - %s' % (s, e)
@@ -1,781 +1,801 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 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
26 26 import os
27 27 import re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 35 import decorator
36 36 import warnings
37 37 from os.path import abspath
38 38 from os.path import dirname as dn, join as jn
39 39
40 40 from paste.script.command import Command, BadCommand
41 41
42 42 from mercurial import ui, config
43 43
44 44 from webhelpers.text import collapse, remove_formatting, strip_tags
45 45
46 46 from rhodecode.lib.vcs import get_backend
47 47 from rhodecode.lib.vcs.backends.base import BaseChangeset
48 48 from rhodecode.lib.vcs.utils.lazy import LazyProperty
49 49 from rhodecode.lib.vcs.utils.helpers import get_scm
50 50 from rhodecode.lib.vcs.exceptions import VCSError
51 51
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model import meta
55 55 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
56 56 UserLog, RepoGroup, RhodeCodeSetting, CacheInvalidation
57 57 from rhodecode.model.meta import Session
58 58 from rhodecode.model.repos_group import ReposGroupModel
59 59 from rhodecode.lib.utils2 import safe_str, safe_unicode
60 60 from rhodecode.lib.vcs.utils.fakemod import create_module
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
65 65
66 66
67 67 def recursive_replace(str_, replace=' '):
68 68 """
69 69 Recursive replace of given sign to just one instance
70 70
71 71 :param str_: given string
72 72 :param replace: char to find and replace multiple instances
73 73
74 74 Examples::
75 75 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
76 76 'Mighty-Mighty-Bo-sstones'
77 77 """
78 78
79 79 if str_.find(replace * 2) == -1:
80 80 return str_
81 81 else:
82 82 str_ = str_.replace(replace * 2, replace)
83 83 return recursive_replace(str_, replace)
84 84
85 85
86 86 def repo_name_slug(value):
87 87 """
88 88 Return slug of name of repository
89 89 This function is called on each creation/modification
90 90 of repository to prevent bad names in repo
91 91 """
92 92
93 93 slug = remove_formatting(value)
94 94 slug = strip_tags(slug)
95 95
96 96 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
97 97 slug = slug.replace(c, '-')
98 98 slug = recursive_replace(slug, '-')
99 99 slug = collapse(slug, '-')
100 100 return slug
101 101
102 102
103 103 def get_repo_slug(request):
104 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
105 105 if _repo:
106 106 _repo = _repo.rstrip('/')
107 107 return _repo
108 108
109 109
110 110 def get_repos_group_slug(request):
111 111 _group = request.environ['pylons.routes_dict'].get('group_name')
112 112 if _group:
113 113 _group = _group.rstrip('/')
114 114 return _group
115 115
116 116
117 117 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
118 118 """
119 119 Action logger for various actions made by users
120 120
121 121 :param user: user that made this action, can be a unique username string or
122 122 object containing user_id attribute
123 123 :param action: action to log, should be on of predefined unique actions for
124 124 easy translations
125 125 :param repo: string name of repository or object containing repo_id,
126 126 that action was made on
127 127 :param ipaddr: optional ip address from what the action was made
128 128 :param sa: optional sqlalchemy session
129 129
130 130 """
131 131
132 132 if not sa:
133 133 sa = meta.Session()
134 134
135 135 try:
136 136 if hasattr(user, 'user_id'):
137 137 user_obj = User.get(user.user_id)
138 138 elif isinstance(user, basestring):
139 139 user_obj = User.get_by_username(user)
140 140 else:
141 141 raise Exception('You have to provide a user object or a username')
142 142
143 143 if hasattr(repo, 'repo_id'):
144 144 repo_obj = Repository.get(repo.repo_id)
145 145 repo_name = repo_obj.repo_name
146 146 elif isinstance(repo, basestring):
147 147 repo_name = repo.lstrip('/')
148 148 repo_obj = Repository.get_by_repo_name(repo_name)
149 149 else:
150 150 repo_obj = None
151 151 repo_name = ''
152 152
153 153 user_log = UserLog()
154 154 user_log.user_id = user_obj.user_id
155 155 user_log.username = user_obj.username
156 156 user_log.action = safe_unicode(action)
157 157
158 158 user_log.repository = repo_obj
159 159 user_log.repository_name = repo_name
160 160
161 161 user_log.action_date = datetime.datetime.now()
162 162 user_log.user_ip = ipaddr
163 163 sa.add(user_log)
164 164
165 165 log.info('Logging action %s on %s by %s' %
166 166 (action, safe_unicode(repo), user_obj))
167 167 if commit:
168 168 sa.commit()
169 169 except:
170 170 log.error(traceback.format_exc())
171 171 raise
172 172
173 173
174 174 def get_repos(path, recursive=False, skip_removed_repos=True):
175 175 """
176 176 Scans given path for repos and return (name,(type,path)) tuple
177 177
178 178 :param path: path to scan for repositories
179 179 :param recursive: recursive search and return names with subdirs in front
180 180 """
181 181
182 182 # remove ending slash for better results
183 183 path = path.rstrip(os.sep)
184 184 log.debug('now scanning in %s location recursive:%s...' % (path, recursive))
185 185
186 186 def _get_repos(p):
187 187 if not os.access(p, os.W_OK):
188 188 return
189 189 for dirpath in os.listdir(p):
190 190 if os.path.isfile(os.path.join(p, dirpath)):
191 191 continue
192 192 cur_path = os.path.join(p, dirpath)
193 193
194 194 # skip removed repos
195 195 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
196 196 continue
197 197
198 198 #skip .<somethin> dirs
199 199 if dirpath.startswith('.'):
200 200 continue
201 201
202 202 try:
203 203 scm_info = get_scm(cur_path)
204 204 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
205 205 except VCSError:
206 206 if not recursive:
207 207 continue
208 208 #check if this dir containts other repos for recursive scan
209 209 rec_path = os.path.join(p, dirpath)
210 210 if os.path.isdir(rec_path):
211 211 for inner_scm in _get_repos(rec_path):
212 212 yield inner_scm
213 213
214 214 return _get_repos(path)
215 215
216 216 #alias for backward compat
217 217 get_filesystem_repos = get_repos
218 218
219 219
220 220 def is_valid_repo(repo_name, base_path, scm=None):
221 221 """
222 222 Returns True if given path is a valid repository False otherwise.
223 223 If scm param is given also compare if given scm is the same as expected
224 224 from scm parameter
225 225
226 226 :param repo_name:
227 227 :param base_path:
228 228 :param scm:
229 229
230 230 :return True: if given path is a valid repository
231 231 """
232 232 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
233 233
234 234 try:
235 235 scm_ = get_scm(full_path)
236 236 if scm:
237 237 return scm_[0] == scm
238 238 return True
239 239 except VCSError:
240 240 return False
241 241
242 242
243 243 def is_valid_repos_group(repos_group_name, base_path):
244 244 """
245 245 Returns True if given path is a repos group False otherwise
246 246
247 247 :param repo_name:
248 248 :param base_path:
249 249 """
250 250 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
251 251
252 252 # check if it's not a repo
253 253 if is_valid_repo(repos_group_name, base_path):
254 254 return False
255 255
256 256 try:
257 257 # we need to check bare git repos at higher level
258 258 # since we might match branches/hooks/info/objects or possible
259 259 # other things inside bare git repo
260 260 get_scm(os.path.dirname(full_path))
261 261 return False
262 262 except VCSError:
263 263 pass
264 264
265 265 # check if it's a valid path
266 266 if os.path.isdir(full_path):
267 267 return True
268 268
269 269 return False
270 270
271 271
272 272 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
273 273 while True:
274 274 ok = raw_input(prompt)
275 275 if ok in ('y', 'ye', 'yes'):
276 276 return True
277 277 if ok in ('n', 'no', 'nop', 'nope'):
278 278 return False
279 279 retries = retries - 1
280 280 if retries < 0:
281 281 raise IOError
282 282 print complaint
283 283
284 284 #propagated from mercurial documentation
285 285 ui_sections = ['alias', 'auth',
286 286 'decode/encode', 'defaults',
287 287 'diff', 'email',
288 288 'extensions', 'format',
289 289 'merge-patterns', 'merge-tools',
290 290 'hooks', 'http_proxy',
291 291 'smtp', 'patch',
292 292 'paths', 'profiling',
293 293 'server', 'trusted',
294 294 'ui', 'web', ]
295 295
296 296
297 297 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
298 298 """
299 299 A function that will read python rc files or database
300 300 and make an mercurial ui object from read options
301 301
302 302 :param path: path to mercurial config file
303 303 :param checkpaths: check the path
304 304 :param read_from: read from 'file' or 'db'
305 305 """
306 306
307 307 baseui = ui.ui()
308 308
309 309 # clean the baseui object
310 310 baseui._ocfg = config.config()
311 311 baseui._ucfg = config.config()
312 312 baseui._tcfg = config.config()
313 313
314 314 if read_from == 'file':
315 315 if not os.path.isfile(path):
316 316 log.debug('hgrc file is not present at %s, skipping...' % path)
317 317 return False
318 318 log.debug('reading hgrc from %s' % path)
319 319 cfg = config.config()
320 320 cfg.read(path)
321 321 for section in ui_sections:
322 322 for k, v in cfg.items(section):
323 323 log.debug('settings ui from file: [%s] %s=%s' % (section, k, v))
324 324 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
325 325
326 326 elif read_from == 'db':
327 327 sa = meta.Session()
328 328 ret = sa.query(RhodeCodeUi)\
329 329 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
330 330 .all()
331 331
332 332 hg_ui = ret
333 333 for ui_ in hg_ui:
334 334 if ui_.ui_active:
335 335 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
336 336 ui_.ui_key, ui_.ui_value)
337 337 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
338 338 safe_str(ui_.ui_value))
339 339 if ui_.ui_key == 'push_ssl':
340 340 # force set push_ssl requirement to False, rhodecode
341 341 # handles that
342 342 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
343 343 False)
344 344 if clear_session:
345 345 meta.Session.remove()
346 346 return baseui
347 347
348 348
349 349 def set_rhodecode_config(config):
350 350 """
351 351 Updates pylons config with new settings from database
352 352
353 353 :param config:
354 354 """
355 355 hgsettings = RhodeCodeSetting.get_app_settings()
356 356
357 357 for k, v in hgsettings.items():
358 358 config[k] = v
359 359
360 360
361 361 def invalidate_cache(cache_key, *args):
362 362 """
363 363 Puts cache invalidation task into db for
364 364 further global cache invalidation
365 365 """
366 366
367 367 from rhodecode.model.scm import ScmModel
368 368
369 369 if cache_key.startswith('get_repo_cached_'):
370 370 name = cache_key.split('get_repo_cached_')[-1]
371 371 ScmModel().mark_for_invalidation(name)
372 372
373 373
374 374 def map_groups(path):
375 375 """
376 376 Given a full path to a repository, create all nested groups that this
377 377 repo is inside. This function creates parent-child relationships between
378 378 groups and creates default perms for all new groups.
379 379
380 380 :param paths: full path to repository
381 381 """
382 382 sa = meta.Session()
383 383 groups = path.split(Repository.url_sep())
384 384 parent = None
385 385 group = None
386 386
387 387 # last element is repo in nested groups structure
388 388 groups = groups[:-1]
389 389 rgm = ReposGroupModel(sa)
390 390 for lvl, group_name in enumerate(groups):
391 391 group_name = '/'.join(groups[:lvl] + [group_name])
392 392 group = RepoGroup.get_by_group_name(group_name)
393 393 desc = '%s group' % group_name
394 394
395 395 # skip folders that are now removed repos
396 396 if REMOVED_REPO_PAT.match(group_name):
397 397 break
398 398
399 399 if group is None:
400 400 log.debug('creating group level: %s group_name: %s' % (lvl,
401 401 group_name))
402 402 group = RepoGroup(group_name, parent)
403 403 group.group_description = desc
404 404 sa.add(group)
405 405 rgm._create_default_perms(group)
406 406 sa.flush()
407 407 parent = group
408 408 return group
409 409
410 410
411 411 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
412 412 install_git_hook=False):
413 413 """
414 414 maps all repos given in initial_repo_list, non existing repositories
415 415 are created, if remove_obsolete is True it also check for db entries
416 416 that are not in initial_repo_list and removes them.
417 417
418 418 :param initial_repo_list: list of repositories found by scanning methods
419 419 :param remove_obsolete: check for obsolete entries in database
420 420 :param install_git_hook: if this is True, also check and install githook
421 421 for a repo if missing
422 422 """
423 423 from rhodecode.model.repo import RepoModel
424 424 from rhodecode.model.scm import ScmModel
425 425 sa = meta.Session()
426 426 rm = RepoModel()
427 427 user = sa.query(User).filter(User.admin == True).first()
428 428 if user is None:
429 429 raise Exception('Missing administrative account!')
430 430 added = []
431 431
432 432 # # clear cache keys
433 433 # log.debug("Clearing cache keys now...")
434 434 # CacheInvalidation.clear_cache()
435 435 # sa.commit()
436 436
437 437 ##creation defaults
438 438 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
439 439 enable_statistics = defs.get('repo_enable_statistics')
440 440 enable_locking = defs.get('repo_enable_locking')
441 441 enable_downloads = defs.get('repo_enable_downloads')
442 442 private = defs.get('repo_private')
443 443
444 444 for name, repo in initial_repo_list.items():
445 445 group = map_groups(name)
446 446 db_repo = rm.get_by_repo_name(name)
447 447 # found repo that is on filesystem not in RhodeCode database
448 448 if not db_repo:
449 449 log.info('repository %s not found, creating now' % name)
450 450 added.append(name)
451 451 desc = (repo.description
452 452 if repo.description != 'unknown'
453 453 else '%s repository' % name)
454 454
455 455 new_repo = rm.create_repo(
456 456 repo_name=name,
457 457 repo_type=repo.alias,
458 458 description=desc,
459 459 repos_group=getattr(group, 'group_id', None),
460 460 owner=user,
461 461 just_db=True,
462 462 enable_locking=enable_locking,
463 463 enable_downloads=enable_downloads,
464 464 enable_statistics=enable_statistics,
465 465 private=private
466 466 )
467 467 # we added that repo just now, and make sure it has githook
468 468 # installed
469 469 if new_repo.repo_type == 'git':
470 470 ScmModel().install_git_hook(new_repo.scm_instance)
471 471 new_repo.update_changeset_cache()
472 472 elif install_git_hook:
473 473 if db_repo.repo_type == 'git':
474 474 ScmModel().install_git_hook(db_repo.scm_instance)
475 475 # during starting install all cache keys for all repositories in the
476 476 # system, this will register all repos and multiple instances
477 477 key, _prefix, _org_key = CacheInvalidation._get_key(name)
478 478 CacheInvalidation.invalidate(name)
479 479 log.debug("Creating a cache key for %s, instance_id %s"
480 480 % (name, _prefix or 'unknown'))
481 481
482 482 sa.commit()
483 483 removed = []
484 484 if remove_obsolete:
485 485 # remove from database those repositories that are not in the filesystem
486 486 for repo in sa.query(Repository).all():
487 487 if repo.repo_name not in initial_repo_list.keys():
488 488 log.debug("Removing non-existing repository found in db `%s`" %
489 489 repo.repo_name)
490 490 try:
491 491 sa.delete(repo)
492 492 sa.commit()
493 493 removed.append(repo.repo_name)
494 494 except:
495 495 #don't hold further removals on error
496 496 log.error(traceback.format_exc())
497 497 sa.rollback()
498 498
499 499 return added, removed
500 500
501 501
502 502 # set cache regions for beaker so celery can utilise it
503 503 def add_cache(settings):
504 504 cache_settings = {'regions': None}
505 505 for key in settings.keys():
506 506 for prefix in ['beaker.cache.', 'cache.']:
507 507 if key.startswith(prefix):
508 508 name = key.split(prefix)[1].strip()
509 509 cache_settings[name] = settings[key].strip()
510 510 if cache_settings['regions']:
511 511 for region in cache_settings['regions'].split(','):
512 512 region = region.strip()
513 513 region_settings = {}
514 514 for key, value in cache_settings.items():
515 515 if key.startswith(region):
516 516 region_settings[key.split('.')[1]] = value
517 517 region_settings['expire'] = int(region_settings.get('expire',
518 518 60))
519 519 region_settings.setdefault('lock_dir',
520 520 cache_settings.get('lock_dir'))
521 521 region_settings.setdefault('data_dir',
522 522 cache_settings.get('data_dir'))
523 523
524 524 if 'type' not in region_settings:
525 525 region_settings['type'] = cache_settings.get('type',
526 526 'memory')
527 527 beaker.cache.cache_regions[region] = region_settings
528 528
529 529
530 530 def load_rcextensions(root_path):
531 531 import rhodecode
532 532 from rhodecode.config import conf
533 533
534 534 path = os.path.join(root_path, 'rcextensions', '__init__.py')
535 535 if os.path.isfile(path):
536 536 rcext = create_module('rc', path)
537 537 EXT = rhodecode.EXTENSIONS = rcext
538 538 log.debug('Found rcextensions now loading %s...' % rcext)
539 539
540 540 # Additional mappings that are not present in the pygments lexers
541 541 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
542 542
543 543 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
544 544
545 545 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
546 546 log.debug('settings custom INDEX_EXTENSIONS')
547 547 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
548 548
549 549 #ADDITIONAL MAPPINGS
550 550 log.debug('adding extra into INDEX_EXTENSIONS')
551 551 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
552 552
553 # auto check if the module is not missing any data, set to default if is
554 # this will help autoupdate new feature of rcext module
555 from rhodecode.config import rcextensions
556 for k in dir(rcextensions):
557 if not k.startswith('_') and not hasattr(EXT, k):
558 setattr(EXT, k, getattr(rcextensions, k))
559
560
561 def get_custom_lexer(extension):
562 """
563 returns a custom lexer if it's defined in rcextensions module, or None
564 if there's no custom lexer defined
565 """
566 import rhodecode
567 from pygments import lexers
568 #check if we didn't define this extension as other lexer
569 if rhodecode.EXTENSIONS and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
570 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
571 return lexers.get_lexer_by_name(_lexer_name)
572
553 573
554 574 #==============================================================================
555 575 # TEST FUNCTIONS AND CREATORS
556 576 #==============================================================================
557 577 def create_test_index(repo_location, config, full_index):
558 578 """
559 579 Makes default test index
560 580
561 581 :param config: test config
562 582 :param full_index:
563 583 """
564 584
565 585 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
566 586 from rhodecode.lib.pidlock import DaemonLock, LockHeld
567 587
568 588 repo_location = repo_location
569 589
570 590 index_location = os.path.join(config['app_conf']['index_dir'])
571 591 if not os.path.exists(index_location):
572 592 os.makedirs(index_location)
573 593
574 594 try:
575 595 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
576 596 WhooshIndexingDaemon(index_location=index_location,
577 597 repo_location=repo_location)\
578 598 .run(full_index=full_index)
579 599 l.release()
580 600 except LockHeld:
581 601 pass
582 602
583 603
584 604 def create_test_env(repos_test_path, config):
585 605 """
586 606 Makes a fresh database and
587 607 install test repository into tmp dir
588 608 """
589 609 from rhodecode.lib.db_manage import DbManage
590 610 from rhodecode.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
591 611
592 612 # PART ONE create db
593 613 dbconf = config['sqlalchemy.db1.url']
594 614 log.debug('making test db %s' % dbconf)
595 615
596 616 # create test dir if it doesn't exist
597 617 if not os.path.isdir(repos_test_path):
598 618 log.debug('Creating testdir %s' % repos_test_path)
599 619 os.makedirs(repos_test_path)
600 620
601 621 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
602 622 tests=True)
603 623 dbmanage.create_tables(override=True)
604 624 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
605 625 dbmanage.create_default_user()
606 626 dbmanage.admin_prompt()
607 627 dbmanage.create_permissions()
608 628 dbmanage.populate_default_permissions()
609 629 Session().commit()
610 630 # PART TWO make test repo
611 631 log.debug('making test vcs repositories')
612 632
613 633 idx_path = config['app_conf']['index_dir']
614 634 data_path = config['app_conf']['cache_dir']
615 635
616 636 #clean index and data
617 637 if idx_path and os.path.exists(idx_path):
618 638 log.debug('remove %s' % idx_path)
619 639 shutil.rmtree(idx_path)
620 640
621 641 if data_path and os.path.exists(data_path):
622 642 log.debug('remove %s' % data_path)
623 643 shutil.rmtree(data_path)
624 644
625 645 #CREATE DEFAULT TEST REPOS
626 646 cur_dir = dn(dn(abspath(__file__)))
627 647 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
628 648 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
629 649 tar.close()
630 650
631 651 cur_dir = dn(dn(abspath(__file__)))
632 652 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_git.tar.gz"))
633 653 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
634 654 tar.close()
635 655
636 656 #LOAD VCS test stuff
637 657 from rhodecode.tests.vcs import setup_package
638 658 setup_package()
639 659
640 660
641 661 #==============================================================================
642 662 # PASTER COMMANDS
643 663 #==============================================================================
644 664 class BasePasterCommand(Command):
645 665 """
646 666 Abstract Base Class for paster commands.
647 667
648 668 The celery commands are somewhat aggressive about loading
649 669 celery.conf, and since our module sets the `CELERY_LOADER`
650 670 environment variable to our loader, we have to bootstrap a bit and
651 671 make sure we've had a chance to load the pylons config off of the
652 672 command line, otherwise everything fails.
653 673 """
654 674 min_args = 1
655 675 min_args_error = "Please provide a paster config file as an argument."
656 676 takes_config_file = 1
657 677 requires_config_file = True
658 678
659 679 def notify_msg(self, msg, log=False):
660 680 """Make a notification to user, additionally if logger is passed
661 681 it logs this action using given logger
662 682
663 683 :param msg: message that will be printed to user
664 684 :param log: logging instance, to use to additionally log this message
665 685
666 686 """
667 687 if log and isinstance(log, logging):
668 688 log(msg)
669 689
670 690 def run(self, args):
671 691 """
672 692 Overrides Command.run
673 693
674 694 Checks for a config file argument and loads it.
675 695 """
676 696 if len(args) < self.min_args:
677 697 raise BadCommand(
678 698 self.min_args_error % {'min_args': self.min_args,
679 699 'actual_args': len(args)})
680 700
681 701 # Decrement because we're going to lob off the first argument.
682 702 # @@ This is hacky
683 703 self.min_args -= 1
684 704 self.bootstrap_config(args[0])
685 705 self.update_parser()
686 706 return super(BasePasterCommand, self).run(args[1:])
687 707
688 708 def update_parser(self):
689 709 """
690 710 Abstract method. Allows for the class's parser to be updated
691 711 before the superclass's `run` method is called. Necessary to
692 712 allow options/arguments to be passed through to the underlying
693 713 celery command.
694 714 """
695 715 raise NotImplementedError("Abstract Method.")
696 716
697 717 def bootstrap_config(self, conf):
698 718 """
699 719 Loads the pylons configuration.
700 720 """
701 721 from pylons import config as pylonsconfig
702 722
703 723 self.path_to_ini_file = os.path.realpath(conf)
704 724 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
705 725 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
706 726
707 727 def _init_session(self):
708 728 """
709 729 Inits SqlAlchemy Session
710 730 """
711 731 logging.config.fileConfig(self.path_to_ini_file)
712 732 from pylons import config
713 733 from rhodecode.model import init_model
714 734 from rhodecode.lib.utils2 import engine_from_config
715 735
716 736 #get to remove repos !!
717 737 add_cache(config)
718 738 engine = engine_from_config(config, 'sqlalchemy.db1.')
719 739 init_model(engine)
720 740
721 741
722 742 def check_git_version():
723 743 """
724 744 Checks what version of git is installed in system, and issues a warning
725 745 if it's too old for RhodeCode to properly work.
726 746 """
727 747 import subprocess
728 748 from distutils.version import StrictVersion
729 749 from rhodecode import BACKENDS
730 750
731 751 p = subprocess.Popen('git --version', shell=True,
732 752 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
733 753 stdout, stderr = p.communicate()
734 754 ver = (stdout.split(' ')[-1] or '').strip() or '0.0.0'
735 755 if len(ver.split('.')) > 3:
736 756 #StrictVersion needs to be only 3 element type
737 757 ver = '.'.join(ver.split('.')[:3])
738 758 try:
739 759 _ver = StrictVersion(ver)
740 760 except:
741 761 _ver = StrictVersion('0.0.0')
742 762 stderr = traceback.format_exc()
743 763
744 764 req_ver = '1.7.4'
745 765 to_old_git = False
746 766 if _ver < StrictVersion(req_ver):
747 767 to_old_git = True
748 768
749 769 if 'git' in BACKENDS:
750 770 log.debug('GIT version detected: %s' % stdout)
751 771 if stderr:
752 772 log.warning('Unable to detect git version org error was:%r' % stderr)
753 773 elif to_old_git:
754 774 log.warning('RhodeCode detected git version %s, which is too old '
755 775 'for the system to function properly. Make sure '
756 776 'its version is at least %s' % (ver, req_ver))
757 777 return _ver
758 778
759 779
760 780 @decorator.decorator
761 781 def jsonify(func, *args, **kwargs):
762 782 """Action decorator that formats output for JSON
763 783
764 784 Given a function that will return content, this decorator will turn
765 785 the result into JSON, with a content-type of 'application/json' and
766 786 output it.
767 787
768 788 """
769 789 from pylons.decorators.util import get_pylons
770 790 from rhodecode.lib.ext_json import json
771 791 pylons = get_pylons(args)
772 792 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
773 793 data = func(*args, **kwargs)
774 794 if isinstance(data, (list, tuple)):
775 795 msg = "JSON responses with Array envelopes are susceptible to " \
776 796 "cross-site data leak attacks, see " \
777 797 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
778 798 warnings.warn(msg, Warning, 2)
779 799 log.warning(msg)
780 800 log.debug("Returning JSON wrapped action output")
781 801 return json.dumps(data, encoding='utf-8')
General Comments 0
You need to be logged in to leave comments. Login now