##// END OF EJS Templates
moved soon-to-be-deleted code from vcs to rhodecode...
marcink -
r1753:1d1ccb87 beta
parent child Browse files
Show More
@@ -0,0 +1,190
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.annotate
4 ~~~~~~~~~~~~~~~~~~~~~~
5
6 Anontation library for usage in rhodecode, previously part of vcs
7
8 :created_on: Dec 4, 2011
9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13
14 from vcs.exceptions import VCSError
15 from vcs.nodes import FileNode
16 from pygments.formatters import HtmlFormatter
17 from pygments import highlight
18
19 import StringIO
20
21
22 def annotate_highlight(filenode, annotate_from_changeset_func=None,
23 order=None, headers=None, **options):
24 """
25 Returns html portion containing annotated table with 3 columns: line
26 numbers, changeset information and pygmentized line of code.
27
28 :param filenode: FileNode object
29 :param annotate_from_changeset_func: function taking changeset and
30 returning single annotate cell; needs break line at the end
31 :param order: ordered sequence of ``ls`` (line numbers column),
32 ``annotate`` (annotate column), ``code`` (code column); Default is
33 ``['ls', 'annotate', 'code']``
34 :param headers: dictionary with headers (keys are whats in ``order``
35 parameter)
36 """
37 options['linenos'] = True
38 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
39 headers=headers,
40 annotate_from_changeset_func=annotate_from_changeset_func, **options)
41 lexer = filenode.lexer
42 highlighted = highlight(filenode.content, lexer, formatter)
43 return highlighted
44
45
46 class AnnotateHtmlFormatter(HtmlFormatter):
47
48 def __init__(self, filenode, annotate_from_changeset_func=None,
49 order=None, **options):
50 """
51 If ``annotate_from_changeset_func`` is passed it should be a function
52 which returns string from the given changeset. For example, we may pass
53 following function as ``annotate_from_changeset_func``::
54
55 def changeset_to_anchor(changeset):
56 return '<a href="/changesets/%s/">%s</a>\n' %\
57 (changeset.id, changeset.id)
58
59 :param annotate_from_changeset_func: see above
60 :param order: (default: ``['ls', 'annotate', 'code']``); order of
61 columns;
62 :param options: standard pygment's HtmlFormatter options, there is
63 extra option tough, ``headers``. For instance we can pass::
64
65 formatter = AnnotateHtmlFormatter(filenode, headers={
66 'ls': '#',
67 'annotate': 'Annotate',
68 'code': 'Code',
69 })
70
71 """
72 super(AnnotateHtmlFormatter, self).__init__(**options)
73 self.annotate_from_changeset_func = annotate_from_changeset_func
74 self.order = order or ('ls', 'annotate', 'code')
75 headers = options.pop('headers', None)
76 if headers and not ('ls' in headers and 'annotate' in headers and
77 'code' in headers):
78 raise ValueError("If headers option dict is specified it must "
79 "all 'ls', 'annotate' and 'code' keys")
80 self.headers = headers
81 if isinstance(filenode, FileNode):
82 self.filenode = filenode
83 else:
84 raise VCSError("This formatter expect FileNode parameter, not %r"
85 % type(filenode))
86
87 def annotate_from_changeset(self, changeset):
88 """
89 Returns full html line for single changeset per annotated line.
90 """
91 if self.annotate_from_changeset_func:
92 return self.annotate_from_changeset_func(changeset)
93 else:
94 return ''.join((changeset.id, '\n'))
95
96 def _wrap_tablelinenos(self, inner):
97 dummyoutfile = StringIO.StringIO()
98 lncount = 0
99 for t, line in inner:
100 if t:
101 lncount += 1
102 dummyoutfile.write(line)
103
104 fl = self.linenostart
105 mw = len(str(lncount + fl - 1))
106 sp = self.linenospecial
107 st = self.linenostep
108 la = self.lineanchors
109 aln = self.anchorlinenos
110 if sp:
111 lines = []
112
113 for i in range(fl, fl + lncount):
114 if i % st == 0:
115 if i % sp == 0:
116 if aln:
117 lines.append('<a href="#%s-%d" class="special">'
118 '%*d</a>' %
119 (la, i, mw, i))
120 else:
121 lines.append('<span class="special">'
122 '%*d</span>' % (mw, i))
123 else:
124 if aln:
125 lines.append('<a href="#%s-%d">'
126 '%*d</a>' % (la, i, mw, i))
127 else:
128 lines.append('%*d' % (mw, i))
129 else:
130 lines.append('')
131 ls = '\n'.join(lines)
132 else:
133 lines = []
134 for i in range(fl, fl + lncount):
135 if i % st == 0:
136 if aln:
137 lines.append('<a href="#%s-%d">%*d</a>' \
138 % (la, i, mw, i))
139 else:
140 lines.append('%*d' % (mw, i))
141 else:
142 lines.append('')
143 ls = '\n'.join(lines)
144
145 annotate_changesets = [tup[1] for tup in self.filenode.annotate]
146 # If pygments cropped last lines break we need do that too
147 ln_cs = len(annotate_changesets)
148 ln_ = len(ls.splitlines())
149 if ln_cs > ln_:
150 annotate_changesets = annotate_changesets[:ln_ - ln_cs]
151 annotate = ''.join((self.annotate_from_changeset(changeset)
152 for changeset in annotate_changesets))
153 # in case you wonder about the seemingly redundant <div> here:
154 # since the content in the other cell also is wrapped in a div,
155 # some browsers in some configurations seem to mess up the formatting.
156 '''
157 yield 0, ('<table class="%stable">' % self.cssclass +
158 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
159 ls + '</pre></div></td>' +
160 '<td class="code">')
161 yield 0, dummyoutfile.getvalue()
162 yield 0, '</td></tr></table>'
163
164 '''
165 headers_row = []
166 if self.headers:
167 headers_row = ['<tr class="annotate-header">']
168 for key in self.order:
169 td = ''.join(('<td>', self.headers[key], '</td>'))
170 headers_row.append(td)
171 headers_row.append('</tr>')
172
173 body_row_start = ['<tr>']
174 for key in self.order:
175 if key == 'ls':
176 body_row_start.append(
177 '<td class="linenos"><div class="linenodiv"><pre>' +
178 ls + '</pre></div></td>')
179 elif key == 'annotate':
180 body_row_start.append(
181 '<td class="annotate"><div class="annotatediv"><pre>' +
182 annotate + '</pre></div></td>')
183 elif key == 'code':
184 body_row_start.append('<td class="code">')
185 yield 0, ('<table class="%stable">' % self.cssclass +
186 ''.join(headers_row) +
187 ''.join(body_row_start)
188 )
189 yield 0, dummyoutfile.getvalue()
190 yield 0, '</td></tr></table>'
@@ -0,0 +1,447
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
5
6 Set of diffing helpers, previously part of vcs
7
8
9 :created_on: Dec 4, 2011
10 :author: marcink
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
14 """
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
28 import re
29 import difflib
30
31 from itertools import tee, imap
32
33 from mercurial.match import match
34
35 from vcs.exceptions import VCSError
36 from vcs.nodes import FileNode
37
38 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True):
39 """
40 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
41
42 :param ignore_whitespace: ignore whitespaces in diff
43 """
44
45 for filenode in (filenode_old, filenode_new):
46 if not isinstance(filenode, FileNode):
47 raise VCSError("Given object should be FileNode object, not %s"
48 % filenode.__class__)
49
50 old_raw_id = getattr(filenode_old.changeset, 'raw_id', '0' * 40)
51 new_raw_id = getattr(filenode_new.changeset, 'raw_id', '0' * 40)
52
53 repo = filenode_new.changeset.repository
54 vcs_gitdiff = repo._get_diff(old_raw_id, new_raw_id, filenode_new.path,
55 ignore_whitespace)
56
57 return vcs_gitdiff
58
59
60 class DiffProcessor(object):
61 """
62 Give it a unified diff and it returns a list of the files that were
63 mentioned in the diff together with a dict of meta information that
64 can be used to render it in a HTML template.
65 """
66 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
67
68 def __init__(self, diff, differ='diff', format='udiff'):
69 """
70 :param diff: a text in diff format or generator
71 :param format: format of diff passed, `udiff` or `gitdiff`
72 """
73 if isinstance(diff, basestring):
74 diff = [diff]
75
76 self.__udiff = diff
77 self.__format = format
78 self.adds = 0
79 self.removes = 0
80
81 if isinstance(self.__udiff, basestring):
82 self.lines = iter(self.__udiff.splitlines(1))
83
84 elif self.__format == 'gitdiff':
85 udiff_copy = self.copy_iterator()
86 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
87 else:
88 udiff_copy = self.copy_iterator()
89 self.lines = imap(self.escaper, udiff_copy)
90
91 # Select a differ.
92 if differ == 'difflib':
93 self.differ = self._highlight_line_difflib
94 else:
95 self.differ = self._highlight_line_udiff
96
97 def escaper(self, string):
98 return string.replace('<', '&lt;').replace('>', '&gt;')
99
100 def copy_iterator(self):
101 """
102 make a fresh copy of generator, we should not iterate thru
103 an original as it's needed for repeating operations on
104 this instance of DiffProcessor
105 """
106 self.__udiff, iterator_copy = tee(self.__udiff)
107 return iterator_copy
108
109 def _extract_rev(self, line1, line2):
110 """
111 Extract the filename and revision hint from a line.
112 """
113
114 try:
115 if line1.startswith('--- ') and line2.startswith('+++ '):
116 l1 = line1[4:].split(None, 1)
117 old_filename = l1[0].lstrip('a/') if len(l1) >= 1 else None
118 old_rev = l1[1] if len(l1) == 2 else 'old'
119
120 l2 = line2[4:].split(None, 1)
121 new_filename = l2[0].lstrip('b/') if len(l1) >= 1 else None
122 new_rev = l2[1] if len(l2) == 2 else 'new'
123
124 filename = old_filename if (old_filename !=
125 'dev/null') else new_filename
126
127 return filename, new_rev, old_rev
128 except (ValueError, IndexError):
129 pass
130
131 return None, None, None
132
133 def _parse_gitdiff(self, diffiterator):
134 def line_decoder(l):
135 if l.startswith('+') and not l.startswith('+++'):
136 self.adds += 1
137 elif l.startswith('-') and not l.startswith('---'):
138 self.removes += 1
139 return l.decode('utf8', 'replace')
140
141 output = list(diffiterator)
142 size = len(output)
143
144 if size == 2:
145 l = []
146 l.extend([output[0]])
147 l.extend(output[1].splitlines(1))
148 return map(line_decoder, l)
149 elif size == 1:
150 return map(line_decoder, output[0].splitlines(1))
151 elif size == 0:
152 return []
153
154 raise Exception('wrong size of diff %s' % size)
155
156 def _highlight_line_difflib(self, line, next):
157 """
158 Highlight inline changes in both lines.
159 """
160
161 if line['action'] == 'del':
162 old, new = line, next
163 else:
164 old, new = next, line
165
166 oldwords = re.split(r'(\W)', old['line'])
167 newwords = re.split(r'(\W)', new['line'])
168
169 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
170
171 oldfragments, newfragments = [], []
172 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
173 oldfrag = ''.join(oldwords[i1:i2])
174 newfrag = ''.join(newwords[j1:j2])
175 if tag != 'equal':
176 if oldfrag:
177 oldfrag = '<del>%s</del>' % oldfrag
178 if newfrag:
179 newfrag = '<ins>%s</ins>' % newfrag
180 oldfragments.append(oldfrag)
181 newfragments.append(newfrag)
182
183 old['line'] = "".join(oldfragments)
184 new['line'] = "".join(newfragments)
185
186 def _highlight_line_udiff(self, line, next):
187 """
188 Highlight inline changes in both lines.
189 """
190 start = 0
191 limit = min(len(line['line']), len(next['line']))
192 while start < limit and line['line'][start] == next['line'][start]:
193 start += 1
194 end = -1
195 limit -= start
196 while -end <= limit and line['line'][end] == next['line'][end]:
197 end -= 1
198 end += 1
199 if start or end:
200 def do(l):
201 last = end + len(l['line'])
202 if l['action'] == 'add':
203 tag = 'ins'
204 else:
205 tag = 'del'
206 l['line'] = '%s<%s>%s</%s>%s' % (
207 l['line'][:start],
208 tag,
209 l['line'][start:last],
210 tag,
211 l['line'][last:]
212 )
213 do(line)
214 do(next)
215
216 def _parse_udiff(self):
217 """
218 Parse the diff an return data for the template.
219 """
220 lineiter = self.lines
221 files = []
222 try:
223 line = lineiter.next()
224 # skip first context
225 skipfirst = True
226 while 1:
227 # continue until we found the old file
228 if not line.startswith('--- '):
229 line = lineiter.next()
230 continue
231
232 chunks = []
233 filename, old_rev, new_rev = \
234 self._extract_rev(line, lineiter.next())
235 files.append({
236 'filename': filename,
237 'old_revision': old_rev,
238 'new_revision': new_rev,
239 'chunks': chunks
240 })
241
242 line = lineiter.next()
243 while line:
244 match = self._chunk_re.match(line)
245 if not match:
246 break
247
248 lines = []
249 chunks.append(lines)
250
251 old_line, old_end, new_line, new_end = \
252 [int(x or 1) for x in match.groups()[:-1]]
253 old_line -= 1
254 new_line -= 1
255 context = len(match.groups()) == 5
256 old_end += old_line
257 new_end += new_line
258
259 if context:
260 if not skipfirst:
261 lines.append({
262 'old_lineno': '...',
263 'new_lineno': '...',
264 'action': 'context',
265 'line': line,
266 })
267 else:
268 skipfirst = False
269
270 line = lineiter.next()
271 while old_line < old_end or new_line < new_end:
272 if line:
273 command, line = line[0], line[1:]
274 else:
275 command = ' '
276 affects_old = affects_new = False
277
278 # ignore those if we don't expect them
279 if command in '#@':
280 continue
281 elif command == '+':
282 affects_new = True
283 action = 'add'
284 elif command == '-':
285 affects_old = True
286 action = 'del'
287 else:
288 affects_old = affects_new = True
289 action = 'unmod'
290
291 old_line += affects_old
292 new_line += affects_new
293 lines.append({
294 'old_lineno': affects_old and old_line or '',
295 'new_lineno': affects_new and new_line or '',
296 'action': action,
297 'line': line
298 })
299 line = lineiter.next()
300
301 except StopIteration:
302 pass
303
304 # highlight inline changes
305 for file in files:
306 for chunk in chunks:
307 lineiter = iter(chunk)
308 #first = True
309 try:
310 while 1:
311 line = lineiter.next()
312 if line['action'] != 'unmod':
313 nextline = lineiter.next()
314 if nextline['action'] == 'unmod' or \
315 nextline['action'] == line['action']:
316 continue
317 self.differ(line, nextline)
318 except StopIteration:
319 pass
320
321 return files
322
323 def prepare(self):
324 """
325 Prepare the passed udiff for HTML rendering. It'l return a list
326 of dicts
327 """
328 return self._parse_udiff()
329
330 def _safe_id(self, idstring):
331 """Make a string safe for including in an id attribute.
332
333 The HTML spec says that id attributes 'must begin with
334 a letter ([A-Za-z]) and may be followed by any number
335 of letters, digits ([0-9]), hyphens ("-"), underscores
336 ("_"), colons (":"), and periods (".")'. These regexps
337 are slightly over-zealous, in that they remove colons
338 and periods unnecessarily.
339
340 Whitespace is transformed into underscores, and then
341 anything which is not a hyphen or a character that
342 matches \w (alphanumerics and underscore) is removed.
343
344 """
345 # Transform all whitespace to underscore
346 idstring = re.sub(r'\s', "_", '%s' % idstring)
347 # Remove everything that is not a hyphen or a member of \w
348 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
349 return idstring
350
351 def raw_diff(self):
352 """
353 Returns raw string as udiff
354 """
355 udiff_copy = self.copy_iterator()
356 if self.__format == 'gitdiff':
357 udiff_copy = self._parse_gitdiff(udiff_copy)
358 return u''.join(udiff_copy)
359
360 def as_html(self, table_class='code-difftable', line_class='line',
361 new_lineno_class='lineno old', old_lineno_class='lineno new',
362 code_class='code'):
363 """
364 Return udiff as html table with customized css classes
365 """
366 def _link_to_if(condition, label, url):
367 """
368 Generates a link if condition is meet or just the label if not.
369 """
370
371 if condition:
372 return '''<a href="%(url)s">%(label)s</a>''' % {'url': url,
373 'label': label}
374 else:
375 return label
376 diff_lines = self.prepare()
377 _html_empty = True
378 _html = []
379 _html.append('''<table class="%(table_class)s">\n''' \
380 % {'table_class': table_class})
381 for diff in diff_lines:
382 for line in diff['chunks']:
383 _html_empty = False
384 for change in line:
385 _html.append('''<tr class="%(line_class)s %(action)s">\n''' \
386 % {'line_class': line_class,
387 'action': change['action']})
388 anchor_old_id = ''
389 anchor_new_id = ''
390 anchor_old = "%(filename)s_o%(oldline_no)s" % \
391 {'filename': self._safe_id(diff['filename']),
392 'oldline_no': change['old_lineno']}
393 anchor_new = "%(filename)s_n%(oldline_no)s" % \
394 {'filename': self._safe_id(diff['filename']),
395 'oldline_no': change['new_lineno']}
396 cond_old = change['old_lineno'] != '...' and \
397 change['old_lineno']
398 cond_new = change['new_lineno'] != '...' and \
399 change['new_lineno']
400 if cond_old:
401 anchor_old_id = 'id="%s"' % anchor_old
402 if cond_new:
403 anchor_new_id = 'id="%s"' % anchor_new
404 ###########################################################
405 # OLD LINE NUMBER
406 ###########################################################
407 _html.append('''\t<td %(a_id)s class="%(old_lineno_cls)s">''' \
408 % {'a_id': anchor_old_id,
409 'old_lineno_cls': old_lineno_class})
410
411 _html.append('''<pre>%(link)s</pre>''' \
412 % {'link':
413 _link_to_if(cond_old, change['old_lineno'], '#%s' \
414 % anchor_old)})
415 _html.append('''</td>\n''')
416 ###########################################################
417 # NEW LINE NUMBER
418 ###########################################################
419
420 _html.append('''\t<td %(a_id)s class="%(new_lineno_cls)s">''' \
421 % {'a_id': anchor_new_id,
422 'new_lineno_cls': new_lineno_class})
423
424 _html.append('''<pre>%(link)s</pre>''' \
425 % {'link':
426 _link_to_if(cond_new, change['new_lineno'], '#%s' \
427 % anchor_new)})
428 _html.append('''</td>\n''')
429 ###########################################################
430 # CODE
431 ###########################################################
432 _html.append('''\t<td class="%(code_class)s">''' \
433 % {'code_class': code_class})
434 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' \
435 % {'code': change['line']})
436 _html.append('''\t</td>''')
437 _html.append('''\n</tr>\n''')
438 _html.append('''</table>''')
439 if _html_empty:
440 return None
441 return ''.join(_html)
442
443 def stat(self):
444 """
445 Returns tuple of added, and removed lines for this instance
446 """
447 return self.adds, self.removes
@@ -1,302 +1,302
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2011 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 import logging
27 27 import traceback
28 28
29 29 from pylons import tmpl_context as c, url, request, response
30 30 from pylons.i18n.translation import _
31 31 from pylons.controllers.util import redirect
32 32 from pylons.decorators import jsonify
33 33
34 34 import rhodecode.lib.helpers as h
35 35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 36 from rhodecode.lib.base import BaseRepoController, render
37 37 from rhodecode.lib.utils import EmptyChangeset
38 38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib import diffs
39 40 from rhodecode.model.db import ChangesetComment
40 41 from rhodecode.model.comment import ChangesetCommentsModel
41 42
42 43 from vcs.exceptions import RepositoryError, ChangesetError, \
43 44 ChangesetDoesNotExistError
44 45 from vcs.nodes import FileNode
45 from vcs.utils import diffs as differ
46 46 from webob.exc import HTTPForbidden
47 47 from rhodecode.model.meta import Session
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class ChangesetController(BaseRepoController):
53 53
54 54 @LoginRequired()
55 55 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
56 56 'repository.admin')
57 57 def __before__(self):
58 58 super(ChangesetController, self).__before__()
59 59 c.affected_files_cut_off = 60
60 60
61 61 def index(self, revision):
62 62 ignore_whitespace = request.GET.get('ignorews') == '1'
63 63 def wrap_to_table(str):
64 64
65 65 return '''<table class="code-difftable">
66 66 <tr class="line">
67 67 <td class="lineno new"></td>
68 68 <td class="code"><pre>%s</pre></td>
69 69 </tr>
70 70 </table>''' % str
71 71
72 72 #get ranges of revisions if preset
73 73 rev_range = revision.split('...')[:2]
74 74
75 75 try:
76 76 if len(rev_range) == 2:
77 77 rev_start = rev_range[0]
78 78 rev_end = rev_range[1]
79 79 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
80 80 end=rev_end)
81 81 else:
82 82 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
83 83
84 84 c.cs_ranges = list(rev_ranges)
85 85 if not c.cs_ranges:
86 86 raise RepositoryError('Changeset range returned empty result')
87 87
88 88 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
89 89 log.error(traceback.format_exc())
90 90 h.flash(str(e), category='warning')
91 91 return redirect(url('home'))
92 92
93 93 c.changes = OrderedDict()
94 94 c.sum_added = 0
95 95 c.sum_removed = 0
96 96 c.lines_added = 0
97 97 c.lines_deleted = 0
98 98 c.cut_off = False # defines if cut off limit is reached
99 99
100 100 c.comments = []
101 101 c.inline_comments = []
102 102 c.inline_cnt = 0
103 103 # Iterate over ranges (default changeset view is always one changeset)
104 104 for changeset in c.cs_ranges:
105 105 c.comments.extend(ChangesetCommentsModel()\
106 106 .get_comments(c.rhodecode_db_repo.repo_id,
107 107 changeset.raw_id))
108 108 inlines = ChangesetCommentsModel()\
109 109 .get_inline_comments(c.rhodecode_db_repo.repo_id,
110 110 changeset.raw_id)
111 111 c.inline_comments.extend(inlines)
112 112 c.changes[changeset.raw_id] = []
113 113 try:
114 114 changeset_parent = changeset.parents[0]
115 115 except IndexError:
116 116 changeset_parent = None
117 117
118 118 #==================================================================
119 119 # ADDED FILES
120 120 #==================================================================
121 121 for node in changeset.added:
122 122
123 123 filenode_old = FileNode(node.path, '', EmptyChangeset())
124 124 if filenode_old.is_binary or node.is_binary:
125 125 diff = wrap_to_table(_('binary file'))
126 126 st = (0, 0)
127 127 else:
128 128 # in this case node.size is good parameter since those are
129 129 # added nodes and their size defines how many changes were
130 130 # made
131 131 c.sum_added += node.size
132 132 if c.sum_added < self.cut_off_limit:
133 f_gitdiff = differ.get_gitdiff(filenode_old, node,
133 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
134 134 ignore_whitespace=ignore_whitespace)
135 d = differ.DiffProcessor(f_gitdiff, format='gitdiff')
135 d = diffs.DiffProcessor(f_gitdiff, format='gitdiff')
136 136
137 137 st = d.stat()
138 138 diff = d.as_html()
139 139
140 140 else:
141 141 diff = wrap_to_table(_('Changeset is to big and '
142 142 'was cut off, see raw '
143 143 'changeset instead'))
144 144 c.cut_off = True
145 145 break
146 146
147 147 cs1 = None
148 148 cs2 = node.last_changeset.raw_id
149 149 c.lines_added += st[0]
150 150 c.lines_deleted += st[1]
151 151 c.changes[changeset.raw_id].append(('added', node, diff,
152 152 cs1, cs2, st))
153 153
154 154 #==================================================================
155 155 # CHANGED FILES
156 156 #==================================================================
157 157 if not c.cut_off:
158 158 for node in changeset.changed:
159 159 try:
160 160 filenode_old = changeset_parent.get_node(node.path)
161 161 except ChangesetError:
162 162 log.warning('Unable to fetch parent node for diff')
163 163 filenode_old = FileNode(node.path, '',
164 164 EmptyChangeset())
165 165
166 166 if filenode_old.is_binary or node.is_binary:
167 167 diff = wrap_to_table(_('binary file'))
168 168 st = (0, 0)
169 169 else:
170 170
171 171 if c.sum_removed < self.cut_off_limit:
172 f_gitdiff = differ.get_gitdiff(filenode_old, node,
172 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
173 173 ignore_whitespace=ignore_whitespace)
174 d = differ.DiffProcessor(f_gitdiff,
174 d = diffs.DiffProcessor(f_gitdiff,
175 175 format='gitdiff')
176 176 st = d.stat()
177 177 if (st[0] + st[1]) * 256 > self.cut_off_limit:
178 178 diff = wrap_to_table(_('Diff is to big '
179 179 'and was cut off, see '
180 180 'raw diff instead'))
181 181 else:
182 182 diff = d.as_html()
183 183
184 184 if diff:
185 185 c.sum_removed += len(diff)
186 186 else:
187 187 diff = wrap_to_table(_('Changeset is to big and '
188 188 'was cut off, see raw '
189 189 'changeset instead'))
190 190 c.cut_off = True
191 191 break
192 192
193 193 cs1 = filenode_old.last_changeset.raw_id
194 194 cs2 = node.last_changeset.raw_id
195 195 c.lines_added += st[0]
196 196 c.lines_deleted += st[1]
197 197 c.changes[changeset.raw_id].append(('changed', node, diff,
198 198 cs1, cs2, st))
199 199
200 200 #==================================================================
201 201 # REMOVED FILES
202 202 #==================================================================
203 203 if not c.cut_off:
204 204 for node in changeset.removed:
205 205 c.changes[changeset.raw_id].append(('removed', node, None,
206 206 None, None, (0, 0)))
207 207
208 208 # count inline comments
209 209 for path, lines in c.inline_comments:
210 210 for comments in lines.values():
211 211 c.inline_cnt += len(comments)
212 212
213 213 if len(c.cs_ranges) == 1:
214 214 c.changeset = c.cs_ranges[0]
215 215 c.changes = c.changes[c.changeset.raw_id]
216 216
217 217 return render('changeset/changeset.html')
218 218 else:
219 219 return render('changeset/changeset_range.html')
220 220
221 221 def raw_changeset(self, revision):
222 222
223 223 method = request.GET.get('diff', 'show')
224 224 ignore_whitespace = request.GET.get('ignorews') == '1'
225 225 try:
226 226 c.scm_type = c.rhodecode_repo.alias
227 227 c.changeset = c.rhodecode_repo.get_changeset(revision)
228 228 except RepositoryError:
229 229 log.error(traceback.format_exc())
230 230 return redirect(url('home'))
231 231 else:
232 232 try:
233 233 c.changeset_parent = c.changeset.parents[0]
234 234 except IndexError:
235 235 c.changeset_parent = None
236 236 c.changes = []
237 237
238 238 for node in c.changeset.added:
239 239 filenode_old = FileNode(node.path, '')
240 240 if filenode_old.is_binary or node.is_binary:
241 241 diff = _('binary file') + '\n'
242 242 else:
243 f_gitdiff = differ.get_gitdiff(filenode_old, node,
243 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
244 244 ignore_whitespace=ignore_whitespace)
245 diff = differ.DiffProcessor(f_gitdiff,
245 diff = diffs.DiffProcessor(f_gitdiff,
246 246 format='gitdiff').raw_diff()
247 247
248 248 cs1 = None
249 249 cs2 = node.last_changeset.raw_id
250 250 c.changes.append(('added', node, diff, cs1, cs2))
251 251
252 252 for node in c.changeset.changed:
253 253 filenode_old = c.changeset_parent.get_node(node.path)
254 254 if filenode_old.is_binary or node.is_binary:
255 255 diff = _('binary file')
256 256 else:
257 f_gitdiff = differ.get_gitdiff(filenode_old, node,
257 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
258 258 ignore_whitespace=ignore_whitespace)
259 diff = differ.DiffProcessor(f_gitdiff,
259 diff = diffs.DiffProcessor(f_gitdiff,
260 260 format='gitdiff').raw_diff()
261 261
262 262 cs1 = filenode_old.last_changeset.raw_id
263 263 cs2 = node.last_changeset.raw_id
264 264 c.changes.append(('changed', node, diff, cs1, cs2))
265 265
266 266 response.content_type = 'text/plain'
267 267
268 268 if method == 'download':
269 269 response.content_disposition = 'attachment; filename=%s.patch' \
270 270 % revision
271 271
272 272 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id for x in
273 273 c.changeset.parents])
274 274
275 275 c.diffs = ''
276 276 for x in c.changes:
277 277 c.diffs += x[2]
278 278
279 279 return render('changeset/raw_changeset.html')
280 280
281 281 def comment(self, repo_name, revision):
282 282 ChangesetCommentsModel().create(text=request.POST.get('text'),
283 283 repo_id=c.rhodecode_db_repo.repo_id,
284 284 user_id=c.rhodecode_user.user_id,
285 285 revision=revision,
286 286 f_path=request.POST.get('f_path'),
287 287 line_no=request.POST.get('line'))
288 288 Session.commit()
289 289 return redirect(h.url('changeset_home', repo_name=repo_name,
290 290 revision=revision))
291 291
292 292 @jsonify
293 293 def delete_comment(self, repo_name, comment_id):
294 294 co = ChangesetComment.get(comment_id)
295 295 owner = lambda : co.author.user_id == c.rhodecode_user.user_id
296 296 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
297 297 ChangesetCommentsModel().delete(comment=co)
298 298 Session.commit()
299 299 return True
300 300 else:
301 301 raise HTTPForbidden()
302 302
@@ -1,517 +1,518
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 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 logging
28 28 import traceback
29 29
30 30 from os.path import join as jn
31 31
32 32 from pylons import request, response, session, tmpl_context as c, url
33 33 from pylons.i18n.translation import _
34 34 from pylons.controllers.util import redirect
35 35 from pylons.decorators import jsonify
36 36
37 37 from vcs.conf import settings
38 38 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
39 39 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
40 40 from vcs.nodes import FileNode, NodeKind
41 from vcs.utils import diffs as differ
41
42 42
43 43 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
44 44 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 45 from rhodecode.lib.base import BaseRepoController, render
46 46 from rhodecode.lib.utils import EmptyChangeset
47 from rhodecode.lib import diffs
47 48 import rhodecode.lib.helpers as h
48 49 from rhodecode.model.repo import RepoModel
49 50
50 51 log = logging.getLogger(__name__)
51 52
52 53
53 54 class FilesController(BaseRepoController):
54 55
55 56 @LoginRequired()
56 57 def __before__(self):
57 58 super(FilesController, self).__before__()
58 59 c.cut_off_limit = self.cut_off_limit
59 60
60 61 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
61 62 """
62 63 Safe way to get changeset if error occur it redirects to tip with
63 64 proper message
64 65
65 66 :param rev: revision to fetch
66 67 :param repo_name: repo name to redirect after
67 68 """
68 69
69 70 try:
70 71 return c.rhodecode_repo.get_changeset(rev)
71 72 except EmptyRepositoryError, e:
72 73 if not redirect_after:
73 74 return None
74 75 url_ = url('files_add_home',
75 76 repo_name=c.repo_name,
76 77 revision=0, f_path='')
77 78 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
78 79 h.flash(h.literal(_('There are no files yet %s' % add_new)),
79 80 category='warning')
80 81 redirect(h.url('summary_home', repo_name=repo_name))
81 82
82 83 except RepositoryError, e:
83 84 h.flash(str(e), category='warning')
84 85 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
85 86
86 87 def __get_filenode_or_redirect(self, repo_name, cs, path):
87 88 """
88 89 Returns file_node, if error occurs or given path is directory,
89 90 it'll redirect to top level path
90 91
91 92 :param repo_name: repo_name
92 93 :param cs: given changeset
93 94 :param path: path to lookup
94 95 """
95 96
96 97 try:
97 98 file_node = cs.get_node(path)
98 99 if file_node.is_dir():
99 100 raise RepositoryError('given path is a directory')
100 101 except RepositoryError, e:
101 102 h.flash(str(e), category='warning')
102 103 redirect(h.url('files_home', repo_name=repo_name,
103 104 revision=cs.raw_id))
104 105
105 106 return file_node
106 107
107 108
108 109 def __get_paths(self, changeset, starting_path):
109 110 """recursive walk in root dir and return a set of all path in that dir
110 111 based on repository walk function
111 112 """
112 113 _files = list()
113 114 _dirs = list()
114 115
115 116 try:
116 117 tip = changeset
117 118 for topnode, dirs, files in tip.walk(starting_path):
118 119 for f in files:
119 120 _files.append(f.path)
120 121 for d in dirs:
121 122 _dirs.append(d.path)
122 123 except RepositoryError, e:
123 124 log.debug(traceback.format_exc())
124 125 pass
125 126 return _dirs, _files
126 127
127 128 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
128 129 'repository.admin')
129 130 def index(self, repo_name, revision, f_path):
130 131 #reditect to given revision from form if given
131 132 post_revision = request.POST.get('at_rev', None)
132 133 if post_revision:
133 134 cs = self.__get_cs_or_redirect(post_revision, repo_name)
134 135 redirect(url('files_home', repo_name=c.repo_name,
135 136 revision=cs.raw_id, f_path=f_path))
136 137
137 138 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
138 139 c.branch = request.GET.get('branch', None)
139 140 c.f_path = f_path
140 141
141 142 cur_rev = c.changeset.revision
142 143
143 144 #prev link
144 145 try:
145 146 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
146 147 c.url_prev = url('files_home', repo_name=c.repo_name,
147 148 revision=prev_rev.raw_id, f_path=f_path)
148 149 if c.branch:
149 150 c.url_prev += '?branch=%s' % c.branch
150 151 except (ChangesetDoesNotExistError, VCSError):
151 152 c.url_prev = '#'
152 153
153 154 #next link
154 155 try:
155 156 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
156 157 c.url_next = url('files_home', repo_name=c.repo_name,
157 158 revision=next_rev.raw_id, f_path=f_path)
158 159 if c.branch:
159 160 c.url_next += '?branch=%s' % c.branch
160 161 except (ChangesetDoesNotExistError, VCSError):
161 162 c.url_next = '#'
162 163
163 164 #files or dirs
164 165 try:
165 166 c.file = c.changeset.get_node(f_path)
166 167
167 168 if c.file.is_file():
168 169 c.file_history = self._get_node_history(c.changeset, f_path)
169 170 else:
170 171 c.file_history = []
171 172 except RepositoryError, e:
172 173 h.flash(str(e), category='warning')
173 174 redirect(h.url('files_home', repo_name=repo_name,
174 175 revision=revision))
175 176
176 177 return render('files/files.html')
177 178
178 179 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
179 180 'repository.admin')
180 181 def rawfile(self, repo_name, revision, f_path):
181 182 cs = self.__get_cs_or_redirect(revision, repo_name)
182 183 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
183 184
184 185 response.content_disposition = 'attachment; filename=%s' % \
185 186 safe_str(f_path.split(os.sep)[-1])
186 187
187 188 response.content_type = file_node.mimetype
188 189 return file_node.content
189 190
190 191 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
191 192 'repository.admin')
192 193 def raw(self, repo_name, revision, f_path):
193 194 cs = self.__get_cs_or_redirect(revision, repo_name)
194 195 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
195 196
196 197 raw_mimetype_mapping = {
197 198 # map original mimetype to a mimetype used for "show as raw"
198 199 # you can also provide a content-disposition to override the
199 200 # default "attachment" disposition.
200 201 # orig_type: (new_type, new_dispo)
201 202
202 203 # show images inline:
203 204 'image/x-icon': ('image/x-icon', 'inline'),
204 205 'image/png': ('image/png', 'inline'),
205 206 'image/gif': ('image/gif', 'inline'),
206 207 'image/jpeg': ('image/jpeg', 'inline'),
207 208 'image/svg+xml': ('image/svg+xml', 'inline'),
208 209 }
209 210
210 211 mimetype = file_node.mimetype
211 212 try:
212 213 mimetype, dispo = raw_mimetype_mapping[mimetype]
213 214 except KeyError:
214 215 # we don't know anything special about this, handle it safely
215 216 if file_node.is_binary:
216 217 # do same as download raw for binary files
217 218 mimetype, dispo = 'application/octet-stream', 'attachment'
218 219 else:
219 220 # do not just use the original mimetype, but force text/plain,
220 221 # otherwise it would serve text/html and that might be unsafe.
221 222 # Note: underlying vcs library fakes text/plain mimetype if the
222 223 # mimetype can not be determined and it thinks it is not
223 224 # binary.This might lead to erroneous text display in some
224 225 # cases, but helps in other cases, like with text files
225 226 # without extension.
226 227 mimetype, dispo = 'text/plain', 'inline'
227 228
228 229 if dispo == 'attachment':
229 230 dispo = 'attachment; filename=%s' % \
230 231 safe_str(f_path.split(os.sep)[-1])
231 232
232 233 response.content_disposition = dispo
233 234 response.content_type = mimetype
234 235 return file_node.content
235 236
236 237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
237 238 'repository.admin')
238 239 def annotate(self, repo_name, revision, f_path):
239 240 c.cs = self.__get_cs_or_redirect(revision, repo_name)
240 241 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
241 242
242 243 c.file_history = self._get_node_history(c.cs, f_path)
243 244 c.f_path = f_path
244 245 return render('files/files_annotate.html')
245 246
246 247 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
247 248 def edit(self, repo_name, revision, f_path):
248 249 r_post = request.POST
249 250
250 251 c.cs = self.__get_cs_or_redirect(revision, repo_name)
251 252 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
252 253
253 254 if c.file.is_binary:
254 255 return redirect(url('files_home', repo_name=c.repo_name,
255 256 revision=c.cs.raw_id, f_path=f_path))
256 257
257 258 c.f_path = f_path
258 259
259 260 if r_post:
260 261
261 262 old_content = c.file.content
262 263 sl = old_content.splitlines(1)
263 264 first_line = sl[0] if sl else ''
264 265 # modes: 0 - Unix, 1 - Mac, 2 - DOS
265 266 mode = detect_mode(first_line, 0)
266 267 content = convert_line_endings(r_post.get('content'), mode)
267 268
268 269 message = r_post.get('message') or (_('Edited %s via RhodeCode')
269 270 % (f_path))
270 271 author = self.rhodecode_user.full_contact
271 272
272 273 if content == old_content:
273 274 h.flash(_('No changes'),
274 275 category='warning')
275 276 return redirect(url('changeset_home', repo_name=c.repo_name,
276 277 revision='tip'))
277 278
278 279 try:
279 280 self.scm_model.commit_change(repo=c.rhodecode_repo,
280 281 repo_name=repo_name, cs=c.cs,
281 282 user=self.rhodecode_user,
282 283 author=author, message=message,
283 284 content=content, f_path=f_path)
284 285 h.flash(_('Successfully committed to %s' % f_path),
285 286 category='success')
286 287
287 288 except Exception:
288 289 log.error(traceback.format_exc())
289 290 h.flash(_('Error occurred during commit'), category='error')
290 291 return redirect(url('changeset_home',
291 292 repo_name=c.repo_name, revision='tip'))
292 293
293 294 return render('files/files_edit.html')
294 295
295 296 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
296 297 def add(self, repo_name, revision, f_path):
297 298 r_post = request.POST
298 299 c.cs = self.__get_cs_or_redirect(revision, repo_name,
299 300 redirect_after=False)
300 301 if c.cs is None:
301 302 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
302 303
303 304 c.f_path = f_path
304 305
305 306 if r_post:
306 307 unix_mode = 0
307 308 content = convert_line_endings(r_post.get('content'), unix_mode)
308 309
309 310 message = r_post.get('message') or (_('Added %s via RhodeCode')
310 311 % (f_path))
311 312 location = r_post.get('location')
312 313 filename = r_post.get('filename')
313 314 file_obj = r_post.get('upload_file', None)
314 315
315 316 if file_obj is not None and hasattr(file_obj, 'filename'):
316 317 filename = file_obj.filename
317 318 content = file_obj.file
318 319
319 320 node_path = os.path.join(location, filename)
320 321 author = self.rhodecode_user.full_contact
321 322
322 323 if not content:
323 324 h.flash(_('No content'), category='warning')
324 325 return redirect(url('changeset_home', repo_name=c.repo_name,
325 326 revision='tip'))
326 327 if not filename:
327 328 h.flash(_('No filename'), category='warning')
328 329 return redirect(url('changeset_home', repo_name=c.repo_name,
329 330 revision='tip'))
330 331
331 332 try:
332 333 self.scm_model.create_node(repo=c.rhodecode_repo,
333 334 repo_name=repo_name, cs=c.cs,
334 335 user=self.rhodecode_user,
335 336 author=author, message=message,
336 337 content=content, f_path=node_path)
337 338 h.flash(_('Successfully committed to %s' % node_path),
338 339 category='success')
339 340 except NodeAlreadyExistsError, e:
340 341 h.flash(_(e), category='error')
341 342 except Exception:
342 343 log.error(traceback.format_exc())
343 344 h.flash(_('Error occurred during commit'), category='error')
344 345 return redirect(url('changeset_home',
345 346 repo_name=c.repo_name, revision='tip'))
346 347
347 348 return render('files/files_add.html')
348 349
349 350 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
350 351 'repository.admin')
351 352 def archivefile(self, repo_name, fname):
352 353
353 354 fileformat = None
354 355 revision = None
355 356 ext = None
356 357 subrepos = request.GET.get('subrepos') == 'true'
357 358
358 359 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
359 360 archive_spec = fname.split(ext_data[1])
360 361 if len(archive_spec) == 2 and archive_spec[1] == '':
361 362 fileformat = a_type or ext_data[1]
362 363 revision = archive_spec[0]
363 364 ext = ext_data[1]
364 365
365 366 try:
366 367 dbrepo = RepoModel().get_by_repo_name(repo_name)
367 368 if dbrepo.enable_downloads is False:
368 369 return _('downloads disabled')
369 370
370 371 # patch and reset hooks section of UI config to not run any
371 372 # hooks on fetching archives with subrepos
372 373 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
373 374 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
374 375
375 376 cs = c.rhodecode_repo.get_changeset(revision)
376 377 content_type = settings.ARCHIVE_SPECS[fileformat][0]
377 378 except ChangesetDoesNotExistError:
378 379 return _('Unknown revision %s') % revision
379 380 except EmptyRepositoryError:
380 381 return _('Empty repository')
381 382 except (ImproperArchiveTypeError, KeyError):
382 383 return _('Unknown archive type')
383 384
384 385 response.content_type = content_type
385 386 response.content_disposition = 'attachment; filename=%s-%s%s' \
386 387 % (repo_name, revision, ext)
387 388
388 389 import tempfile
389 390 archive = tempfile.mkstemp()[1]
390 391 t = open(archive, 'wb')
391 392 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
392 393
393 394 def get_chunked_archive(archive):
394 395 stream = open(archive, 'rb')
395 396 while True:
396 397 data = stream.read(4096)
397 398 if not data:
398 399 os.remove(archive)
399 400 break
400 401 yield data
401 402
402 403 return get_chunked_archive(archive)
403 404
404 405 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
405 406 'repository.admin')
406 407 def diff(self, repo_name, f_path):
407 408 ignore_whitespace = request.GET.get('ignorews') == '1'
408 409 diff1 = request.GET.get('diff1')
409 410 diff2 = request.GET.get('diff2')
410 411 c.action = request.GET.get('diff')
411 412 c.no_changes = diff1 == diff2
412 413 c.f_path = f_path
413 414 c.big_diff = False
414 415
415 416 try:
416 417 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
417 418 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
418 419 node1 = c.changeset_1.get_node(f_path)
419 420 else:
420 421 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
421 422 node1 = FileNode('.', '', changeset=c.changeset_1)
422 423
423 424 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
424 425 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
425 426 node2 = c.changeset_2.get_node(f_path)
426 427 else:
427 428 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
428 429 node2 = FileNode('.', '', changeset=c.changeset_2)
429 430 except RepositoryError:
430 431 return redirect(url('files_home',
431 432 repo_name=c.repo_name, f_path=f_path))
432 433
433 434 if c.action == 'download':
434 _diff = differ.get_gitdiff(node1, node2,
435 _diff = diffs.get_gitdiff(node1, node2,
435 436 ignore_whitespace=ignore_whitespace)
436 diff = differ.DiffProcessor(_diff,format='gitdiff')
437 diff = diffs.DiffProcessor(_diff,format='gitdiff')
437 438
438 439 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
439 440 response.content_type = 'text/plain'
440 441 response.content_disposition = 'attachment; filename=%s' \
441 442 % diff_name
442 443 return diff.raw_diff()
443 444
444 445 elif c.action == 'raw':
445 _diff = differ.get_gitdiff(node1, node2,
446 _diff = diffs.get_gitdiff(node1, node2,
446 447 ignore_whitespace=ignore_whitespace)
447 diff = differ.DiffProcessor(_diff,format='gitdiff')
448 diff = diffs.DiffProcessor(_diff,format='gitdiff')
448 449 response.content_type = 'text/plain'
449 450 return diff.raw_diff()
450 451
451 452 elif c.action == 'diff':
452 453 if node1.is_binary or node2.is_binary:
453 454 c.cur_diff = _('Binary file')
454 455 elif node1.size > self.cut_off_limit or \
455 456 node2.size > self.cut_off_limit:
456 457 c.cur_diff = ''
457 458 c.big_diff = True
458 459 else:
459 _diff = differ.get_gitdiff(node1, node2,
460 _diff = diffs.get_gitdiff(node1, node2,
460 461 ignore_whitespace=ignore_whitespace)
461 diff = differ.DiffProcessor(_diff,format='gitdiff')
462 diff = diffs.DiffProcessor(_diff,format='gitdiff')
462 463 c.cur_diff = diff.as_html()
463 464 else:
464 465
465 466 #default option
466 467 if node1.is_binary or node2.is_binary:
467 468 c.cur_diff = _('Binary file')
468 469 elif node1.size > self.cut_off_limit or \
469 470 node2.size > self.cut_off_limit:
470 471 c.cur_diff = ''
471 472 c.big_diff = True
472 473
473 474 else:
474 _diff = differ.get_gitdiff(node1, node2,
475 _diff = diffs.get_gitdiff(node1, node2,
475 476 ignore_whitespace=ignore_whitespace)
476 diff = differ.DiffProcessor(_diff,format='gitdiff')
477 diff = diffs.DiffProcessor(_diff,format='gitdiff')
477 478 c.cur_diff = diff.as_html()
478 479
479 480 if not c.cur_diff and not c.big_diff:
480 481 c.no_changes = True
481 482 return render('files/file_diff.html')
482 483
483 484 def _get_node_history(self, cs, f_path):
484 485 changesets = cs.get_file_history(f_path)
485 486 hist_l = []
486 487
487 488 changesets_group = ([], _("Changesets"))
488 489 branches_group = ([], _("Branches"))
489 490 tags_group = ([], _("Tags"))
490 491
491 492 for chs in changesets:
492 493 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
493 494 changesets_group[0].append((chs.raw_id, n_desc,))
494 495
495 496 hist_l.append(changesets_group)
496 497
497 498 for name, chs in c.rhodecode_repo.branches.items():
498 499 #chs = chs.split(':')[-1]
499 500 branches_group[0].append((chs, name),)
500 501 hist_l.append(branches_group)
501 502
502 503 for name, chs in c.rhodecode_repo.tags.items():
503 504 #chs = chs.split(':')[-1]
504 505 tags_group[0].append((chs, name),)
505 506 hist_l.append(tags_group)
506 507
507 508 return hist_l
508 509
509 510 @jsonify
510 511 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
511 512 'repository.admin')
512 513 def nodelist(self, repo_name, revision, f_path):
513 514 if request.environ.get('HTTP_X_PARTIAL_XHR'):
514 515 cs = self.__get_cs_or_redirect(revision, repo_name)
515 516 _d, _f = self.__get_paths(cs, f_path)
516 517 return _d + _f
517 518
@@ -1,677 +1,677
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11
12 12 from datetime import datetime
13 13 from pygments.formatters.html import HtmlFormatter
14 14 from pygments import highlight as code_highlight
15 15 from pylons import url, request, config
16 16 from pylons.i18n.translation import _, ungettext
17 17
18 18 from webhelpers.html import literal, HTML, escape
19 19 from webhelpers.html.tools import *
20 20 from webhelpers.html.builder import make_tag
21 21 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
22 22 end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
23 23 link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
24 24 password, textarea, title, ul, xml_declaration, radio
25 25 from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
26 26 mail_to, strip_links, strip_tags, tag_re
27 27 from webhelpers.number import format_byte_size, format_bit_size
28 28 from webhelpers.pylonslib import Flash as _Flash
29 29 from webhelpers.pylonslib.secure_form import secure_form
30 30 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
31 31 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
32 32 replace_whitespace, urlify, truncate, wrap_paragraphs
33 33 from webhelpers.date import time_ago_in_words
34 34 from webhelpers.paginate import Page
35 35 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
36 36 convert_boolean_attrs, NotGiven, _make_safe_id_component
37 37
38 from vcs.utils.annotate import annotate_highlight
38 from rhodecode.lib.annotate import annotate_highlight
39 39 from rhodecode.lib.utils import repo_name_slug
40 40 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
41 41
42 42 from rhodecode.lib.markup_renderer import MarkupRenderer
43 43
44 44 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
45 45 """
46 46 Reset button
47 47 """
48 48 _set_input_attrs(attrs, type, name, value)
49 49 _set_id_attr(attrs, id, name)
50 50 convert_boolean_attrs(attrs, ["disabled"])
51 51 return HTML.input(**attrs)
52 52
53 53 reset = _reset
54 54 safeid = _make_safe_id_component
55 55
56 56 def get_token():
57 57 """Return the current authentication token, creating one if one doesn't
58 58 already exist.
59 59 """
60 60 token_key = "_authentication_token"
61 61 from pylons import session
62 62 if not token_key in session:
63 63 try:
64 64 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
65 65 except AttributeError: # Python < 2.4
66 66 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
67 67 session[token_key] = token
68 68 if hasattr(session, 'save'):
69 69 session.save()
70 70 return session[token_key]
71 71
72 72 class _GetError(object):
73 73 """Get error from form_errors, and represent it as span wrapped error
74 74 message
75 75
76 76 :param field_name: field to fetch errors for
77 77 :param form_errors: form errors dict
78 78 """
79 79
80 80 def __call__(self, field_name, form_errors):
81 81 tmpl = """<span class="error_msg">%s</span>"""
82 82 if form_errors and form_errors.has_key(field_name):
83 83 return literal(tmpl % form_errors.get(field_name))
84 84
85 85 get_error = _GetError()
86 86
87 87 class _ToolTip(object):
88 88
89 89 def __call__(self, tooltip_title, trim_at=50):
90 90 """Special function just to wrap our text into nice formatted
91 91 autowrapped text
92 92
93 93 :param tooltip_title:
94 94 """
95 95 return escape(tooltip_title)
96 96 tooltip = _ToolTip()
97 97
98 98 class _FilesBreadCrumbs(object):
99 99
100 100 def __call__(self, repo_name, rev, paths):
101 101 if isinstance(paths, str):
102 102 paths = safe_unicode(paths)
103 103 url_l = [link_to(repo_name, url('files_home',
104 104 repo_name=repo_name,
105 105 revision=rev, f_path=''))]
106 106 paths_l = paths.split('/')
107 107 for cnt, p in enumerate(paths_l):
108 108 if p != '':
109 109 url_l.append(link_to(p, url('files_home',
110 110 repo_name=repo_name,
111 111 revision=rev,
112 112 f_path='/'.join(paths_l[:cnt + 1]))))
113 113
114 114 return literal('/'.join(url_l))
115 115
116 116 files_breadcrumbs = _FilesBreadCrumbs()
117 117
118 118 class CodeHtmlFormatter(HtmlFormatter):
119 119 """My code Html Formatter for source codes
120 120 """
121 121
122 122 def wrap(self, source, outfile):
123 123 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
124 124
125 125 def _wrap_code(self, source):
126 126 for cnt, it in enumerate(source):
127 127 i, t = it
128 128 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
129 129 yield i, t
130 130
131 131 def _wrap_tablelinenos(self, inner):
132 132 dummyoutfile = StringIO.StringIO()
133 133 lncount = 0
134 134 for t, line in inner:
135 135 if t:
136 136 lncount += 1
137 137 dummyoutfile.write(line)
138 138
139 139 fl = self.linenostart
140 140 mw = len(str(lncount + fl - 1))
141 141 sp = self.linenospecial
142 142 st = self.linenostep
143 143 la = self.lineanchors
144 144 aln = self.anchorlinenos
145 145 nocls = self.noclasses
146 146 if sp:
147 147 lines = []
148 148
149 149 for i in range(fl, fl + lncount):
150 150 if i % st == 0:
151 151 if i % sp == 0:
152 152 if aln:
153 153 lines.append('<a href="#%s%d" class="special">%*d</a>' %
154 154 (la, i, mw, i))
155 155 else:
156 156 lines.append('<span class="special">%*d</span>' % (mw, i))
157 157 else:
158 158 if aln:
159 159 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
160 160 else:
161 161 lines.append('%*d' % (mw, i))
162 162 else:
163 163 lines.append('')
164 164 ls = '\n'.join(lines)
165 165 else:
166 166 lines = []
167 167 for i in range(fl, fl + lncount):
168 168 if i % st == 0:
169 169 if aln:
170 170 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
171 171 else:
172 172 lines.append('%*d' % (mw, i))
173 173 else:
174 174 lines.append('')
175 175 ls = '\n'.join(lines)
176 176
177 177 # in case you wonder about the seemingly redundant <div> here: since the
178 178 # content in the other cell also is wrapped in a div, some browsers in
179 179 # some configurations seem to mess up the formatting...
180 180 if nocls:
181 181 yield 0, ('<table class="%stable">' % self.cssclass +
182 182 '<tr><td><div class="linenodiv" '
183 183 'style="background-color: #f0f0f0; padding-right: 10px">'
184 184 '<pre style="line-height: 125%">' +
185 185 ls + '</pre></div></td><td id="hlcode" class="code">')
186 186 else:
187 187 yield 0, ('<table class="%stable">' % self.cssclass +
188 188 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
189 189 ls + '</pre></div></td><td id="hlcode" class="code">')
190 190 yield 0, dummyoutfile.getvalue()
191 191 yield 0, '</td></tr></table>'
192 192
193 193
194 194 def pygmentize(filenode, **kwargs):
195 195 """pygmentize function using pygments
196 196
197 197 :param filenode:
198 198 """
199 199
200 200 return literal(code_highlight(filenode.content,
201 201 filenode.lexer, CodeHtmlFormatter(**kwargs)))
202 202
203 203 def pygmentize_annotation(repo_name, filenode, **kwargs):
204 204 """pygmentize function for annotation
205 205
206 206 :param filenode:
207 207 """
208 208
209 209 color_dict = {}
210 210 def gen_color(n=10000):
211 211 """generator for getting n of evenly distributed colors using
212 212 hsv color and golden ratio. It always return same order of colors
213 213
214 214 :returns: RGB tuple
215 215 """
216 216
217 217 def hsv_to_rgb(h, s, v):
218 218 if s == 0.0: return v, v, v
219 219 i = int(h * 6.0) # XXX assume int() truncates!
220 220 f = (h * 6.0) - i
221 221 p = v * (1.0 - s)
222 222 q = v * (1.0 - s * f)
223 223 t = v * (1.0 - s * (1.0 - f))
224 224 i = i % 6
225 225 if i == 0: return v, t, p
226 226 if i == 1: return q, v, p
227 227 if i == 2: return p, v, t
228 228 if i == 3: return p, q, v
229 229 if i == 4: return t, p, v
230 230 if i == 5: return v, p, q
231 231
232 232 golden_ratio = 0.618033988749895
233 233 h = 0.22717784590367374
234 234
235 235 for _ in xrange(n):
236 236 h += golden_ratio
237 237 h %= 1
238 238 HSV_tuple = [h, 0.95, 0.95]
239 239 RGB_tuple = hsv_to_rgb(*HSV_tuple)
240 240 yield map(lambda x:str(int(x * 256)), RGB_tuple)
241 241
242 242 cgenerator = gen_color()
243 243
244 244 def get_color_string(cs):
245 245 if color_dict.has_key(cs):
246 246 col = color_dict[cs]
247 247 else:
248 248 col = color_dict[cs] = cgenerator.next()
249 249 return "color: rgb(%s)! important;" % (', '.join(col))
250 250
251 251 def url_func(repo_name):
252 252
253 253 def _url_func(changeset):
254 254 author = changeset.author
255 255 date = changeset.date
256 256 message = tooltip(changeset.message)
257 257
258 258 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
259 259 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
260 260 "</b> %s<br/></div>")
261 261
262 262 tooltip_html = tooltip_html % (author, date, message)
263 263 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
264 264 short_id(changeset.raw_id))
265 265 uri = link_to(
266 266 lnk_format,
267 267 url('changeset_home', repo_name=repo_name,
268 268 revision=changeset.raw_id),
269 269 style=get_color_string(changeset.raw_id),
270 270 class_='tooltip',
271 271 title=tooltip_html
272 272 )
273 273
274 274 uri += '\n'
275 275 return uri
276 276 return _url_func
277 277
278 278 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
279 279
280 280 def is_following_repo(repo_name, user_id):
281 281 from rhodecode.model.scm import ScmModel
282 282 return ScmModel().is_following_repo(repo_name, user_id)
283 283
284 284 flash = _Flash()
285 285
286 286 #==============================================================================
287 287 # SCM FILTERS available via h.
288 288 #==============================================================================
289 289 from vcs.utils import author_name, author_email
290 290 from rhodecode.lib import credentials_filter, age as _age
291 291
292 292 age = lambda x:_age(x)
293 293 capitalize = lambda x: x.capitalize()
294 294 email = author_email
295 295 email_or_none = lambda x: email(x) if email(x) != x else None
296 296 person = lambda x: author_name(x)
297 297 short_id = lambda x: x[:12]
298 298 hide_credentials = lambda x: ''.join(credentials_filter(x))
299 299
300 300 def bool2icon(value):
301 301 """Returns True/False values represented as small html image of true/false
302 302 icons
303 303
304 304 :param value: bool value
305 305 """
306 306
307 307 if value is True:
308 308 return HTML.tag('img', src=url("/images/icons/accept.png"),
309 309 alt=_('True'))
310 310
311 311 if value is False:
312 312 return HTML.tag('img', src=url("/images/icons/cancel.png"),
313 313 alt=_('False'))
314 314
315 315 return value
316 316
317 317
318 318 def action_parser(user_log, feed=False):
319 319 """This helper will action_map the specified string action into translated
320 320 fancy names with icons and links
321 321
322 322 :param user_log: user log instance
323 323 :param feed: use output for feeds (no html and fancy icons)
324 324 """
325 325
326 326 action = user_log.action
327 327 action_params = ' '
328 328
329 329 x = action.split(':')
330 330
331 331 if len(x) > 1:
332 332 action, action_params = x
333 333
334 334 def get_cs_links():
335 335 revs_limit = 3 #display this amount always
336 336 revs_top_limit = 50 #show upto this amount of changesets hidden
337 337 revs = action_params.split(',')
338 338 repo_name = user_log.repository.repo_name
339 339
340 340 from rhodecode.model.scm import ScmModel
341 341 repo = user_log.repository.scm_instance
342 342
343 343 message = lambda rev: get_changeset_safe(repo, rev).message
344 344 cs_links = []
345 345 cs_links.append(" " + ', '.join ([link_to(rev,
346 346 url('changeset_home',
347 347 repo_name=repo_name,
348 348 revision=rev), title=tooltip(message(rev)),
349 349 class_='tooltip') for rev in revs[:revs_limit] ]))
350 350
351 351 compare_view = (' <div class="compare_view tooltip" title="%s">'
352 352 '<a href="%s">%s</a> '
353 353 '</div>' % (_('Show all combined changesets %s->%s' \
354 354 % (revs[0], revs[-1])),
355 355 url('changeset_home', repo_name=repo_name,
356 356 revision='%s...%s' % (revs[0], revs[-1])
357 357 ),
358 358 _('compare view'))
359 359 )
360 360
361 361 if len(revs) > revs_limit:
362 362 uniq_id = revs[0]
363 363 html_tmpl = ('<span> %s '
364 364 '<a class="show_more" id="_%s" href="#more">%s</a> '
365 365 '%s</span>')
366 366 if not feed:
367 367 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
368 368 % (len(revs) - revs_limit),
369 369 _('revisions')))
370 370
371 371 if not feed:
372 372 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
373 373 else:
374 374 html_tmpl = '<span id="%s"> %s </span>'
375 375
376 376 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
377 377 url('changeset_home',
378 378 repo_name=repo_name, revision=rev),
379 379 title=message(rev), class_='tooltip')
380 380 for rev in revs[revs_limit:revs_top_limit]])))
381 381 if len(revs) > 1:
382 382 cs_links.append(compare_view)
383 383 return ''.join(cs_links)
384 384
385 385 def get_fork_name():
386 386 repo_name = action_params
387 387 return _('fork name ') + str(link_to(action_params, url('summary_home',
388 388 repo_name=repo_name,)))
389 389
390 390 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
391 391 'user_created_repo':(_('[created] repository'), None),
392 392 'user_created_fork':(_('[created] repository as fork'), None),
393 393 'user_forked_repo':(_('[forked] repository'), get_fork_name),
394 394 'user_updated_repo':(_('[updated] repository'), None),
395 395 'admin_deleted_repo':(_('[delete] repository'), None),
396 396 'admin_created_repo':(_('[created] repository'), None),
397 397 'admin_forked_repo':(_('[forked] repository'), None),
398 398 'admin_updated_repo':(_('[updated] repository'), None),
399 399 'push':(_('[pushed] into'), get_cs_links),
400 400 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
401 401 'push_remote':(_('[pulled from remote] into'), get_cs_links),
402 402 'pull':(_('[pulled] from'), None),
403 403 'started_following_repo':(_('[started following] repository'), None),
404 404 'stopped_following_repo':(_('[stopped following] repository'), None),
405 405 }
406 406
407 407 action_str = action_map.get(action, action)
408 408 if feed:
409 409 action = action_str[0].replace('[', '').replace(']', '')
410 410 else:
411 411 action = action_str[0].replace('[', '<span class="journal_highlight">')\
412 412 .replace(']', '</span>')
413 413
414 414 action_params_func = lambda :""
415 415
416 416 if callable(action_str[1]):
417 417 action_params_func = action_str[1]
418 418
419 419 return [literal(action), action_params_func]
420 420
421 421 def action_parser_icon(user_log):
422 422 action = user_log.action
423 423 action_params = None
424 424 x = action.split(':')
425 425
426 426 if len(x) > 1:
427 427 action, action_params = x
428 428
429 429 tmpl = """<img src="%s%s" alt="%s"/>"""
430 430 map = {'user_deleted_repo':'database_delete.png',
431 431 'user_created_repo':'database_add.png',
432 432 'user_created_fork':'arrow_divide.png',
433 433 'user_forked_repo':'arrow_divide.png',
434 434 'user_updated_repo':'database_edit.png',
435 435 'admin_deleted_repo':'database_delete.png',
436 436 'admin_created_repo':'database_add.png',
437 437 'admin_forked_repo':'arrow_divide.png',
438 438 'admin_updated_repo':'database_edit.png',
439 439 'push':'script_add.png',
440 440 'push_local':'script_edit.png',
441 441 'push_remote':'connect.png',
442 442 'pull':'down_16.png',
443 443 'started_following_repo':'heart_add.png',
444 444 'stopped_following_repo':'heart_delete.png',
445 445 }
446 446 return literal(tmpl % ((url('/images/icons/')),
447 447 map.get(action, action), action))
448 448
449 449
450 450 #==============================================================================
451 451 # PERMS
452 452 #==============================================================================
453 453 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
454 454 HasRepoPermissionAny, HasRepoPermissionAll
455 455
456 456 #==============================================================================
457 457 # GRAVATAR URL
458 458 #==============================================================================
459 459
460 460 def gravatar_url(email_address, size=30):
461 461 if (not str2bool(config['app_conf'].get('use_gravatar')) or
462 462 not email_address or email_address == 'anonymous@rhodecode.org'):
463 463 return url("/images/user%s.png" % size)
464 464
465 465 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
466 466 default = 'identicon'
467 467 baseurl_nossl = "http://www.gravatar.com/avatar/"
468 468 baseurl_ssl = "https://secure.gravatar.com/avatar/"
469 469 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
470 470
471 471 if isinstance(email_address, unicode):
472 472 #hashlib crashes on unicode items
473 473 email_address = safe_str(email_address)
474 474 # construct the url
475 475 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
476 476 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
477 477
478 478 return gravatar_url
479 479
480 480
481 481 #==============================================================================
482 482 # REPO PAGER, PAGER FOR REPOSITORY
483 483 #==============================================================================
484 484 class RepoPage(Page):
485 485
486 486 def __init__(self, collection, page=1, items_per_page=20,
487 487 item_count=None, url=None, **kwargs):
488 488
489 489 """Create a "RepoPage" instance. special pager for paging
490 490 repository
491 491 """
492 492 self._url_generator = url
493 493
494 494 # Safe the kwargs class-wide so they can be used in the pager() method
495 495 self.kwargs = kwargs
496 496
497 497 # Save a reference to the collection
498 498 self.original_collection = collection
499 499
500 500 self.collection = collection
501 501
502 502 # The self.page is the number of the current page.
503 503 # The first page has the number 1!
504 504 try:
505 505 self.page = int(page) # make it int() if we get it as a string
506 506 except (ValueError, TypeError):
507 507 self.page = 1
508 508
509 509 self.items_per_page = items_per_page
510 510
511 511 # Unless the user tells us how many items the collections has
512 512 # we calculate that ourselves.
513 513 if item_count is not None:
514 514 self.item_count = item_count
515 515 else:
516 516 self.item_count = len(self.collection)
517 517
518 518 # Compute the number of the first and last available page
519 519 if self.item_count > 0:
520 520 self.first_page = 1
521 521 self.page_count = int(math.ceil(float(self.item_count) /
522 522 self.items_per_page))
523 523 self.last_page = self.first_page + self.page_count - 1
524 524
525 525 # Make sure that the requested page number is the range of valid pages
526 526 if self.page > self.last_page:
527 527 self.page = self.last_page
528 528 elif self.page < self.first_page:
529 529 self.page = self.first_page
530 530
531 531 # Note: the number of items on this page can be less than
532 532 # items_per_page if the last page is not full
533 533 self.first_item = max(0, (self.item_count) - (self.page *
534 534 items_per_page))
535 535 self.last_item = ((self.item_count - 1) - items_per_page *
536 536 (self.page - 1))
537 537
538 538 self.items = list(self.collection[self.first_item:self.last_item + 1])
539 539
540 540
541 541 # Links to previous and next page
542 542 if self.page > self.first_page:
543 543 self.previous_page = self.page - 1
544 544 else:
545 545 self.previous_page = None
546 546
547 547 if self.page < self.last_page:
548 548 self.next_page = self.page + 1
549 549 else:
550 550 self.next_page = None
551 551
552 552 # No items available
553 553 else:
554 554 self.first_page = None
555 555 self.page_count = 0
556 556 self.last_page = None
557 557 self.first_item = None
558 558 self.last_item = None
559 559 self.previous_page = None
560 560 self.next_page = None
561 561 self.items = []
562 562
563 563 # This is a subclass of the 'list' type. Initialise the list now.
564 564 list.__init__(self, reversed(self.items))
565 565
566 566
567 567 def changed_tooltip(nodes):
568 568 """
569 569 Generates a html string for changed nodes in changeset page.
570 570 It limits the output to 30 entries
571 571
572 572 :param nodes: LazyNodesGenerator
573 573 """
574 574 if nodes:
575 575 pref = ': <br/> '
576 576 suf = ''
577 577 if len(nodes) > 30:
578 578 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
579 579 return literal(pref + '<br/> '.join([safe_unicode(x.path)
580 580 for x in nodes[:30]]) + suf)
581 581 else:
582 582 return ': ' + _('No Files')
583 583
584 584
585 585
586 586 def repo_link(groups_and_repos):
587 587 """
588 588 Makes a breadcrumbs link to repo within a group
589 589 joins &raquo; on each group to create a fancy link
590 590
591 591 ex::
592 592 group >> subgroup >> repo
593 593
594 594 :param groups_and_repos:
595 595 """
596 596 groups, repo_name = groups_and_repos
597 597
598 598 if not groups:
599 599 return repo_name
600 600 else:
601 601 def make_link(group):
602 602 return link_to(group.name, url('repos_group_home',
603 603 group_name=group.group_name))
604 604 return literal(' &raquo; '.join(map(make_link, groups)) + \
605 605 " &raquo; " + repo_name)
606 606
607 607 def fancy_file_stats(stats):
608 608 """
609 609 Displays a fancy two colored bar for number of added/deleted
610 610 lines of code on file
611 611
612 612 :param stats: two element list of added/deleted lines of code
613 613 """
614 614
615 615 a, d, t = stats[0], stats[1], stats[0] + stats[1]
616 616 width = 100
617 617 unit = float(width) / (t or 1)
618 618
619 619 # needs > 9% of width to be visible or 0 to be hidden
620 620 a_p = max(9, unit * a) if a > 0 else 0
621 621 d_p = max(9, unit * d) if d > 0 else 0
622 622 p_sum = a_p + d_p
623 623
624 624 if p_sum > width:
625 625 #adjust the percentage to be == 100% since we adjusted to 9
626 626 if a_p > d_p:
627 627 a_p = a_p - (p_sum - width)
628 628 else:
629 629 d_p = d_p - (p_sum - width)
630 630
631 631 a_v = a if a > 0 else ''
632 632 d_v = d if d > 0 else ''
633 633
634 634
635 635 def cgen(l_type):
636 636 mapping = {'tr':'top-right-rounded-corner',
637 637 'tl':'top-left-rounded-corner',
638 638 'br':'bottom-right-rounded-corner',
639 639 'bl':'bottom-left-rounded-corner'}
640 640 map_getter = lambda x:mapping[x]
641 641
642 642 if l_type == 'a' and d_v:
643 643 #case when added and deleted are present
644 644 return ' '.join(map(map_getter, ['tl', 'bl']))
645 645
646 646 if l_type == 'a' and not d_v:
647 647 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
648 648
649 649 if l_type == 'd' and a_v:
650 650 return ' '.join(map(map_getter, ['tr', 'br']))
651 651
652 652 if l_type == 'd' and not a_v:
653 653 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
654 654
655 655
656 656
657 657 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
658 658 a_p, a_v)
659 659 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
660 660 d_p, d_v)
661 661 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
662 662
663 663
664 664 def urlify_text(text):
665 665 import re
666 666
667 667 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
668 668
669 669 def url_func(match_obj):
670 670 url_full = match_obj.groups()[0]
671 671 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
672 672
673 673 return literal(url_pat.sub(url_func, text))
674 674
675 675
676 676 def rst(source):
677 677 return literal('<div class="rst-block">%s</div>' % MarkupRenderer.rst(source))
@@ -1,601 +1,600
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) 2009-2011 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 logging
28 28 import datetime
29 29 import traceback
30 30 import paste
31 31 import beaker
32 32 import tarfile
33 33 import shutil
34 34 from os.path import abspath
35 35 from os.path import dirname as dn, join as jn
36 36
37 37 from paste.script.command import Command, BadCommand
38 38
39 39 from mercurial import ui, config
40 40
41 41 from webhelpers.text import collapse, remove_formatting, strip_tags
42 42
43 43 from vcs import get_backend
44 44 from vcs.backends.base import BaseChangeset
45 45 from vcs.utils.lazy import LazyProperty
46 46 from vcs.utils.helpers import get_scm
47 47 from vcs.exceptions import VCSError
48 48
49 49 from rhodecode.lib.caching_query import FromCache
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
53 53 UserLog, RepoGroup, RhodeCodeSetting
54 54 from rhodecode.model.meta import Session
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def recursive_replace(str_, replace=' '):
60 60 """Recursive replace of given sign to just one instance
61 61
62 62 :param str_: given string
63 63 :param replace: char to find and replace multiple instances
64 64
65 65 Examples::
66 66 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
67 67 'Mighty-Mighty-Bo-sstones'
68 68 """
69 69
70 70 if str_.find(replace * 2) == -1:
71 71 return str_
72 72 else:
73 73 str_ = str_.replace(replace * 2, replace)
74 74 return recursive_replace(str_, replace)
75 75
76 76
77 77 def repo_name_slug(value):
78 78 """Return slug of name of repository
79 79 This function is called on each creation/modification
80 80 of repository to prevent bad names in repo
81 81 """
82 82
83 83 slug = remove_formatting(value)
84 84 slug = strip_tags(slug)
85 85
86 86 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
87 87 slug = slug.replace(c, '-')
88 88 slug = recursive_replace(slug, '-')
89 89 slug = collapse(slug, '-')
90 90 return slug
91 91
92 92
93 93 def get_repo_slug(request):
94 94 return request.environ['pylons.routes_dict'].get('repo_name')
95 95
96 96
97 97 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
98 98 """
99 99 Action logger for various actions made by users
100 100
101 101 :param user: user that made this action, can be a unique username string or
102 102 object containing user_id attribute
103 103 :param action: action to log, should be on of predefined unique actions for
104 104 easy translations
105 105 :param repo: string name of repository or object containing repo_id,
106 106 that action was made on
107 107 :param ipaddr: optional ip address from what the action was made
108 108 :param sa: optional sqlalchemy session
109 109
110 110 """
111 111
112 112 if not sa:
113 113 sa = meta.Session
114 114
115 115 try:
116 116 if hasattr(user, 'user_id'):
117 117 user_obj = user
118 118 elif isinstance(user, basestring):
119 119 user_obj = User.get_by_username(user)
120 120 else:
121 121 raise Exception('You have to provide user object or username')
122 122
123 123 if hasattr(repo, 'repo_id'):
124 124 repo_obj = Repository.get(repo.repo_id)
125 125 repo_name = repo_obj.repo_name
126 126 elif isinstance(repo, basestring):
127 127 repo_name = repo.lstrip('/')
128 128 repo_obj = Repository.get_by_repo_name(repo_name)
129 129 else:
130 130 raise Exception('You have to provide repository to action logger')
131 131
132 132 user_log = UserLog()
133 133 user_log.user_id = user_obj.user_id
134 134 user_log.action = action
135 135
136 136 user_log.repository_id = repo_obj.repo_id
137 137 user_log.repository_name = repo_name
138 138
139 139 user_log.action_date = datetime.datetime.now()
140 140 user_log.user_ip = ipaddr
141 141 sa.add(user_log)
142 142
143 143 log.info('Adding user %s, action %s on %s', user_obj, action, repo)
144 144 if commit:
145 145 sa.commit()
146 146 except:
147 147 log.error(traceback.format_exc())
148 148 raise
149 149
150 150
151 151 def get_repos(path, recursive=False):
152 152 """
153 153 Scans given path for repos and return (name,(type,path)) tuple
154 154
155 155 :param path: path to scann for repositories
156 156 :param recursive: recursive search and return names with subdirs in front
157 157 """
158 158
159 159 if path.endswith(os.sep):
160 160 #remove ending slash for better results
161 161 path = path[:-1]
162 162
163 163 def _get_repos(p):
164 164 if not os.access(p, os.W_OK):
165 165 return
166 166 for dirpath in os.listdir(p):
167 167 if os.path.isfile(os.path.join(p, dirpath)):
168 168 continue
169 169 cur_path = os.path.join(p, dirpath)
170 170 try:
171 171 scm_info = get_scm(cur_path)
172 172 yield scm_info[1].split(path)[-1].lstrip(os.sep), scm_info
173 173 except VCSError:
174 174 if not recursive:
175 175 continue
176 176 #check if this dir containts other repos for recursive scan
177 177 rec_path = os.path.join(p, dirpath)
178 178 if os.path.isdir(rec_path):
179 179 for inner_scm in _get_repos(rec_path):
180 180 yield inner_scm
181 181
182 182 return _get_repos(path)
183 183
184 184
185 185 def is_valid_repo(repo_name, base_path):
186 186 """
187 187 Returns True if given path is a valid repository False otherwise
188 188 :param repo_name:
189 189 :param base_path:
190 190
191 191 :return True: if given path is a valid repository
192 192 """
193 193 full_path = os.path.join(base_path, repo_name)
194 194
195 195 try:
196 196 get_scm(full_path)
197 197 return True
198 198 except VCSError:
199 199 return False
200 200
201 201 def is_valid_repos_group(repos_group_name, base_path):
202 202 """
203 203 Returns True if given path is a repos group False otherwise
204 204
205 205 :param repo_name:
206 206 :param base_path:
207 207 """
208 208 full_path = os.path.join(base_path, repos_group_name)
209 209
210 210 # check if it's not a repo
211 211 if is_valid_repo(repos_group_name, base_path):
212 212 return False
213 213
214 214 # check if it's a valid path
215 215 if os.path.isdir(full_path):
216 216 return True
217 217
218 218 return False
219 219
220 220 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
221 221 while True:
222 222 ok = raw_input(prompt)
223 223 if ok in ('y', 'ye', 'yes'):
224 224 return True
225 225 if ok in ('n', 'no', 'nop', 'nope'):
226 226 return False
227 227 retries = retries - 1
228 228 if retries < 0:
229 229 raise IOError
230 230 print complaint
231 231
232 232 #propagated from mercurial documentation
233 233 ui_sections = ['alias', 'auth',
234 234 'decode/encode', 'defaults',
235 235 'diff', 'email',
236 236 'extensions', 'format',
237 237 'merge-patterns', 'merge-tools',
238 238 'hooks', 'http_proxy',
239 239 'smtp', 'patch',
240 240 'paths', 'profiling',
241 241 'server', 'trusted',
242 242 'ui', 'web', ]
243 243
244 244
245 245 def make_ui(read_from='file', path=None, checkpaths=True):
246 246 """A function that will read python rc files or database
247 247 and make an mercurial ui object from read options
248 248
249 249 :param path: path to mercurial config file
250 250 :param checkpaths: check the path
251 251 :param read_from: read from 'file' or 'db'
252 252 """
253 253
254 254 baseui = ui.ui()
255 255
256 256 #clean the baseui object
257 257 baseui._ocfg = config.config()
258 258 baseui._ucfg = config.config()
259 259 baseui._tcfg = config.config()
260 260
261 261 if read_from == 'file':
262 262 if not os.path.isfile(path):
263 263 log.warning('Unable to read config file %s' % path)
264 264 return False
265 265 log.debug('reading hgrc from %s', path)
266 266 cfg = config.config()
267 267 cfg.read(path)
268 268 for section in ui_sections:
269 269 for k, v in cfg.items(section):
270 270 log.debug('settings ui from file[%s]%s:%s', section, k, v)
271 271 baseui.setconfig(section, k, v)
272 272
273 273 elif read_from == 'db':
274 274 sa = meta.Session
275 275 ret = sa.query(RhodeCodeUi)\
276 276 .options(FromCache("sql_cache_short",
277 277 "get_hg_ui_settings")).all()
278 278
279 279 hg_ui = ret
280 280 for ui_ in hg_ui:
281 281 if ui_.ui_active:
282 282 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
283 283 ui_.ui_key, ui_.ui_value)
284 284 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
285 285
286 286 meta.Session.remove()
287 287 return baseui
288 288
289 289
290 290 def set_rhodecode_config(config):
291 291 """
292 292 Updates pylons config with new settings from database
293 293
294 294 :param config:
295 295 """
296 296 hgsettings = RhodeCodeSetting.get_app_settings()
297 297
298 298 for k, v in hgsettings.items():
299 299 config[k] = v
300 300
301 301
302 302 def invalidate_cache(cache_key, *args):
303 303 """
304 304 Puts cache invalidation task into db for
305 305 further global cache invalidation
306 306 """
307 307
308 308 from rhodecode.model.scm import ScmModel
309 309
310 310 if cache_key.startswith('get_repo_cached_'):
311 311 name = cache_key.split('get_repo_cached_')[-1]
312 312 ScmModel().mark_for_invalidation(name)
313 313
314 314
315 315 class EmptyChangeset(BaseChangeset):
316 316 """
317 317 An dummy empty changeset. It's possible to pass hash when creating
318 318 an EmptyChangeset
319 319 """
320 320
321 321 def __init__(self, cs='0' * 40, repo=None, requested_revision=None, alias=None):
322 322 self._empty_cs = cs
323 323 self.revision = -1
324 324 self.message = ''
325 325 self.author = ''
326 326 self.date = ''
327 327 self.repository = repo
328 328 self.requested_revision = requested_revision
329 329 self.alias = alias
330 330
331 331 @LazyProperty
332 332 def raw_id(self):
333 333 """
334 334 Returns raw string identifying this changeset, useful for web
335 335 representation.
336 336 """
337 337
338 338 return self._empty_cs
339 339
340 340 @LazyProperty
341 341 def branch(self):
342 342 return get_backend(self.alias).DEFAULT_BRANCH_NAME
343 343
344 344 @LazyProperty
345 345 def short_id(self):
346 346 return self.raw_id[:12]
347 347
348 348 def get_file_changeset(self, path):
349 349 return self
350 350
351 351 def get_file_content(self, path):
352 352 return u''
353 353
354 354 def get_file_size(self, path):
355 355 return 0
356 356
357 357
358 358 def map_groups(groups):
359 359 """
360 360 Checks for groups existence, and creates groups structures.
361 361 It returns last group in structure
362 362
363 363 :param groups: list of groups structure
364 364 """
365 365 sa = meta.Session
366 366
367 367 parent = None
368 368 group = None
369 369
370 370 # last element is repo in nested groups structure
371 371 groups = groups[:-1]
372 372
373 373 for lvl, group_name in enumerate(groups):
374 374 group_name = '/'.join(groups[:lvl] + [group_name])
375 375 group = sa.query(RepoGroup).filter(RepoGroup.group_name == group_name).scalar()
376 376
377 377 if group is None:
378 378 group = RepoGroup(group_name, parent)
379 379 sa.add(group)
380 380 sa.commit()
381 381 parent = group
382 382 return group
383 383
384 384
385 385 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
386 386 """
387 387 maps all repos given in initial_repo_list, non existing repositories
388 388 are created, if remove_obsolete is True it also check for db entries
389 389 that are not in initial_repo_list and removes them.
390 390
391 391 :param initial_repo_list: list of repositories found by scanning methods
392 392 :param remove_obsolete: check for obsolete entries in database
393 393 """
394 394 from rhodecode.model.repo import RepoModel
395 395 sa = meta.Session
396 396 rm = RepoModel()
397 397 user = sa.query(User).filter(User.admin == True).first()
398 398 if user is None:
399 399 raise Exception('Missing administrative account !')
400 400 added = []
401 401
402 402 for name, repo in initial_repo_list.items():
403 403 group = map_groups(name.split(Repository.url_sep()))
404 404 if not rm.get_by_repo_name(name, cache=False):
405 405 log.info('repository %s not found creating default', name)
406 406 added.append(name)
407 407 form_data = {
408 408 'repo_name': name,
409 409 'repo_name_full': name,
410 410 'repo_type': repo.alias,
411 411 'description': repo.description \
412 412 if repo.description != 'unknown' else \
413 413 '%s repository' % name,
414 414 'private': False,
415 415 'group_id': getattr(group, 'group_id', None)
416 416 }
417 417 rm.create(form_data, user, just_db=True)
418 418 sa.commit()
419 419 removed = []
420 420 if remove_obsolete:
421 421 #remove from database those repositories that are not in the filesystem
422 422 for repo in sa.query(Repository).all():
423 423 if repo.repo_name not in initial_repo_list.keys():
424 424 removed.append(repo.repo_name)
425 425 sa.delete(repo)
426 426 sa.commit()
427 427
428 428 return added, removed
429 429
430 430 # set cache regions for beaker so celery can utilise it
431 431 def add_cache(settings):
432 432 cache_settings = {'regions': None}
433 433 for key in settings.keys():
434 434 for prefix in ['beaker.cache.', 'cache.']:
435 435 if key.startswith(prefix):
436 436 name = key.split(prefix)[1].strip()
437 437 cache_settings[name] = settings[key].strip()
438 438 if cache_settings['regions']:
439 439 for region in cache_settings['regions'].split(','):
440 440 region = region.strip()
441 441 region_settings = {}
442 442 for key, value in cache_settings.items():
443 443 if key.startswith(region):
444 444 region_settings[key.split('.')[1]] = value
445 445 region_settings['expire'] = int(region_settings.get('expire',
446 446 60))
447 447 region_settings.setdefault('lock_dir',
448 448 cache_settings.get('lock_dir'))
449 449 region_settings.setdefault('data_dir',
450 450 cache_settings.get('data_dir'))
451 451
452 452 if 'type' not in region_settings:
453 453 region_settings['type'] = cache_settings.get('type',
454 454 'memory')
455 455 beaker.cache.cache_regions[region] = region_settings
456 456
457 457
458 458 #==============================================================================
459 459 # TEST FUNCTIONS AND CREATORS
460 460 #==============================================================================
461 461 def create_test_index(repo_location, config, full_index):
462 462 """
463 463 Makes default test index
464 464
465 465 :param config: test config
466 466 :param full_index:
467 467 """
468 468
469 469 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
470 470 from rhodecode.lib.pidlock import DaemonLock, LockHeld
471 471
472 472 repo_location = repo_location
473 473
474 474 index_location = os.path.join(config['app_conf']['index_dir'])
475 475 if not os.path.exists(index_location):
476 476 os.makedirs(index_location)
477 477
478 478 try:
479 479 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
480 480 WhooshIndexingDaemon(index_location=index_location,
481 481 repo_location=repo_location)\
482 482 .run(full_index=full_index)
483 483 l.release()
484 484 except LockHeld:
485 485 pass
486 486
487 487
488 488 def create_test_env(repos_test_path, config):
489 489 """
490 490 Makes a fresh database and
491 491 install test repository into tmp dir
492 492 """
493 493 from rhodecode.lib.db_manage import DbManage
494 494 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
495 495
496 496 # PART ONE create db
497 497 dbconf = config['sqlalchemy.db1.url']
498 498 log.debug('making test db %s', dbconf)
499 499
500 500 # create test dir if it doesn't exist
501 501 if not os.path.isdir(repos_test_path):
502 502 log.debug('Creating testdir %s' % repos_test_path)
503 503 os.makedirs(repos_test_path)
504 504
505 505 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
506 506 tests=True)
507 507 dbmanage.create_tables(override=True)
508 508 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
509 509 dbmanage.create_default_user()
510 510 dbmanage.admin_prompt()
511 511 dbmanage.create_permissions()
512 512 dbmanage.populate_default_permissions()
513 513 Session.commit()
514 514 # PART TWO make test repo
515 515 log.debug('making test vcs repositories')
516 516
517 517 idx_path = config['app_conf']['index_dir']
518 518 data_path = config['app_conf']['cache_dir']
519 519
520 520 #clean index and data
521 521 if idx_path and os.path.exists(idx_path):
522 522 log.debug('remove %s' % idx_path)
523 523 shutil.rmtree(idx_path)
524 524
525 525 if data_path and os.path.exists(data_path):
526 526 log.debug('remove %s' % data_path)
527 527 shutil.rmtree(data_path)
528 528
529 529 #CREATE DEFAULT HG REPOSITORY
530 530 cur_dir = dn(dn(abspath(__file__)))
531 531 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
532 532 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
533 533 tar.close()
534 534
535 535
536 536 #==============================================================================
537 537 # PASTER COMMANDS
538 538 #==============================================================================
539 539 class BasePasterCommand(Command):
540 540 """
541 541 Abstract Base Class for paster commands.
542 542
543 543 The celery commands are somewhat aggressive about loading
544 544 celery.conf, and since our module sets the `CELERY_LOADER`
545 545 environment variable to our loader, we have to bootstrap a bit and
546 546 make sure we've had a chance to load the pylons config off of the
547 547 command line, otherwise everything fails.
548 548 """
549 549 min_args = 1
550 550 min_args_error = "Please provide a paster config file as an argument."
551 551 takes_config_file = 1
552 552 requires_config_file = True
553 553
554 554 def notify_msg(self, msg, log=False):
555 555 """Make a notification to user, additionally if logger is passed
556 556 it logs this action using given logger
557 557
558 558 :param msg: message that will be printed to user
559 559 :param log: logging instance, to use to additionally log this message
560 560
561 561 """
562 562 if log and isinstance(log, logging):
563 563 log(msg)
564 564
565 565 def run(self, args):
566 566 """
567 567 Overrides Command.run
568 568
569 569 Checks for a config file argument and loads it.
570 570 """
571 571 if len(args) < self.min_args:
572 572 raise BadCommand(
573 573 self.min_args_error % {'min_args': self.min_args,
574 574 'actual_args': len(args)})
575 575
576 576 # Decrement because we're going to lob off the first argument.
577 577 # @@ This is hacky
578 578 self.min_args -= 1
579 579 self.bootstrap_config(args[0])
580 580 self.update_parser()
581 581 return super(BasePasterCommand, self).run(args[1:])
582 582
583 583 def update_parser(self):
584 584 """
585 585 Abstract method. Allows for the class's parser to be updated
586 586 before the superclass's `run` method is called. Necessary to
587 587 allow options/arguments to be passed through to the underlying
588 588 celery command.
589 589 """
590 590 raise NotImplementedError("Abstract Method.")
591 591
592 592 def bootstrap_config(self, conf):
593 593 """
594 594 Loads the pylons configuration.
595 595 """
596 596 from pylons import config as pylonsconfig
597 597
598 598 path_to_ini_file = os.path.realpath(conf)
599 599 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
600 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
601
600 pylonsconfig.init_app(conf.global_conf, conf.local_conf) No newline at end of file
@@ -1,372 +1,369
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 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 time
27 27 import traceback
28 28 import logging
29 29
30 30 from vcs import get_backend
31 31 from vcs.exceptions import RepositoryError
32 32 from vcs.utils.lazy import LazyProperty
33 33 from vcs.nodes import FileNode
34 34
35 35 from rhodecode import BACKENDS
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib import safe_str
38 38 from rhodecode.lib.auth import HasRepoPermissionAny
39 39 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
40 40 action_logger, EmptyChangeset
41 41 from rhodecode.model import BaseModel
42 42 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
43 43 UserFollowing, UserLog, User
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class UserTemp(object):
49 49 def __init__(self, user_id):
50 50 self.user_id = user_id
51 51
52 52 def __repr__(self):
53 53 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
54 54
55 55
56 56 class RepoTemp(object):
57 57 def __init__(self, repo_id):
58 58 self.repo_id = repo_id
59 59
60 60 def __repr__(self):
61 61 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
62 62
63 63 class CachedRepoList(object):
64 64
65 65 def __init__(self, db_repo_list, repos_path, order_by=None):
66 66 self.db_repo_list = db_repo_list
67 67 self.repos_path = repos_path
68 68 self.order_by = order_by
69 69 self.reversed = (order_by or '').startswith('-')
70 70
71 71 def __len__(self):
72 72 return len(self.db_repo_list)
73 73
74 74 def __repr__(self):
75 75 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
76 76
77 77 def __iter__(self):
78 78 for dbr in self.db_repo_list:
79 79 scmr = dbr.scm_instance_cached
80 80 # check permission at this level
81 81 if not HasRepoPermissionAny('repository.read', 'repository.write',
82 82 'repository.admin')(dbr.repo_name,
83 83 'get repo check'):
84 84 continue
85 85
86 86 if scmr is None:
87 87 log.error('%s this repository is present in database but it '
88 88 'cannot be created as an scm instance',
89 89 dbr.repo_name)
90 90 continue
91 91
92 92 last_change = scmr.last_change
93 93 tip = h.get_changeset_safe(scmr, 'tip')
94 94
95 95 tmp_d = {}
96 96 tmp_d['name'] = dbr.repo_name
97 97 tmp_d['name_sort'] = tmp_d['name'].lower()
98 98 tmp_d['description'] = dbr.description
99 99 tmp_d['description_sort'] = tmp_d['description']
100 100 tmp_d['last_change'] = last_change
101 101 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
102 102 tmp_d['tip'] = tip.raw_id
103 103 tmp_d['tip_sort'] = tip.revision
104 104 tmp_d['rev'] = tip.revision
105 105 tmp_d['contact'] = dbr.user.full_contact
106 106 tmp_d['contact_sort'] = tmp_d['contact']
107 107 tmp_d['owner_sort'] = tmp_d['contact']
108 108 tmp_d['repo_archives'] = list(scmr._get_archives())
109 109 tmp_d['last_msg'] = tip.message
110 110 tmp_d['author'] = tip.author
111 111 tmp_d['dbrepo'] = dbr.get_dict()
112 112 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
113 113 yield tmp_d
114 114
115 115 class ScmModel(BaseModel):
116 116 """
117 117 Generic Scm Model
118 118 """
119 119
120 120 @LazyProperty
121 121 def repos_path(self):
122 122 """
123 123 Get's the repositories root path from database
124 124 """
125 125
126 126 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
127 127
128 128 return q.ui_value
129 129
130 130 def repo_scan(self, repos_path=None):
131 131 """
132 132 Listing of repositories in given path. This path should not be a
133 133 repository itself. Return a dictionary of repository objects
134 134
135 135 :param repos_path: path to directory containing repositories
136 136 """
137 137
138 138 log.info('scanning for repositories in %s', repos_path)
139 139
140 140 if repos_path is None:
141 141 repos_path = self.repos_path
142 142
143 143 baseui = make_ui('db')
144 144 repos = {}
145 145
146 146 for name, path in get_filesystem_repos(repos_path, recursive=True):
147 147
148 148 # name need to be decomposed and put back together using the /
149 149 # since this is internal storage separator for rhodecode
150 150 name = Repository.url_sep().join(name.split(os.sep))
151 151
152 152 try:
153 153 if name in repos:
154 154 raise RepositoryError('Duplicate repository name %s '
155 155 'found in %s' % (name, path))
156 156 else:
157 157
158 158 klass = get_backend(path[0])
159 159
160 160 if path[0] == 'hg' and path[0] in BACKENDS.keys():
161
162 # for mercurial we need to have an str path
163 repos[name] = klass(safe_str(path[1]),
164 baseui=baseui)
161 repos[name] = klass(safe_str(path[1]), baseui=baseui)
165 162
166 163 if path[0] == 'git' and path[0] in BACKENDS.keys():
167 164 repos[name] = klass(path[1])
168 165 except OSError:
169 166 continue
170 167
171 168 return repos
172 169
173 170 def get_repos(self, all_repos=None, sort_key=None):
174 171 """
175 172 Get all repos from db and for each repo create it's
176 173 backend instance and fill that backed with information from database
177 174
178 175 :param all_repos: list of repository names as strings
179 176 give specific repositories list, good for filtering
180 177 """
181 178 if all_repos is None:
182 179 all_repos = self.sa.query(Repository)\
183 180 .filter(Repository.group_id == None)\
184 181 .order_by(Repository.repo_name).all()
185 182
186 183 repo_iter = CachedRepoList(all_repos, repos_path=self.repos_path,
187 184 order_by=sort_key)
188 185
189 186 return repo_iter
190 187
191 188 def mark_for_invalidation(self, repo_name):
192 189 """Puts cache invalidation task into db for
193 190 further global cache invalidation
194 191
195 192 :param repo_name: this repo that should invalidation take place
196 193 """
197 194 CacheInvalidation.set_invalidate(repo_name)
198 195 CacheInvalidation.set_invalidate(repo_name + "_README")
199 196
200 197 def toggle_following_repo(self, follow_repo_id, user_id):
201 198
202 199 f = self.sa.query(UserFollowing)\
203 200 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
204 201 .filter(UserFollowing.user_id == user_id).scalar()
205 202
206 203 if f is not None:
207 204 try:
208 205 self.sa.delete(f)
209 206 action_logger(UserTemp(user_id),
210 207 'stopped_following_repo',
211 208 RepoTemp(follow_repo_id))
212 209 return
213 210 except:
214 211 log.error(traceback.format_exc())
215 212 raise
216 213
217 214 try:
218 215 f = UserFollowing()
219 216 f.user_id = user_id
220 217 f.follows_repo_id = follow_repo_id
221 218 self.sa.add(f)
222 219
223 220 action_logger(UserTemp(user_id),
224 221 'started_following_repo',
225 222 RepoTemp(follow_repo_id))
226 223 except:
227 224 log.error(traceback.format_exc())
228 225 raise
229 226
230 227 def toggle_following_user(self, follow_user_id, user_id):
231 228 f = self.sa.query(UserFollowing)\
232 229 .filter(UserFollowing.follows_user_id == follow_user_id)\
233 230 .filter(UserFollowing.user_id == user_id).scalar()
234 231
235 232 if f is not None:
236 233 try:
237 234 self.sa.delete(f)
238 235 return
239 236 except:
240 237 log.error(traceback.format_exc())
241 238 raise
242 239
243 240 try:
244 241 f = UserFollowing()
245 242 f.user_id = user_id
246 243 f.follows_user_id = follow_user_id
247 244 self.sa.add(f)
248 245 except:
249 246 log.error(traceback.format_exc())
250 247 raise
251 248
252 249 def is_following_repo(self, repo_name, user_id, cache=False):
253 250 r = self.sa.query(Repository)\
254 251 .filter(Repository.repo_name == repo_name).scalar()
255 252
256 253 f = self.sa.query(UserFollowing)\
257 254 .filter(UserFollowing.follows_repository == r)\
258 255 .filter(UserFollowing.user_id == user_id).scalar()
259 256
260 257 return f is not None
261 258
262 259 def is_following_user(self, username, user_id, cache=False):
263 260 u = User.get_by_username(username)
264 261
265 262 f = self.sa.query(UserFollowing)\
266 263 .filter(UserFollowing.follows_user == u)\
267 264 .filter(UserFollowing.user_id == user_id).scalar()
268 265
269 266 return f is not None
270 267
271 268 def get_followers(self, repo_id):
272 269 if not isinstance(repo_id, int):
273 270 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
274 271
275 272 return self.sa.query(UserFollowing)\
276 273 .filter(UserFollowing.follows_repo_id == repo_id).count()
277 274
278 275 def get_forks(self, repo_id):
279 276 if not isinstance(repo_id, int):
280 277 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
281 278
282 279 return self.sa.query(Repository)\
283 280 .filter(Repository.fork_id == repo_id).count()
284 281
285 282 def pull_changes(self, repo_name, username):
286 283 dbrepo = Repository.get_by_repo_name(repo_name)
287 284 clone_uri = dbrepo.clone_uri
288 285 if not clone_uri:
289 286 raise Exception("This repository doesn't have a clone uri")
290 287
291 288 repo = dbrepo.scm_instance
292 289 try:
293 290 extras = {'ip': '',
294 291 'username': username,
295 292 'action': 'push_remote',
296 293 'repository': repo_name}
297 294
298 295 #inject ui extra param to log this action via push logger
299 296 for k, v in extras.items():
300 297 repo._repo.ui.setconfig('rhodecode_extras', k, v)
301 298
302 299 repo.pull(clone_uri)
303 300 self.mark_for_invalidation(repo_name)
304 301 except:
305 302 log.error(traceback.format_exc())
306 303 raise
307 304
308 305 def commit_change(self, repo, repo_name, cs, user, author, message,
309 306 content, f_path):
310 307
311 308 if repo.alias == 'hg':
312 309 from vcs.backends.hg import MercurialInMemoryChangeset as IMC
313 310 elif repo.alias == 'git':
314 311 from vcs.backends.git import GitInMemoryChangeset as IMC
315 312
316 313 # decoding here will force that we have proper encoded values
317 314 # in any other case this will throw exceptions and deny commit
318 315 content = safe_str(content)
319 316 message = safe_str(message)
320 317 path = safe_str(f_path)
321 318 author = safe_str(author)
322 319 m = IMC(repo)
323 320 m.change(FileNode(path, content))
324 321 tip = m.commit(message=message,
325 322 author=author,
326 323 parents=[cs], branch=cs.branch)
327 324
328 325 new_cs = tip.short_id
329 326 action = 'push_local:%s' % new_cs
330 327
331 328 action_logger(user, action, repo_name)
332 329
333 330 self.mark_for_invalidation(repo_name)
334 331
335 332 def create_node(self, repo, repo_name, cs, user, author, message, content,
336 333 f_path):
337 334 if repo.alias == 'hg':
338 335 from vcs.backends.hg import MercurialInMemoryChangeset as IMC
339 336 elif repo.alias == 'git':
340 337 from vcs.backends.git import GitInMemoryChangeset as IMC
341 338 # decoding here will force that we have proper encoded values
342 339 # in any other case this will throw exceptions and deny commit
343 340
344 341 if isinstance(content, (basestring,)):
345 342 content = safe_str(content)
346 343 elif isinstance(content, file):
347 344 content = content.read()
348 345
349 346 message = safe_str(message)
350 347 path = safe_str(f_path)
351 348 author = safe_str(author)
352 349 m = IMC(repo)
353 350
354 351 if isinstance(cs, EmptyChangeset):
355 352 # Emptychangeset means we we're editing empty repository
356 353 parents = None
357 354 else:
358 355 parents = [cs]
359 356
360 357 m.add(FileNode(path, content=content))
361 358 tip = m.commit(message=message,
362 359 author=author,
363 360 parents=parents, branch=cs.branch)
364 361 new_cs = tip.short_id
365 362 action = 'push_local:%s' % new_cs
366 363
367 364 action_logger(user, action, repo_name)
368 365
369 366 self.mark_for_invalidation(repo_name)
370 367
371 368 def get_unread_journal(self):
372 369 return self.sa.query(UserLog).count()
General Comments 0
You need to be logged in to leave comments. Login now