##// END OF EJS Templates
logged local commit with special action via action_logger,
marcink -
r1312:70a5a9a5 beta
parent child Browse files
Show More
@@ -1,409 +1,410 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import mimetypes
29 29 import traceback
30 30
31 31 from pylons import request, response, session, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34
35 35 from vcs.backends import ARCHIVE_SPECS
36 36 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
37 37 EmptyRepositoryError, ImproperArchiveTypeError, VCSError
38 38 from vcs.nodes import FileNode, NodeKind
39 39 from vcs.utils import diffs as differ
40 40
41 41 from rhodecode.lib import convert_line_endings, detect_mode
42 42 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 43 from rhodecode.lib.base import BaseRepoController, render
44 44 from rhodecode.lib.utils import EmptyChangeset
45 45 import rhodecode.lib.helpers as h
46 46 from rhodecode.model.repo import RepoModel
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class FilesController(BaseRepoController):
52 52
53 53 @LoginRequired()
54 54 def __before__(self):
55 55 super(FilesController, self).__before__()
56 56 c.cut_off_limit = self.cut_off_limit
57 57
58 58 def __get_cs_or_redirect(self, rev, repo_name):
59 59 """
60 60 Safe way to get changeset if error occur it redirects to tip with
61 61 proper message
62 62
63 63 :param rev: revision to fetch
64 64 :param repo_name: repo name to redirect after
65 65 """
66 66
67 67 try:
68 68 return c.rhodecode_repo.get_changeset(rev)
69 69 except EmptyRepositoryError, e:
70 70 h.flash(_('There are no files yet'), category='warning')
71 71 redirect(h.url('summary_home', repo_name=repo_name))
72 72
73 73 except RepositoryError, e:
74 74 h.flash(str(e), category='warning')
75 75 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
76 76
77 77 def __get_filenode_or_redirect(self, repo_name, cs, path):
78 78 """
79 79 Returns file_node, if error occurs or given path is directory,
80 80 it'll redirect to top level path
81 81
82 82 :param repo_name: repo_name
83 83 :param cs: given changeset
84 84 :param path: path to lookup
85 85 """
86 86
87 87 try:
88 88 file_node = cs.get_node(path)
89 89 if file_node.is_dir():
90 90 raise RepositoryError('given path is a directory')
91 91 except RepositoryError, e:
92 92 h.flash(str(e), category='warning')
93 93 redirect(h.url('files_home', repo_name=repo_name,
94 94 revision=cs.raw_id))
95 95
96 96 return file_node
97 97
98 98 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
99 99 'repository.admin')
100 100 def index(self, repo_name, revision, f_path):
101 101 #reditect to given revision from form if given
102 102 post_revision = request.POST.get('at_rev', None)
103 103 if post_revision:
104 104 cs = self.__get_cs_or_redirect(post_revision, repo_name)
105 105 redirect(url('files_home', repo_name=c.repo_name,
106 106 revision=cs.raw_id, f_path=f_path))
107 107
108 108 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
109 109 c.branch = request.GET.get('branch', None)
110 110 c.f_path = f_path
111 111
112 112 cur_rev = c.changeset.revision
113 113
114 114 #prev link
115 115 try:
116 116 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
117 117 c.url_prev = url('files_home', repo_name=c.repo_name,
118 118 revision=prev_rev.raw_id, f_path=f_path)
119 119 if c.branch:
120 120 c.url_prev += '?branch=%s' % c.branch
121 121 except (ChangesetDoesNotExistError, VCSError):
122 122 c.url_prev = '#'
123 123
124 124 #next link
125 125 try:
126 126 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
127 127 c.url_next = url('files_home', repo_name=c.repo_name,
128 128 revision=next_rev.raw_id, f_path=f_path)
129 129 if c.branch:
130 130 c.url_next += '?branch=%s' % c.branch
131 131 except (ChangesetDoesNotExistError, VCSError):
132 132 c.url_next = '#'
133 133
134 134 #files or dirs
135 135 try:
136 136 c.files_list = c.changeset.get_node(f_path)
137 137
138 138 if c.files_list.is_file():
139 139 c.file_history = self._get_node_history(c.changeset, f_path)
140 140 else:
141 141 c.file_history = []
142 142 except RepositoryError, e:
143 143 h.flash(str(e), category='warning')
144 144 redirect(h.url('files_home', repo_name=repo_name,
145 145 revision=revision))
146 146
147 147 return render('files/files.html')
148 148
149 149 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
150 150 'repository.admin')
151 151 def rawfile(self, repo_name, revision, f_path):
152 152 cs = self.__get_cs_or_redirect(revision, repo_name)
153 153 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
154 154
155 155 response.content_disposition = 'attachment; filename=%s' % \
156 156 f_path.split(os.sep)[-1].encode('utf8', 'replace')
157 157
158 158 response.content_type = file_node.mimetype
159 159 return file_node.content
160 160
161 161 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 162 'repository.admin')
163 163 def raw(self, repo_name, revision, f_path):
164 164 cs = self.__get_cs_or_redirect(revision, repo_name)
165 165 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
166 166
167 167 raw_mimetype_mapping = {
168 168 # map original mimetype to a mimetype used for "show as raw"
169 169 # you can also provide a content-disposition to override the
170 170 # default "attachment" disposition.
171 171 # orig_type: (new_type, new_dispo)
172 172
173 173 # show images inline:
174 174 'image/x-icon': ('image/x-icon', 'inline'),
175 175 'image/png': ('image/png', 'inline'),
176 176 'image/gif': ('image/gif', 'inline'),
177 177 'image/jpeg': ('image/jpeg', 'inline'),
178 178 'image/svg+xml': ('image/svg+xml', 'inline'),
179 179 }
180 180
181 181 mimetype = file_node.mimetype
182 182 try:
183 183 mimetype, dispo = raw_mimetype_mapping[mimetype]
184 184 except KeyError:
185 185 # we don't know anything special about this, handle it safely
186 186 if file_node.is_binary:
187 187 # do same as download raw for binary files
188 188 mimetype, dispo = 'application/octet-stream', 'attachment'
189 189 else:
190 190 # do not just use the original mimetype, but force text/plain,
191 191 # otherwise it would serve text/html and that might be unsafe.
192 192 # Note: underlying vcs library fakes text/plain mimetype if the
193 193 # mimetype can not be determined and it thinks it is not
194 194 # binary.This might lead to erroneous text display in some
195 195 # cases, but helps in other cases, like with text files
196 196 # without extension.
197 197 mimetype, dispo = 'text/plain', 'inline'
198 198
199 199 if dispo == 'attachment':
200 200 dispo = 'attachment; filename=%s' % \
201 201 f_path.split(os.sep)[-1].encode('utf8', 'replace')
202 202
203 203 response.content_disposition = dispo
204 204 response.content_type = mimetype
205 205 return file_node.content
206 206
207 207 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
208 208 'repository.admin')
209 209 def annotate(self, repo_name, revision, f_path):
210 210 c.cs = self.__get_cs_or_redirect(revision, repo_name)
211 211 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
212 212
213 213 c.file_history = self._get_node_history(c.cs, f_path)
214 214 c.f_path = f_path
215 215 return render('files/files_annotate.html')
216 216
217 217 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
218 218 def edit(self, repo_name, revision, f_path):
219 219 r_post = request.POST
220 220
221 221 c.cs = self.__get_cs_or_redirect(revision, repo_name)
222 222 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
223 223
224 224 c.file_history = self._get_node_history(c.cs, f_path)
225 225 c.f_path = f_path
226 226
227 227 if r_post:
228 228
229 229 old_content = c.file.content
230 230 sl = old_content.splitlines(1)
231 231 first_line = sl[0] if sl else ''
232 232 # modes: 0 - Unix, 1 - Mac, 2 - DOS
233 233 mode = detect_mode(first_line, 0)
234 234 content = convert_line_endings(r_post.get('content'), mode)
235 235
236 236 message = r_post.get('message') or (_('Edited %s via RhodeCode')
237 237 % (f_path))
238 238 author = self.rhodecode_user.full_contact
239 239
240 240 if content == old_content:
241 241 h.flash(_('No changes'),
242 242 category='warning')
243 243 return redirect(url('changeset_home', repo_name=c.repo_name,
244 244 revision='tip'))
245 245
246 246 try:
247 247 self.scm_model.commit_change(repo=c.rhodecode_repo,
248 248 repo_name=repo_name, cs=c.cs,
249 user=self.rhodecode_user,
249 250 author=author, message=message,
250 251 content=content, f_path=f_path)
251 252 h.flash(_('Successfully committed to %s' % f_path),
252 253 category='success')
253 254
254 255 except Exception:
255 256 log.error(traceback.format_exc())
256 257 h.flash(_('Error occurred during commit'), category='error')
257 258 return redirect(url('changeset_home',
258 259 repo_name=c.repo_name, revision='tip'))
259 260
260 261 return render('files/files_edit.html')
261 262
262 263 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
263 264 'repository.admin')
264 265 def archivefile(self, repo_name, fname):
265 266
266 267 fileformat = None
267 268 revision = None
268 269 ext = None
269 270
270 271 for a_type, ext_data in ARCHIVE_SPECS.items():
271 272 archive_spec = fname.split(ext_data[1])
272 273 if len(archive_spec) == 2 and archive_spec[1] == '':
273 274 fileformat = a_type or ext_data[1]
274 275 revision = archive_spec[0]
275 276 ext = ext_data[1]
276 277
277 278 try:
278 279 dbrepo = RepoModel().get_by_repo_name(repo_name)
279 280 if dbrepo.enable_downloads is False:
280 281 return _('downloads disabled')
281 282
282 283 cs = c.rhodecode_repo.get_changeset(revision)
283 284 content_type = ARCHIVE_SPECS[fileformat][0]
284 285 except ChangesetDoesNotExistError:
285 286 return _('Unknown revision %s') % revision
286 287 except EmptyRepositoryError:
287 288 return _('Empty repository')
288 289 except (ImproperArchiveTypeError, KeyError):
289 290 return _('Unknown archive type')
290 291
291 292 response.content_type = content_type
292 293 response.content_disposition = 'attachment; filename=%s-%s%s' \
293 294 % (repo_name, revision, ext)
294 295
295 296 import tempfile
296 297 archive = tempfile.mkstemp()[1]
297 298 t = open(archive, 'wb')
298 299 cs.fill_archive(stream=t, kind=fileformat)
299 300
300 301 def get_chunked_archive(archive):
301 302 stream = open(archive, 'rb')
302 303 while True:
303 304 data = stream.read(4096)
304 305 if not data:
305 306 os.remove(archive)
306 307 break
307 308 yield data
308 309
309 310 return get_chunked_archive(archive)
310 311
311 312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
312 313 'repository.admin')
313 314 def diff(self, repo_name, f_path):
314 315 diff1 = request.GET.get('diff1')
315 316 diff2 = request.GET.get('diff2')
316 317 c.action = request.GET.get('diff')
317 318 c.no_changes = diff1 == diff2
318 319 c.f_path = f_path
319 320 c.big_diff = False
320 321
321 322 try:
322 323 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
323 324 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
324 325 node1 = c.changeset_1.get_node(f_path)
325 326 else:
326 327 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
327 328 node1 = FileNode('.', '', changeset=c.changeset_1)
328 329
329 330 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
330 331 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
331 332 node2 = c.changeset_2.get_node(f_path)
332 333 else:
333 334 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
334 335 node2 = FileNode('.', '', changeset=c.changeset_2)
335 336 except RepositoryError:
336 337 return redirect(url('files_home',
337 338 repo_name=c.repo_name, f_path=f_path))
338 339
339 340 if c.action == 'download':
340 341 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
341 342 format='gitdiff')
342 343
343 344 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
344 345 response.content_type = 'text/plain'
345 346 response.content_disposition = 'attachment; filename=%s' \
346 347 % diff_name
347 348 return diff.raw_diff()
348 349
349 350 elif c.action == 'raw':
350 351 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
351 352 format='gitdiff')
352 353 response.content_type = 'text/plain'
353 354 return diff.raw_diff()
354 355
355 356 elif c.action == 'diff':
356 357 if node1.is_binary or node2.is_binary:
357 358 c.cur_diff = _('Binary file')
358 359 elif node1.size > self.cut_off_limit or \
359 360 node2.size > self.cut_off_limit:
360 361 c.cur_diff = ''
361 362 c.big_diff = True
362 363 else:
363 364 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
364 365 format='gitdiff')
365 366 c.cur_diff = diff.as_html()
366 367 else:
367 368
368 369 #default option
369 370 if node1.is_binary or node2.is_binary:
370 371 c.cur_diff = _('Binary file')
371 372 elif node1.size > self.cut_off_limit or \
372 373 node2.size > self.cut_off_limit:
373 374 c.cur_diff = ''
374 375 c.big_diff = True
375 376
376 377 else:
377 378 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
378 379 format='gitdiff')
379 380 c.cur_diff = diff.as_html()
380 381
381 382 if not c.cur_diff and not c.big_diff:
382 383 c.no_changes = True
383 384 return render('files/file_diff.html')
384 385
385 386 def _get_node_history(self, cs, f_path):
386 387 changesets = cs.get_file_history(f_path)
387 388 hist_l = []
388 389
389 390 changesets_group = ([], _("Changesets"))
390 391 branches_group = ([], _("Branches"))
391 392 tags_group = ([], _("Tags"))
392 393
393 394 for chs in changesets:
394 395 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
395 396 changesets_group[0].append((chs.raw_id, n_desc,))
396 397
397 398 hist_l.append(changesets_group)
398 399
399 400 for name, chs in c.rhodecode_repo.branches.items():
400 401 #chs = chs.split(':')[-1]
401 402 branches_group[0].append((chs, name),)
402 403 hist_l.append(branches_group)
403 404
404 405 for name, chs in c.rhodecode_repo.tags.items():
405 406 #chs = chs.split(':')[-1]
406 407 tags_group[0].append((chs, name),)
407 408 hist_l.append(tags_group)
408 409
409 410 return hist_l
@@ -1,691 +1,693 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10
11 11 from datetime import datetime
12 12 from pygments.formatters import HtmlFormatter
13 13 from pygments import highlight as code_highlight
14 14 from pylons import url, request, config
15 15 from pylons.i18n.translation import _, ungettext
16 16
17 17 from webhelpers.html import literal, HTML, escape
18 18 from webhelpers.html.tools import *
19 19 from webhelpers.html.builder import make_tag
20 20 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
21 21 end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
22 22 link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
23 23 password, textarea, title, ul, xml_declaration, radio
24 24 from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
25 25 mail_to, strip_links, strip_tags, tag_re
26 26 from webhelpers.number import format_byte_size, format_bit_size
27 27 from webhelpers.pylonslib import Flash as _Flash
28 28 from webhelpers.pylonslib.secure_form import secure_form
29 29 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
30 30 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
31 31 replace_whitespace, urlify, truncate, wrap_paragraphs
32 32 from webhelpers.date import time_ago_in_words
33 33 from webhelpers.paginate import Page
34 34 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
35 35 convert_boolean_attrs, NotGiven
36 36
37 37 from vcs.utils.annotate import annotate_highlight
38 38 from rhodecode.lib.utils import repo_name_slug
39 39 from rhodecode.lib import str2bool, safe_unicode
40 40
41 41 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
42 42 """
43 43 Reset button
44 44 """
45 45 _set_input_attrs(attrs, type, name, value)
46 46 _set_id_attr(attrs, id, name)
47 47 convert_boolean_attrs(attrs, ["disabled"])
48 48 return HTML.input(**attrs)
49 49
50 50 reset = _reset
51 51
52 52
53 53 def get_token():
54 54 """Return the current authentication token, creating one if one doesn't
55 55 already exist.
56 56 """
57 57 token_key = "_authentication_token"
58 58 from pylons import session
59 59 if not token_key in session:
60 60 try:
61 61 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
62 62 except AttributeError: # Python < 2.4
63 63 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
64 64 session[token_key] = token
65 65 if hasattr(session, 'save'):
66 66 session.save()
67 67 return session[token_key]
68 68
69 69 class _GetError(object):
70 70 """Get error from form_errors, and represent it as span wrapped error
71 71 message
72 72
73 73 :param field_name: field to fetch errors for
74 74 :param form_errors: form errors dict
75 75 """
76 76
77 77 def __call__(self, field_name, form_errors):
78 78 tmpl = """<span class="error_msg">%s</span>"""
79 79 if form_errors and form_errors.has_key(field_name):
80 80 return literal(tmpl % form_errors.get(field_name))
81 81
82 82 get_error = _GetError()
83 83
84 84 class _ToolTip(object):
85 85
86 86 def __call__(self, tooltip_title, trim_at=50):
87 87 """Special function just to wrap our text into nice formatted
88 88 autowrapped text
89 89
90 90 :param tooltip_title:
91 91 """
92 92
93 93 return wrap_paragraphs(escape(tooltip_title), trim_at)\
94 94 .replace('\n', '<br/>')
95 95
96 96 def activate(self):
97 97 """Adds tooltip mechanism to the given Html all tooltips have to have
98 98 set class `tooltip` and set attribute `tooltip_title`.
99 99 Then a tooltip will be generated based on that. All with yui js tooltip
100 100 """
101 101
102 102 js = '''
103 103 YAHOO.util.Event.onDOMReady(function(){
104 104 function toolTipsId(){
105 105 var ids = [];
106 106 var tts = YAHOO.util.Dom.getElementsByClassName('tooltip');
107 107
108 108 for (var i = 0; i < tts.length; i++) {
109 109 //if element doesn't not have and id autogenerate one for tooltip
110 110
111 111 if (!tts[i].id){
112 112 tts[i].id='tt'+i*100;
113 113 }
114 114 ids.push(tts[i].id);
115 115 }
116 116 return ids
117 117 };
118 118 var myToolTips = new YAHOO.widget.Tooltip("tooltip", {
119 119 context: [[toolTipsId()],"tl","bl",null,[0,5]],
120 120 monitorresize:false,
121 121 xyoffset :[0,0],
122 122 autodismissdelay:300000,
123 123 hidedelay:5,
124 124 showdelay:20,
125 125 });
126 126
127 127 });
128 128 '''
129 129 return literal(js)
130 130
131 131 tooltip = _ToolTip()
132 132
133 133 class _FilesBreadCrumbs(object):
134 134
135 135 def __call__(self, repo_name, rev, paths):
136 136 if isinstance(paths, str):
137 137 paths = safe_unicode(paths)
138 138 url_l = [link_to(repo_name, url('files_home',
139 139 repo_name=repo_name,
140 140 revision=rev, f_path=''))]
141 141 paths_l = paths.split('/')
142 142 for cnt, p in enumerate(paths_l):
143 143 if p != '':
144 144 url_l.append(link_to(p, url('files_home',
145 145 repo_name=repo_name,
146 146 revision=rev,
147 147 f_path='/'.join(paths_l[:cnt + 1]))))
148 148
149 149 return literal('/'.join(url_l))
150 150
151 151 files_breadcrumbs = _FilesBreadCrumbs()
152 152
153 153 class CodeHtmlFormatter(HtmlFormatter):
154 154 """My code Html Formatter for source codes
155 155 """
156 156
157 157 def wrap(self, source, outfile):
158 158 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
159 159
160 160 def _wrap_code(self, source):
161 161 for cnt, it in enumerate(source):
162 162 i, t = it
163 163 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
164 164 yield i, t
165 165
166 166 def _wrap_tablelinenos(self, inner):
167 167 dummyoutfile = StringIO.StringIO()
168 168 lncount = 0
169 169 for t, line in inner:
170 170 if t:
171 171 lncount += 1
172 172 dummyoutfile.write(line)
173 173
174 174 fl = self.linenostart
175 175 mw = len(str(lncount + fl - 1))
176 176 sp = self.linenospecial
177 177 st = self.linenostep
178 178 la = self.lineanchors
179 179 aln = self.anchorlinenos
180 180 nocls = self.noclasses
181 181 if sp:
182 182 lines = []
183 183
184 184 for i in range(fl, fl + lncount):
185 185 if i % st == 0:
186 186 if i % sp == 0:
187 187 if aln:
188 188 lines.append('<a href="#%s%d" class="special">%*d</a>' %
189 189 (la, i, mw, i))
190 190 else:
191 191 lines.append('<span class="special">%*d</span>' % (mw, i))
192 192 else:
193 193 if aln:
194 194 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
195 195 else:
196 196 lines.append('%*d' % (mw, i))
197 197 else:
198 198 lines.append('')
199 199 ls = '\n'.join(lines)
200 200 else:
201 201 lines = []
202 202 for i in range(fl, fl + lncount):
203 203 if i % st == 0:
204 204 if aln:
205 205 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
206 206 else:
207 207 lines.append('%*d' % (mw, i))
208 208 else:
209 209 lines.append('')
210 210 ls = '\n'.join(lines)
211 211
212 212 # in case you wonder about the seemingly redundant <div> here: since the
213 213 # content in the other cell also is wrapped in a div, some browsers in
214 214 # some configurations seem to mess up the formatting...
215 215 if nocls:
216 216 yield 0, ('<table class="%stable">' % self.cssclass +
217 217 '<tr><td><div class="linenodiv" '
218 218 'style="background-color: #f0f0f0; padding-right: 10px">'
219 219 '<pre style="line-height: 125%">' +
220 220 ls + '</pre></div></td><td class="code">')
221 221 else:
222 222 yield 0, ('<table class="%stable">' % self.cssclass +
223 223 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
224 224 ls + '</pre></div></td><td class="code">')
225 225 yield 0, dummyoutfile.getvalue()
226 226 yield 0, '</td></tr></table>'
227 227
228 228
229 229 def pygmentize(filenode, **kwargs):
230 230 """pygmentize function using pygments
231 231
232 232 :param filenode:
233 233 """
234 234
235 235 return literal(code_highlight(filenode.content,
236 236 filenode.lexer, CodeHtmlFormatter(**kwargs)))
237 237
238 238 def pygmentize_annotation(repo_name, filenode, **kwargs):
239 239 """pygmentize function for annotation
240 240
241 241 :param filenode:
242 242 """
243 243
244 244 color_dict = {}
245 245 def gen_color(n=10000):
246 246 """generator for getting n of evenly distributed colors using
247 247 hsv color and golden ratio. It always return same order of colors
248 248
249 249 :returns: RGB tuple
250 250 """
251 251 import colorsys
252 252 golden_ratio = 0.618033988749895
253 253 h = 0.22717784590367374
254 254
255 255 for c in xrange(n):
256 256 h += golden_ratio
257 257 h %= 1
258 258 HSV_tuple = [h, 0.95, 0.95]
259 259 RGB_tuple = colorsys.hsv_to_rgb(*HSV_tuple)
260 260 yield map(lambda x:str(int(x * 256)), RGB_tuple)
261 261
262 262 cgenerator = gen_color()
263 263
264 264 def get_color_string(cs):
265 265 if color_dict.has_key(cs):
266 266 col = color_dict[cs]
267 267 else:
268 268 col = color_dict[cs] = cgenerator.next()
269 269 return "color: rgb(%s)! important;" % (', '.join(col))
270 270
271 271 def url_func(repo_name):
272 272 def _url_func(changeset):
273 273 tooltip_html = "<div style='font-size:0.8em'><b>Author:</b>" + \
274 274 " %s<br/><b>Date:</b> %s</b><br/><b>Message:</b> %s<br/></div>"
275 275
276 276 tooltip_html = tooltip_html % (changeset.author,
277 277 changeset.date,
278 278 tooltip(changeset.message))
279 279 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
280 280 short_id(changeset.raw_id))
281 281 uri = link_to(
282 282 lnk_format,
283 283 url('changeset_home', repo_name=repo_name,
284 284 revision=changeset.raw_id),
285 285 style=get_color_string(changeset.raw_id),
286 286 class_='tooltip',
287 287 title=tooltip_html
288 288 )
289 289
290 290 uri += '\n'
291 291 return uri
292 292 return _url_func
293 293
294 294 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
295 295
296 296 def get_changeset_safe(repo, rev):
297 297 from vcs.backends.base import BaseRepository
298 298 from vcs.exceptions import RepositoryError
299 299 if not isinstance(repo, BaseRepository):
300 300 raise Exception('You must pass an Repository '
301 301 'object as first argument got %s', type(repo))
302 302
303 303 try:
304 304 cs = repo.get_changeset(rev)
305 305 except RepositoryError:
306 306 from rhodecode.lib.utils import EmptyChangeset
307 307 cs = EmptyChangeset()
308 308 return cs
309 309
310 310
311 311 def is_following_repo(repo_name, user_id):
312 312 from rhodecode.model.scm import ScmModel
313 313 return ScmModel().is_following_repo(repo_name, user_id)
314 314
315 315 flash = _Flash()
316 316
317 317
318 318 #==============================================================================
319 319 # MERCURIAL FILTERS available via h.
320 320 #==============================================================================
321 321 from mercurial import util
322 322 from mercurial.templatefilters import person as _person
323 323
324 324 def _age(curdate):
325 325 """turns a datetime into an age string."""
326 326
327 327 if not curdate:
328 328 return ''
329 329
330 330 agescales = [("year", 3600 * 24 * 365),
331 331 ("month", 3600 * 24 * 30),
332 332 ("day", 3600 * 24),
333 333 ("hour", 3600),
334 334 ("minute", 60),
335 335 ("second", 1), ]
336 336
337 337 age = datetime.now() - curdate
338 338 age_seconds = (age.days * agescales[2][1]) + age.seconds
339 339 pos = 1
340 340 for scale in agescales:
341 341 if scale[1] <= age_seconds:
342 342 if pos == 6:pos = 5
343 343 return time_ago_in_words(curdate, agescales[pos][0]) + ' ' + _('ago')
344 344 pos += 1
345 345
346 346 return _('just now')
347 347
348 348 age = lambda x:_age(x)
349 349 capitalize = lambda x: x.capitalize()
350 350 email = util.email
351 351 email_or_none = lambda x: util.email(x) if util.email(x) != x else None
352 352 person = lambda x: _person(x)
353 353 short_id = lambda x: x[:12]
354 354
355 355
356 356 def bool2icon(value):
357 357 """Returns True/False values represented as small html image of true/false
358 358 icons
359 359
360 360 :param value: bool value
361 361 """
362 362
363 363 if value is True:
364 364 return HTML.tag('img', src=url("/images/icons/accept.png"),
365 365 alt=_('True'))
366 366
367 367 if value is False:
368 368 return HTML.tag('img', src=url("/images/icons/cancel.png"),
369 369 alt=_('False'))
370 370
371 371 return value
372 372
373 373
374 374 def action_parser(user_log, feed=False):
375 375 """This helper will action_map the specified string action into translated
376 376 fancy names with icons and links
377 377
378 378 :param user_log: user log instance
379 379 :param feed: use output for feeds (no html and fancy icons)
380 380 """
381 381
382 382 action = user_log.action
383 383 action_params = ' '
384 384
385 385 x = action.split(':')
386 386
387 387 if len(x) > 1:
388 388 action, action_params = x
389 389
390 390 def get_cs_links():
391 391 revs_limit = 5 #display this amount always
392 392 revs_top_limit = 50 #show upto this amount of changesets hidden
393 393 revs = action_params.split(',')
394 394 repo_name = user_log.repository.repo_name
395 395
396 396 from rhodecode.model.scm import ScmModel
397 397 repo, dbrepo = ScmModel().get(repo_name, retval='repo',
398 398 invalidation_list=[])
399 399
400 400 message = lambda rev: get_changeset_safe(repo, rev).message
401 401
402 402 cs_links = " " + ', '.join ([link_to(rev,
403 403 url('changeset_home',
404 404 repo_name=repo_name,
405 405 revision=rev), title=tooltip(message(rev)),
406 406 class_='tooltip') for rev in revs[:revs_limit] ])
407 407
408 408 compare_view = (' <div class="compare_view tooltip" title="%s">'
409 409 '<a href="%s">%s</a> '
410 410 '</div>' % (_('Show all combined changesets %s->%s' \
411 411 % (revs[0], revs[-1])),
412 412 url('changeset_home', repo_name=repo_name,
413 413 revision='%s...%s' % (revs[0], revs[-1])
414 414 ),
415 415 _('compare view'))
416 416 )
417 417
418 418 if len(revs) > revs_limit:
419 419 uniq_id = revs[0]
420 420 html_tmpl = ('<span> %s '
421 421 '<a class="show_more" id="_%s" href="#more">%s</a> '
422 422 '%s</span>')
423 423 if not feed:
424 424 cs_links += html_tmpl % (_('and'), uniq_id, _('%s more') \
425 425 % (len(revs) - revs_limit),
426 426 _('revisions'))
427 427
428 428 if not feed:
429 429 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
430 430 else:
431 431 html_tmpl = '<span id="%s"> %s </span>'
432 432
433 433 cs_links += html_tmpl % (uniq_id, ', '.join([link_to(rev,
434 434 url('changeset_home',
435 435 repo_name=repo_name, revision=rev),
436 436 title=message(rev), class_='tooltip')
437 437 for rev in revs[revs_limit:revs_top_limit]]))
438 438 if len(revs) > 1:
439 439 cs_links += compare_view
440 440 return cs_links
441 441
442 442 def get_fork_name():
443 443 repo_name = action_params
444 444 return _('fork name ') + str(link_to(action_params, url('summary_home',
445 445 repo_name=repo_name,)))
446 446
447 447 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
448 448 'user_created_repo':(_('[created] repository'), None),
449 449 'user_forked_repo':(_('[forked] repository'), get_fork_name),
450 450 'user_updated_repo':(_('[updated] repository'), None),
451 451 'admin_deleted_repo':(_('[delete] repository'), None),
452 452 'admin_created_repo':(_('[created] repository'), None),
453 453 'admin_forked_repo':(_('[forked] repository'), None),
454 454 'admin_updated_repo':(_('[updated] repository'), None),
455 455 'push':(_('[pushed] into'), get_cs_links),
456 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
456 457 'push_remote':(_('[pulled from remote] into'), get_cs_links),
457 458 'pull':(_('[pulled] from'), None),
458 459 'started_following_repo':(_('[started following] repository'), None),
459 460 'stopped_following_repo':(_('[stopped following] repository'), None),
460 461 }
461 462
462 463 action_str = action_map.get(action, action)
463 464 if feed:
464 465 action = action_str[0].replace('[', '').replace(']', '')
465 466 else:
466 467 action = action_str[0].replace('[', '<span class="journal_highlight">')\
467 468 .replace(']', '</span>')
468 469
469 470 action_params_func = lambda :""
470 471
471 472 if callable(action_str[1]):
472 473 action_params_func = action_str[1]
473 474
474 475 return [literal(action), action_params_func]
475 476
476 477 def action_parser_icon(user_log):
477 478 action = user_log.action
478 479 action_params = None
479 480 x = action.split(':')
480 481
481 482 if len(x) > 1:
482 483 action, action_params = x
483 484
484 485 tmpl = """<img src="%s%s" alt="%s"/>"""
485 486 map = {'user_deleted_repo':'database_delete.png',
486 487 'user_created_repo':'database_add.png',
487 488 'user_forked_repo':'arrow_divide.png',
488 489 'user_updated_repo':'database_edit.png',
489 490 'admin_deleted_repo':'database_delete.png',
490 491 'admin_created_repo':'database_add.png',
491 492 'admin_forked_repo':'arrow_divide.png',
492 493 'admin_updated_repo':'database_edit.png',
493 494 'push':'script_add.png',
495 'push_local':'script_edit.png',
494 496 'push_remote':'connect.png',
495 497 'pull':'down_16.png',
496 498 'started_following_repo':'heart_add.png',
497 499 'stopped_following_repo':'heart_delete.png',
498 500 }
499 501 return literal(tmpl % ((url('/images/icons/')),
500 502 map.get(action, action), action))
501 503
502 504
503 505 #==============================================================================
504 506 # PERMS
505 507 #==============================================================================
506 508 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
507 509 HasRepoPermissionAny, HasRepoPermissionAll
508 510
509 511 #==============================================================================
510 512 # GRAVATAR URL
511 513 #==============================================================================
512 514
513 515 def gravatar_url(email_address, size=30):
514 516 if not str2bool(config['app_conf'].get('use_gravatar')):
515 517 return "/images/user%s.png" % size
516 518
517 519 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
518 520 default = 'identicon'
519 521 baseurl_nossl = "http://www.gravatar.com/avatar/"
520 522 baseurl_ssl = "https://secure.gravatar.com/avatar/"
521 523 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
522 524
523 525 if isinstance(email_address, unicode):
524 526 #hashlib crashes on unicode items
525 527 email_address = email_address.encode('utf8', 'replace')
526 528 # construct the url
527 529 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
528 530 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
529 531
530 532 return gravatar_url
531 533
532 534
533 535 #==============================================================================
534 536 # REPO PAGER
535 537 #==============================================================================
536 538 class RepoPage(Page):
537 539
538 540 def __init__(self, collection, page=1, items_per_page=20,
539 541 item_count=None, url=None, branch_name=None, **kwargs):
540 542
541 543 """Create a "RepoPage" instance. special pager for paging
542 544 repository
543 545 """
544 546 self._url_generator = url
545 547
546 548 # Safe the kwargs class-wide so they can be used in the pager() method
547 549 self.kwargs = kwargs
548 550
549 551 # Save a reference to the collection
550 552 self.original_collection = collection
551 553
552 554 self.collection = collection
553 555
554 556 # The self.page is the number of the current page.
555 557 # The first page has the number 1!
556 558 try:
557 559 self.page = int(page) # make it int() if we get it as a string
558 560 except (ValueError, TypeError):
559 561 self.page = 1
560 562
561 563 self.items_per_page = items_per_page
562 564
563 565 # Unless the user tells us how many items the collections has
564 566 # we calculate that ourselves.
565 567 if item_count is not None:
566 568 self.item_count = item_count
567 569 else:
568 570 self.item_count = len(self.collection)
569 571
570 572 # Compute the number of the first and last available page
571 573 if self.item_count > 0:
572 574 self.first_page = 1
573 575 self.page_count = ((self.item_count - 1) / self.items_per_page) + 1
574 576 self.last_page = self.first_page + self.page_count - 1
575 577
576 578 # Make sure that the requested page number is the range of valid pages
577 579 if self.page > self.last_page:
578 580 self.page = self.last_page
579 581 elif self.page < self.first_page:
580 582 self.page = self.first_page
581 583
582 584 # Note: the number of items on this page can be less than
583 585 # items_per_page if the last page is not full
584 586 self.first_item = max(0, (self.item_count) - (self.page * items_per_page))
585 587 self.last_item = ((self.item_count - 1) - items_per_page * (self.page - 1))
586 588
587 589 iterator = self.collection.get_changesets(start=self.first_item,
588 590 end=self.last_item,
589 591 reverse=True,
590 592 branch_name=branch_name)
591 593 self.items = list(iterator)
592 594
593 595 # Links to previous and next page
594 596 if self.page > self.first_page:
595 597 self.previous_page = self.page - 1
596 598 else:
597 599 self.previous_page = None
598 600
599 601 if self.page < self.last_page:
600 602 self.next_page = self.page + 1
601 603 else:
602 604 self.next_page = None
603 605
604 606 # No items available
605 607 else:
606 608 self.first_page = None
607 609 self.page_count = 0
608 610 self.last_page = None
609 611 self.first_item = None
610 612 self.last_item = None
611 613 self.previous_page = None
612 614 self.next_page = None
613 615 self.items = []
614 616
615 617 # This is a subclass of the 'list' type. Initialise the list now.
616 618 list.__init__(self, self.items)
617 619
618 620
619 621 def changed_tooltip(nodes):
620 622 if nodes:
621 623 pref = ': <br/> '
622 624 suf = ''
623 625 if len(nodes) > 30:
624 626 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
625 627 return literal(pref + '<br/> '.join([safe_unicode(x.path)
626 628 for x in nodes[:30]]) + suf)
627 629 else:
628 630 return ': ' + _('No Files')
629 631
630 632
631 633
632 634 def repo_link(groups_and_repos):
633 635 groups, repo_name = groups_and_repos
634 636
635 637 if not groups:
636 638 return repo_name
637 639 else:
638 640 def make_link(group):
639 641 return link_to(group.group_name, url('repos_group',
640 642 id=group.group_id))
641 643 return literal(' &raquo; '.join(map(make_link, groups)) + \
642 644 " &raquo; " + repo_name)
643 645
644 646
645 647 def fancy_file_stats(stats):
646 648 a, d, t = stats[0], stats[1], stats[0] + stats[1]
647 649 width = 100
648 650 unit = float(width) / (t or 1)
649 651
650 652 a_p = max(9, unit * a) if a > 0 else 0# needs > 9% to be visible
651 653 d_p = max(9, unit * d) if d > 0 else 0 # needs > 9% to be visible
652 654 p_sum = a_p + d_p
653 655
654 656 if p_sum > width:
655 657 #adjust the percentage to be == 100% since we adjusted to 9
656 658 if a_p > d_p:
657 659 a_p = a_p - (p_sum - width)
658 660 else:
659 661 d_p = d_p - (p_sum - width)
660 662
661 663 a_v = a if a > 0 else ''
662 664 d_v = d if d > 0 else ''
663 665
664 666
665 667 def cgen(l_type):
666 668 mapping = {'tr':'top-right-rounded-corner',
667 669 'tl':'top-left-rounded-corner',
668 670 'br':'bottom-right-rounded-corner',
669 671 'bl':'bottom-left-rounded-corner'}
670 672 map_getter = lambda x:mapping[x]
671 673
672 674 if l_type == 'a' and d_v:
673 675 #case when added and deleted are present
674 676 return ' '.join(map(map_getter, ['tl', 'bl']))
675 677
676 678 if l_type == 'a' and not d_v:
677 679 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
678 680
679 681 if l_type == 'd' and a_v:
680 682 return ' '.join(map(map_getter, ['tr', 'br']))
681 683
682 684 if l_type == 'd' and not a_v:
683 685 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
684 686
685 687
686 688
687 689 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
688 690 a_p, a_v)
689 691 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
690 692 d_p, d_v)
691 693 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
@@ -1,435 +1,440 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import time
27 27 import traceback
28 28 import logging
29 29
30 30 from mercurial import ui
31 31
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.orm import make_transient
34 34
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 37 from vcs import get_backend
38 38 from vcs.utils.helpers import get_scm
39 39 from vcs.exceptions import RepositoryError, VCSError
40 40 from vcs.utils.lazy import LazyProperty
41 41 from vcs.nodes import FileNode
42 42
43 43 from rhodecode import BACKENDS
44 44 from rhodecode.lib import helpers as h
45 45 from rhodecode.lib.auth import HasRepoPermissionAny
46 46 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
47 47 action_logger
48 48 from rhodecode.model import BaseModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
52 52 UserFollowing, UserLog
53 53 from rhodecode.model.caching_query import FromCache
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UserTemp(object):
59 59 def __init__(self, user_id):
60 60 self.user_id = user_id
61 61
62 62 def __repr__(self):
63 63 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
64 64
65 65
66 66 class RepoTemp(object):
67 67 def __init__(self, repo_id):
68 68 self.repo_id = repo_id
69 69
70 70 def __repr__(self):
71 71 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
72 72
73 73
74 74 class ScmModel(BaseModel):
75 75 """Generic Scm Model
76 76 """
77 77
78 78 @LazyProperty
79 79 def repos_path(self):
80 80 """Get's the repositories root path from database
81 81 """
82 82
83 83 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
84 84
85 85 return q.ui_value
86 86
87 87 def repo_scan(self, repos_path=None):
88 88 """Listing of repositories in given path. This path should not be a
89 89 repository itself. Return a dictionary of repository objects
90 90
91 91 :param repos_path: path to directory containing repositories
92 92 """
93 93
94 94 log.info('scanning for repositories in %s', repos_path)
95 95
96 96 if repos_path is None:
97 97 repos_path = self.repos_path
98 98
99 99 baseui = make_ui('db')
100 100 repos_list = {}
101 101
102 102 for name, path in get_filesystem_repos(repos_path, recursive=True):
103 103 try:
104 104 if name in repos_list:
105 105 raise RepositoryError('Duplicate repository name %s '
106 106 'found in %s' % (name, path))
107 107 else:
108 108
109 109 klass = get_backend(path[0])
110 110
111 111 if path[0] == 'hg' and path[0] in BACKENDS.keys():
112 112 repos_list[name] = klass(path[1], baseui=baseui)
113 113
114 114 if path[0] == 'git' and path[0] in BACKENDS.keys():
115 115 repos_list[name] = klass(path[1])
116 116 except OSError:
117 117 continue
118 118
119 119 return repos_list
120 120
121 121 def get_repos(self, all_repos=None):
122 122 """Get all repos from db and for each repo create it's
123 123 backend instance and fill that backed with information from database
124 124
125 125 :param all_repos: give specific repositories list, good for filtering
126 126 this have to be a list of just the repository names
127 127 """
128 128 if all_repos is None:
129 129 repos = self.sa.query(Repository)\
130 130 .order_by(Repository.repo_name).all()
131 131 all_repos = [r.repo_name for r in repos]
132 132
133 133 #get the repositories that should be invalidated
134 134 invalidation_list = [str(x.cache_key) for x in \
135 135 self.sa.query(CacheInvalidation.cache_key)\
136 136 .filter(CacheInvalidation.cache_active == False)\
137 137 .all()]
138 138 for r_name in all_repos:
139 139 r_dbr = self.get(r_name, invalidation_list)
140 140 if r_dbr is not None:
141 141 repo, dbrepo = r_dbr
142 142
143 143 if repo is None or dbrepo is None:
144 144 log.error('Repository "%s" looks somehow corrupted '
145 145 'fs-repo:%s,db-repo:%s both values should be '
146 146 'present', r_name, repo, dbrepo)
147 147 continue
148 148 last_change = repo.last_change
149 149 tip = h.get_changeset_safe(repo, 'tip')
150 150
151 151 tmp_d = {}
152 152 tmp_d['name'] = dbrepo.repo_name
153 153 tmp_d['name_sort'] = tmp_d['name'].lower()
154 154 tmp_d['description'] = dbrepo.description
155 155 tmp_d['description_sort'] = tmp_d['description']
156 156 tmp_d['last_change'] = last_change
157 157 tmp_d['last_change_sort'] = time.mktime(last_change \
158 158 .timetuple())
159 159 tmp_d['tip'] = tip.raw_id
160 160 tmp_d['tip_sort'] = tip.revision
161 161 tmp_d['rev'] = tip.revision
162 162 tmp_d['contact'] = dbrepo.user.full_contact
163 163 tmp_d['contact_sort'] = tmp_d['contact']
164 164 tmp_d['owner_sort'] = tmp_d['contact']
165 165 tmp_d['repo_archives'] = list(repo._get_archives())
166 166 tmp_d['last_msg'] = tip.message
167 167 tmp_d['repo'] = repo
168 168 tmp_d['dbrepo'] = dbrepo.get_dict()
169 169 tmp_d['dbrepo_fork'] = dbrepo.fork.get_dict() if dbrepo.fork \
170 170 else {}
171 171 yield tmp_d
172 172
173 173 def get(self, repo_name, invalidation_list=None, retval='all'):
174 174 """Returns a tuple of Repository,DbRepository,
175 175 Get's repository from given name, creates BackendInstance and
176 176 propagates it's data from database with all additional information
177 177
178 178 :param repo_name:
179 179 :param invalidation_list: if a invalidation list is given the get
180 180 method should not manually check if this repository needs
181 181 invalidation and just invalidate the repositories in list
182 182 :param retval: string specifing what to return one of 'repo','dbrepo',
183 183 'all'if repo or dbrepo is given it'll just lazy load chosen type
184 184 and return None as the second
185 185 """
186 186 if not HasRepoPermissionAny('repository.read', 'repository.write',
187 187 'repository.admin')(repo_name, 'get repo check'):
188 188 return
189 189
190 190 #======================================================================
191 191 # CACHE FUNCTION
192 192 #======================================================================
193 193 @cache_region('long_term')
194 194 def _get_repo(repo_name):
195 195
196 196 repo_path = os.path.join(self.repos_path, repo_name)
197 197
198 198 try:
199 199 alias = get_scm(repo_path)[0]
200 200 log.debug('Creating instance of %s repository', alias)
201 201 backend = get_backend(alias)
202 202 except VCSError:
203 203 log.error(traceback.format_exc())
204 204 log.error('Perhaps this repository is in db and not in '
205 205 'filesystem run rescan repositories with '
206 206 '"destroy old data " option from admin panel')
207 207 return
208 208
209 209 if alias == 'hg':
210 210 repo = backend(repo_path, create=False, baseui=make_ui('db'))
211 211 #skip hidden web repository
212 212 if repo._get_hidden():
213 213 return
214 214 else:
215 215 repo = backend(repo_path, create=False)
216 216
217 217 return repo
218 218
219 219 pre_invalidate = True
220 220 dbinvalidate = False
221 221
222 222 if invalidation_list is not None:
223 223 pre_invalidate = repo_name in invalidation_list
224 224
225 225 if pre_invalidate:
226 226 #this returns object to invalidate
227 227 invalidate = self._should_invalidate(repo_name)
228 228 if invalidate:
229 229 log.info('invalidating cache for repository %s', repo_name)
230 230 region_invalidate(_get_repo, None, repo_name)
231 231 self._mark_invalidated(invalidate)
232 232 dbinvalidate = True
233 233
234 234 r, dbr = None, None
235 235 if retval == 'repo' or 'all':
236 236 r = _get_repo(repo_name)
237 237 if retval == 'dbrepo' or 'all':
238 238 dbr = RepoModel().get_full(repo_name, cache=True,
239 239 invalidate=dbinvalidate)
240 240
241 241 return r, dbr
242 242
243 243 def mark_for_invalidation(self, repo_name):
244 244 """Puts cache invalidation task into db for
245 245 further global cache invalidation
246 246
247 247 :param repo_name: this repo that should invalidation take place
248 248 """
249 249
250 250 log.debug('marking %s for invalidation', repo_name)
251 251 cache = self.sa.query(CacheInvalidation)\
252 252 .filter(CacheInvalidation.cache_key == repo_name).scalar()
253 253
254 254 if cache:
255 255 #mark this cache as inactive
256 256 cache.cache_active = False
257 257 else:
258 258 log.debug('cache key not found in invalidation db -> creating one')
259 259 cache = CacheInvalidation(repo_name)
260 260
261 261 try:
262 262 self.sa.add(cache)
263 263 self.sa.commit()
264 264 except (DatabaseError,):
265 265 log.error(traceback.format_exc())
266 266 self.sa.rollback()
267 267
268 268 def toggle_following_repo(self, follow_repo_id, user_id):
269 269
270 270 f = self.sa.query(UserFollowing)\
271 271 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
272 272 .filter(UserFollowing.user_id == user_id).scalar()
273 273
274 274 if f is not None:
275 275
276 276 try:
277 277 self.sa.delete(f)
278 278 self.sa.commit()
279 279 action_logger(UserTemp(user_id),
280 280 'stopped_following_repo',
281 281 RepoTemp(follow_repo_id))
282 282 return
283 283 except:
284 284 log.error(traceback.format_exc())
285 285 self.sa.rollback()
286 286 raise
287 287
288 288 try:
289 289 f = UserFollowing()
290 290 f.user_id = user_id
291 291 f.follows_repo_id = follow_repo_id
292 292 self.sa.add(f)
293 293 self.sa.commit()
294 294 action_logger(UserTemp(user_id),
295 295 'started_following_repo',
296 296 RepoTemp(follow_repo_id))
297 297 except:
298 298 log.error(traceback.format_exc())
299 299 self.sa.rollback()
300 300 raise
301 301
302 302 def toggle_following_user(self, follow_user_id, user_id):
303 303 f = self.sa.query(UserFollowing)\
304 304 .filter(UserFollowing.follows_user_id == follow_user_id)\
305 305 .filter(UserFollowing.user_id == user_id).scalar()
306 306
307 307 if f is not None:
308 308 try:
309 309 self.sa.delete(f)
310 310 self.sa.commit()
311 311 return
312 312 except:
313 313 log.error(traceback.format_exc())
314 314 self.sa.rollback()
315 315 raise
316 316
317 317 try:
318 318 f = UserFollowing()
319 319 f.user_id = user_id
320 320 f.follows_user_id = follow_user_id
321 321 self.sa.add(f)
322 322 self.sa.commit()
323 323 except:
324 324 log.error(traceback.format_exc())
325 325 self.sa.rollback()
326 326 raise
327 327
328 328 def is_following_repo(self, repo_name, user_id, cache=False):
329 329 r = self.sa.query(Repository)\
330 330 .filter(Repository.repo_name == repo_name).scalar()
331 331
332 332 f = self.sa.query(UserFollowing)\
333 333 .filter(UserFollowing.follows_repository == r)\
334 334 .filter(UserFollowing.user_id == user_id).scalar()
335 335
336 336 return f is not None
337 337
338 338 def is_following_user(self, username, user_id, cache=False):
339 339 u = UserModel(self.sa).get_by_username(username)
340 340
341 341 f = self.sa.query(UserFollowing)\
342 342 .filter(UserFollowing.follows_user == u)\
343 343 .filter(UserFollowing.user_id == user_id).scalar()
344 344
345 345 return f is not None
346 346
347 347 def get_followers(self, repo_id):
348 348 if not isinstance(repo_id, int):
349 349 repo_id = getattr(Repository.by_repo_name(repo_id), 'repo_id')
350 350
351 351 return self.sa.query(UserFollowing)\
352 352 .filter(UserFollowing.follows_repo_id == repo_id).count()
353 353
354 354 def get_forks(self, repo_id):
355 355 if not isinstance(repo_id, int):
356 356 repo_id = getattr(Repository.by_repo_name(repo_id), 'repo_id')
357 357
358 358 return self.sa.query(Repository)\
359 359 .filter(Repository.fork_id == repo_id).count()
360 360
361 361 def pull_changes(self, repo_name, username):
362 362 repo, dbrepo = self.get(repo_name, retval='all')
363 363
364 364 try:
365 365 extras = {'ip': '',
366 366 'username': username,
367 367 'action': 'push_remote',
368 368 'repository': repo_name}
369 369
370 370 #inject ui extra param to log this action via push logger
371 371 for k, v in extras.items():
372 372 repo._repo.ui.setconfig('rhodecode_extras', k, v)
373 373
374 374 repo.pull(dbrepo.clone_uri)
375 375 self.mark_for_invalidation(repo_name)
376 376 except:
377 377 log.error(traceback.format_exc())
378 378 raise
379 379
380 380
381 def commit_change(self, repo, repo_name, cs, author, message, content,
381 def commit_change(self, repo, repo_name, cs, user, author, message, content,
382 382 f_path):
383 383
384 384 if repo.alias == 'hg':
385 385 from vcs.backends.hg import MercurialInMemoryChangeset as IMC
386 386 elif repo.alias == 'git':
387 387 from vcs.backends.git import GitInMemoryChangeset as IMC
388 388
389 389 # decoding here will force that we have proper encoded values
390 390 # in any other case this will throw exceptions and deny commit
391 391 content = content.encode('utf8')
392 392 message = message.encode('utf8')
393 393 path = f_path.encode('utf8')
394 394 author = author.encode('utf8')
395 395 m = IMC(repo)
396 396 m.change(FileNode(path, content))
397 m.commit(message=message,
397 tip = m.commit(message=message,
398 398 author=author,
399 399 parents=[cs], branch=cs.branch)
400 400
401 new_cs = tip.short_id
402 action = 'push_local:%s' % new_cs
403
404 action_logger(user, action, repo_name)
405
401 406 self.mark_for_invalidation(repo_name)
402 407
403 408
404 409 def get_unread_journal(self):
405 410 return self.sa.query(UserLog).count()
406 411
407 412 def _should_invalidate(self, repo_name):
408 413 """Looks up database for invalidation signals for this repo_name
409 414
410 415 :param repo_name:
411 416 """
412 417
413 418 ret = self.sa.query(CacheInvalidation)\
414 419 .filter(CacheInvalidation.cache_key == repo_name)\
415 420 .filter(CacheInvalidation.cache_active == False)\
416 421 .scalar()
417 422
418 423 return ret
419 424
420 425 def _mark_invalidated(self, cache_key):
421 426 """ Marks all occurrences of cache to invalidation as already
422 427 invalidated
423 428
424 429 :param cache_key:
425 430 """
426 431
427 432 if cache_key:
428 433 log.debug('marking %s as already invalidated', cache_key)
429 434 try:
430 435 cache_key.cache_active = True
431 436 self.sa.add(cache_key)
432 437 self.sa.commit()
433 438 except (DatabaseError,):
434 439 log.error(traceback.format_exc())
435 440 self.sa.rollback()
General Comments 0
You need to be logged in to leave comments. Login now