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