##// END OF EJS Templates
fixed some unicode problems with archive downloads
marcink -
r3488:1b4fc339 beta
parent child Browse files
Show More
@@ -1,609 +1,610
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) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from rhodecode.lib.utils import jsonify
35 35
36 36 from rhodecode.lib import diffs
37 37 from rhodecode.lib import helpers as h
38 38
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
41 41 str2bool
42 42 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 43 from rhodecode.lib.base import BaseRepoController, render
44 44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 45 from rhodecode.lib.vcs.conf import settings
46 46 from rhodecode.lib.vcs.exceptions import RepositoryError, \
47 47 ChangesetDoesNotExistError, EmptyRepositoryError, \
48 48 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
49 49 NodeDoesNotExistError, ChangesetError, NodeError
50 50 from rhodecode.lib.vcs.nodes import FileNode
51 51
52 52 from rhodecode.model.repo import RepoModel
53 53 from rhodecode.model.scm import ScmModel
54 54 from rhodecode.model.db import Repository
55 55
56 56 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
57 57 _context_url, get_line_ctx, get_ignore_ws
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class FilesController(BaseRepoController):
64 64
65 65 def __before__(self):
66 66 super(FilesController, self).__before__()
67 67 c.cut_off_limit = self.cut_off_limit
68 68
69 69 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
70 70 """
71 71 Safe way to get changeset if error occur it redirects to tip with
72 72 proper message
73 73
74 74 :param rev: revision to fetch
75 75 :param repo_name: repo name to redirect after
76 76 """
77 77
78 78 try:
79 79 return c.rhodecode_repo.get_changeset(rev)
80 80 except EmptyRepositoryError, e:
81 81 if not redirect_after:
82 82 return None
83 83 url_ = url('files_add_home',
84 84 repo_name=c.repo_name,
85 85 revision=0, f_path='')
86 86 add_new = '<a href="%s">[%s]</a>' % (url_, _('click here to add new file'))
87 87 h.flash(h.literal(_('There are no files yet %s') % add_new),
88 88 category='warning')
89 89 redirect(h.url('summary_home', repo_name=repo_name))
90 90
91 91 except RepositoryError, e:
92 92 h.flash(str(e), category='warning')
93 93 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
94 94
95 95 def __get_filenode_or_redirect(self, repo_name, cs, path):
96 96 """
97 97 Returns file_node, if error occurs or given path is directory,
98 98 it'll redirect to top level path
99 99
100 100 :param repo_name: repo_name
101 101 :param cs: given changeset
102 102 :param path: path to lookup
103 103 """
104 104
105 105 try:
106 106 file_node = cs.get_node(path)
107 107 if file_node.is_dir():
108 108 raise RepositoryError('given path is a directory')
109 109 except RepositoryError, e:
110 110 h.flash(str(e), category='warning')
111 111 redirect(h.url('files_home', repo_name=repo_name,
112 112 revision=cs.raw_id))
113 113
114 114 return file_node
115 115
116 116 @LoginRequired()
117 117 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
118 118 'repository.admin')
119 119 def index(self, repo_name, revision, f_path, annotate=False):
120 120 # redirect to given revision from form if given
121 121 post_revision = request.POST.get('at_rev', None)
122 122 if post_revision:
123 123 cs = self.__get_cs_or_redirect(post_revision, repo_name)
124 124 redirect(url('files_home', repo_name=c.repo_name,
125 125 revision=cs.raw_id, f_path=f_path))
126 126
127 127 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
128 128 c.branch = request.GET.get('branch', None)
129 129 c.f_path = f_path
130 130 c.annotate = annotate
131 131 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
132 132 cur_rev = c.changeset.revision
133 133
134 134 # prev link
135 135 try:
136 136 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
137 137 c.url_prev = url('files_home', repo_name=c.repo_name,
138 138 revision=prev_rev.raw_id, f_path=f_path)
139 139 if c.branch:
140 140 c.url_prev += '?branch=%s' % c.branch
141 141 except (ChangesetDoesNotExistError, VCSError):
142 142 c.url_prev = '#'
143 143
144 144 # next link
145 145 try:
146 146 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
147 147 c.url_next = url('files_home', repo_name=c.repo_name,
148 148 revision=next_rev.raw_id, f_path=f_path)
149 149 if c.branch:
150 150 c.url_next += '?branch=%s' % c.branch
151 151 except (ChangesetDoesNotExistError, VCSError):
152 152 c.url_next = '#'
153 153
154 154 # files or dirs
155 155 try:
156 156 c.file = c.changeset.get_node(f_path)
157 157
158 158 if c.file.is_file():
159 159 c.load_full_history = False
160 160 file_last_cs = c.file.last_changeset
161 161 c.file_changeset = (c.changeset
162 162 if c.changeset.revision < file_last_cs.revision
163 163 else file_last_cs)
164 164 #determine if we're on branch head
165 165 _branches = c.rhodecode_repo.branches
166 166 c.on_branch_head = revision in _branches.keys() + _branches.values()
167 167 _hist = []
168 168 c.file_history = []
169 169 if c.load_full_history:
170 170 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
171 171
172 172 c.authors = []
173 173 for a in set([x.author for x in _hist]):
174 174 c.authors.append((h.email(a), h.person(a)))
175 175 else:
176 176 c.authors = c.file_history = []
177 177 except RepositoryError, e:
178 178 h.flash(str(e), category='warning')
179 179 redirect(h.url('files_home', repo_name=repo_name,
180 180 revision='tip'))
181 181
182 182 if request.environ.get('HTTP_X_PARTIAL_XHR'):
183 183 return render('files/files_ypjax.html')
184 184
185 185 return render('files/files.html')
186 186
187 187 def history(self, repo_name, revision, f_path, annotate=False):
188 188 if request.environ.get('HTTP_X_PARTIAL_XHR'):
189 189 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
190 190 c.f_path = f_path
191 191 c.annotate = annotate
192 192 c.file = c.changeset.get_node(f_path)
193 193 if c.file.is_file():
194 194 file_last_cs = c.file.last_changeset
195 195 c.file_changeset = (c.changeset
196 196 if c.changeset.revision < file_last_cs.revision
197 197 else file_last_cs)
198 198 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
199 199 c.authors = []
200 200 for a in set([x.author for x in _hist]):
201 201 c.authors.append((h.email(a), h.person(a)))
202 202 return render('files/files_history_box.html')
203 203
204 204 @LoginRequired()
205 205 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
206 206 'repository.admin')
207 207 def rawfile(self, repo_name, revision, f_path):
208 208 cs = self.__get_cs_or_redirect(revision, repo_name)
209 209 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
210 210
211 211 response.content_disposition = 'attachment; filename=%s' % \
212 212 safe_str(f_path.split(Repository.url_sep())[-1])
213 213
214 214 response.content_type = file_node.mimetype
215 215 return file_node.content
216 216
217 217 @LoginRequired()
218 218 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
219 219 'repository.admin')
220 220 def raw(self, repo_name, revision, f_path):
221 221 cs = self.__get_cs_or_redirect(revision, repo_name)
222 222 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
223 223
224 224 raw_mimetype_mapping = {
225 225 # map original mimetype to a mimetype used for "show as raw"
226 226 # you can also provide a content-disposition to override the
227 227 # default "attachment" disposition.
228 228 # orig_type: (new_type, new_dispo)
229 229
230 230 # show images inline:
231 231 'image/x-icon': ('image/x-icon', 'inline'),
232 232 'image/png': ('image/png', 'inline'),
233 233 'image/gif': ('image/gif', 'inline'),
234 234 'image/jpeg': ('image/jpeg', 'inline'),
235 235 'image/svg+xml': ('image/svg+xml', 'inline'),
236 236 }
237 237
238 238 mimetype = file_node.mimetype
239 239 try:
240 240 mimetype, dispo = raw_mimetype_mapping[mimetype]
241 241 except KeyError:
242 242 # we don't know anything special about this, handle it safely
243 243 if file_node.is_binary:
244 244 # do same as download raw for binary files
245 245 mimetype, dispo = 'application/octet-stream', 'attachment'
246 246 else:
247 247 # do not just use the original mimetype, but force text/plain,
248 248 # otherwise it would serve text/html and that might be unsafe.
249 249 # Note: underlying vcs library fakes text/plain mimetype if the
250 250 # mimetype can not be determined and it thinks it is not
251 251 # binary.This might lead to erroneous text display in some
252 252 # cases, but helps in other cases, like with text files
253 253 # without extension.
254 254 mimetype, dispo = 'text/plain', 'inline'
255 255
256 256 if dispo == 'attachment':
257 257 dispo = 'attachment; filename=%s' % \
258 258 safe_str(f_path.split(os.sep)[-1])
259 259
260 260 response.content_disposition = dispo
261 261 response.content_type = mimetype
262 262 return file_node.content
263 263
264 264 @LoginRequired()
265 265 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
266 266 def edit(self, repo_name, revision, f_path):
267 267 repo = c.rhodecode_db_repo
268 268 if repo.enable_locking and repo.locked[0]:
269 269 h.flash(_('This repository is has been locked by %s on %s')
270 270 % (h.person_by_id(repo.locked[0]),
271 271 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
272 272 'warning')
273 273 return redirect(h.url('files_home',
274 274 repo_name=repo_name, revision='tip'))
275 275
276 276 # check if revision is a branch identifier- basically we cannot
277 277 # create multiple heads via file editing
278 278 _branches = repo.scm_instance.branches
279 279 # check if revision is a branch name or branch hash
280 280 if revision not in _branches.keys() + _branches.values():
281 281 h.flash(_('You can only edit files with revision '
282 282 'being a valid branch '), category='warning')
283 283 return redirect(h.url('files_home',
284 284 repo_name=repo_name, revision='tip',
285 285 f_path=f_path))
286 286
287 287 r_post = request.POST
288 288
289 289 c.cs = self.__get_cs_or_redirect(revision, repo_name)
290 290 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
291 291
292 292 if c.file.is_binary:
293 293 return redirect(url('files_home', repo_name=c.repo_name,
294 294 revision=c.cs.raw_id, f_path=f_path))
295 295 c.default_message = _('Edited file %s via RhodeCode') % (f_path)
296 296 c.f_path = f_path
297 297
298 298 if r_post:
299 299
300 300 old_content = c.file.content
301 301 sl = old_content.splitlines(1)
302 302 first_line = sl[0] if sl else ''
303 303 # modes: 0 - Unix, 1 - Mac, 2 - DOS
304 304 mode = detect_mode(first_line, 0)
305 305 content = convert_line_endings(r_post.get('content'), mode)
306 306
307 307 message = r_post.get('message') or c.default_message
308 308 author = self.rhodecode_user.full_contact
309 309
310 310 if content == old_content:
311 311 h.flash(_('No changes'),
312 312 category='warning')
313 313 return redirect(url('changeset_home', repo_name=c.repo_name,
314 314 revision='tip'))
315 315 try:
316 316 self.scm_model.commit_change(repo=c.rhodecode_repo,
317 317 repo_name=repo_name, cs=c.cs,
318 318 user=self.rhodecode_user.user_id,
319 319 author=author, message=message,
320 320 content=content, f_path=f_path)
321 321 h.flash(_('Successfully committed to %s') % f_path,
322 322 category='success')
323 323
324 324 except Exception:
325 325 log.error(traceback.format_exc())
326 326 h.flash(_('Error occurred during commit'), category='error')
327 327 return redirect(url('changeset_home',
328 328 repo_name=c.repo_name, revision='tip'))
329 329
330 330 return render('files/files_edit.html')
331 331
332 332 @LoginRequired()
333 333 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
334 334 def add(self, repo_name, revision, f_path):
335 335
336 336 repo = Repository.get_by_repo_name(repo_name)
337 337 if repo.enable_locking and repo.locked[0]:
338 338 h.flash(_('This repository is has been locked by %s on %s')
339 339 % (h.person_by_id(repo.locked[0]),
340 340 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
341 341 'warning')
342 342 return redirect(h.url('files_home',
343 343 repo_name=repo_name, revision='tip'))
344 344
345 345 r_post = request.POST
346 346 c.cs = self.__get_cs_or_redirect(revision, repo_name,
347 347 redirect_after=False)
348 348 if c.cs is None:
349 349 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
350 350 c.default_message = (_('Added file via RhodeCode'))
351 351 c.f_path = f_path
352 352
353 353 if r_post:
354 354 unix_mode = 0
355 355 content = convert_line_endings(r_post.get('content'), unix_mode)
356 356
357 357 message = r_post.get('message') or c.default_message
358 358 location = r_post.get('location')
359 359 filename = r_post.get('filename')
360 360 file_obj = r_post.get('upload_file', None)
361 361
362 362 if file_obj is not None and hasattr(file_obj, 'filename'):
363 363 filename = file_obj.filename
364 364 content = file_obj.file
365 365
366 366 node_path = os.path.join(location, filename)
367 367 author = self.rhodecode_user.full_contact
368 368
369 369 if not content:
370 370 h.flash(_('No content'), category='warning')
371 371 return redirect(url('changeset_home', repo_name=c.repo_name,
372 372 revision='tip'))
373 373 if not filename:
374 374 h.flash(_('No filename'), category='warning')
375 375 return redirect(url('changeset_home', repo_name=c.repo_name,
376 376 revision='tip'))
377 377
378 378 try:
379 379 self.scm_model.create_node(repo=c.rhodecode_repo,
380 380 repo_name=repo_name, cs=c.cs,
381 381 user=self.rhodecode_user.user_id,
382 382 author=author, message=message,
383 383 content=content, f_path=node_path)
384 384 h.flash(_('Successfully committed to %s') % node_path,
385 385 category='success')
386 386 except NodeAlreadyExistsError, e:
387 387 h.flash(_(e), category='error')
388 388 except Exception:
389 389 log.error(traceback.format_exc())
390 390 h.flash(_('Error occurred during commit'), category='error')
391 391 return redirect(url('changeset_home',
392 392 repo_name=c.repo_name, revision='tip'))
393 393
394 394 return render('files/files_add.html')
395 395
396 396 @LoginRequired()
397 397 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 398 'repository.admin')
399 399 def archivefile(self, repo_name, fname):
400 400
401 401 fileformat = None
402 402 revision = None
403 403 ext = None
404 404 subrepos = request.GET.get('subrepos') == 'true'
405 405
406 406 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
407 407 archive_spec = fname.split(ext_data[1])
408 408 if len(archive_spec) == 2 and archive_spec[1] == '':
409 409 fileformat = a_type or ext_data[1]
410 410 revision = archive_spec[0]
411 411 ext = ext_data[1]
412 412
413 413 try:
414 414 dbrepo = RepoModel().get_by_repo_name(repo_name)
415 415 if dbrepo.enable_downloads is False:
416 416 return _('downloads disabled')
417 417
418 418 if c.rhodecode_repo.alias == 'hg':
419 419 # patch and reset hooks section of UI config to not run any
420 420 # hooks on fetching archives with subrepos
421 421 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
422 422 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
423 423
424 424 cs = c.rhodecode_repo.get_changeset(revision)
425 425 content_type = settings.ARCHIVE_SPECS[fileformat][0]
426 426 except ChangesetDoesNotExistError:
427 427 return _('Unknown revision %s') % revision
428 428 except EmptyRepositoryError:
429 429 return _('Empty repository')
430 430 except (ImproperArchiveTypeError, KeyError):
431 431 return _('Unknown archive type')
432 432
433 433 fd, archive = tempfile.mkstemp()
434 434 t = open(archive, 'wb')
435 435 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
436 436 t.close()
437 437
438 438 def get_chunked_archive(archive):
439 439 stream = open(archive, 'rb')
440 440 while True:
441 441 data = stream.read(16 * 1024)
442 442 if not data:
443 443 stream.close()
444 444 os.close(fd)
445 445 os.remove(archive)
446 446 break
447 447 yield data
448 448
449 449 response.content_disposition = str('attachment; filename=%s-%s%s' \
450 % (repo_name, revision[:12], ext))
450 % (safe_str(repo_name),
451 safe_str(revision), ext))
451 452 response.content_type = str(content_type)
452 453 return get_chunked_archive(archive)
453 454
454 455 @LoginRequired()
455 456 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 457 'repository.admin')
457 458 def diff(self, repo_name, f_path):
458 459 ignore_whitespace = request.GET.get('ignorews') == '1'
459 460 line_context = request.GET.get('context', 3)
460 461 diff1 = request.GET.get('diff1', '')
461 462 diff2 = request.GET.get('diff2', '')
462 463 c.action = request.GET.get('diff')
463 464 c.no_changes = diff1 == diff2
464 465 c.f_path = f_path
465 466 c.big_diff = False
466 467 c.anchor_url = anchor_url
467 468 c.ignorews_url = _ignorews_url
468 469 c.context_url = _context_url
469 470 c.changes = OrderedDict()
470 471 c.changes[diff2] = []
471 472
472 473 #special case if we want a show rev only, it's impl here
473 474 #to reduce JS and callbacks
474 475
475 476 if request.GET.get('show_rev'):
476 477 if str2bool(request.GET.get('annotate', 'False')):
477 478 _url = url('files_annotate_home', repo_name=c.repo_name,
478 479 revision=diff1, f_path=c.f_path)
479 480 else:
480 481 _url = url('files_home', repo_name=c.repo_name,
481 482 revision=diff1, f_path=c.f_path)
482 483
483 484 return redirect(_url)
484 485 try:
485 486 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
486 487 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
487 488 try:
488 489 node1 = c.changeset_1.get_node(f_path)
489 490 if node1.is_dir():
490 491 raise NodeError('%s path is a %s not a file' % (node1, type(node1)))
491 492 except NodeDoesNotExistError:
492 493 c.changeset_1 = EmptyChangeset(cs=diff1,
493 494 revision=c.changeset_1.revision,
494 495 repo=c.rhodecode_repo)
495 496 node1 = FileNode(f_path, '', changeset=c.changeset_1)
496 497 else:
497 498 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
498 499 node1 = FileNode(f_path, '', changeset=c.changeset_1)
499 500
500 501 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
501 502 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
502 503 try:
503 504 node2 = c.changeset_2.get_node(f_path)
504 505 raise NodeError('%s path is a %s not a file' % (node2, type(node2)))
505 506 except NodeDoesNotExistError:
506 507 c.changeset_2 = EmptyChangeset(cs=diff2,
507 508 revision=c.changeset_2.revision,
508 509 repo=c.rhodecode_repo)
509 510 node2 = FileNode(f_path, '', changeset=c.changeset_2)
510 511 else:
511 512 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
512 513 node2 = FileNode(f_path, '', changeset=c.changeset_2)
513 514 except (RepositoryError, NodeError):
514 515 log.error(traceback.format_exc())
515 516 return redirect(url('files_home', repo_name=c.repo_name,
516 517 f_path=f_path))
517 518
518 519 if c.action == 'download':
519 520 _diff = diffs.get_gitdiff(node1, node2,
520 521 ignore_whitespace=ignore_whitespace,
521 522 context=line_context)
522 523 diff = diffs.DiffProcessor(_diff, format='gitdiff')
523 524
524 525 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
525 526 response.content_type = 'text/plain'
526 527 response.content_disposition = (
527 528 'attachment; filename=%s' % diff_name
528 529 )
529 530 return diff.as_raw()
530 531
531 532 elif c.action == 'raw':
532 533 _diff = diffs.get_gitdiff(node1, node2,
533 534 ignore_whitespace=ignore_whitespace,
534 535 context=line_context)
535 536 diff = diffs.DiffProcessor(_diff, format='gitdiff')
536 537 response.content_type = 'text/plain'
537 538 return diff.as_raw()
538 539
539 540 else:
540 541 fid = h.FID(diff2, node2.path)
541 542 line_context_lcl = get_line_ctx(fid, request.GET)
542 543 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
543 544
544 545 lim = request.GET.get('fulldiff') or self.cut_off_limit
545 546 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
546 547 filenode_new=node2,
547 548 cut_off_limit=lim,
548 549 ignore_whitespace=ign_whitespace_lcl,
549 550 line_context=line_context_lcl,
550 551 enable_comments=False)
551 552 op = ''
552 553 filename = node1.path
553 554 cs_changes = {
554 555 'fid': [cs1, cs2, op, filename, diff, st]
555 556 }
556 557 c.changes = cs_changes
557 558
558 559 return render('files/file_diff.html')
559 560
560 561 def _get_node_history(self, cs, f_path, changesets=None):
561 562 """
562 563 get changesets history for given node
563 564
564 565 :param cs: changeset to calculate history
565 566 :param f_path: path for node to calculate history for
566 567 :param changesets: if passed don't calculate history and take
567 568 changesets defined in this list
568 569 """
569 570 # calculate history based on tip
570 571 tip_cs = c.rhodecode_repo.get_changeset()
571 572 if changesets is None:
572 573 try:
573 574 changesets = tip_cs.get_file_history(f_path)
574 575 except (NodeDoesNotExistError, ChangesetError):
575 576 #this node is not present at tip !
576 577 changesets = cs.get_file_history(f_path)
577 578 hist_l = []
578 579
579 580 changesets_group = ([], _("Changesets"))
580 581 branches_group = ([], _("Branches"))
581 582 tags_group = ([], _("Tags"))
582 583 _hg = cs.repository.alias == 'hg'
583 584 for chs in changesets:
584 585 #_branch = '(%s)' % chs.branch if _hg else ''
585 586 _branch = chs.branch
586 587 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, _branch)
587 588 changesets_group[0].append((chs.raw_id, n_desc,))
588 589 hist_l.append(changesets_group)
589 590
590 591 for name, chs in c.rhodecode_repo.branches.items():
591 592 branches_group[0].append((chs, name),)
592 593 hist_l.append(branches_group)
593 594
594 595 for name, chs in c.rhodecode_repo.tags.items():
595 596 tags_group[0].append((chs, name),)
596 597 hist_l.append(tags_group)
597 598
598 599 return hist_l, changesets
599 600
600 601 @LoginRequired()
601 602 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
602 603 'repository.admin')
603 604 @jsonify
604 605 def nodelist(self, repo_name, revision, f_path):
605 606 if request.environ.get('HTTP_X_PARTIAL_XHR'):
606 607 cs = self.__get_cs_or_redirect(revision, repo_name)
607 608 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
608 609 flat=False)
609 610 return {'nodes': _d + _f}
@@ -1,2053 +1,2053
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48 48
49 49 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
50 50 safe_unicode, remove_suffix, remove_prefix
51 51 from rhodecode.lib.compat import json
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model.meta import Base, Session
55 55
56 56 URL_SEP = '/'
57 57 log = logging.getLogger(__name__)
58 58
59 59 #==============================================================================
60 60 # BASE CLASSES
61 61 #==============================================================================
62 62
63 63 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
64 64
65 65
66 66 class BaseModel(object):
67 67 """
68 68 Base Model for all classess
69 69 """
70 70
71 71 @classmethod
72 72 def _get_keys(cls):
73 73 """return column names for this model """
74 74 return class_mapper(cls).c.keys()
75 75
76 76 def get_dict(self):
77 77 """
78 78 return dict with keys and values corresponding
79 79 to this model data """
80 80
81 81 d = {}
82 82 for k in self._get_keys():
83 83 d[k] = getattr(self, k)
84 84
85 85 # also use __json__() if present to get additional fields
86 86 _json_attr = getattr(self, '__json__', None)
87 87 if _json_attr:
88 88 # update with attributes from __json__
89 89 if callable(_json_attr):
90 90 _json_attr = _json_attr()
91 91 for k, val in _json_attr.iteritems():
92 92 d[k] = val
93 93 return d
94 94
95 95 def get_appstruct(self):
96 96 """return list with keys and values tupples corresponding
97 97 to this model data """
98 98
99 99 l = []
100 100 for k in self._get_keys():
101 101 l.append((k, getattr(self, k),))
102 102 return l
103 103
104 104 def populate_obj(self, populate_dict):
105 105 """populate model with data from given populate_dict"""
106 106
107 107 for k in self._get_keys():
108 108 if k in populate_dict:
109 109 setattr(self, k, populate_dict[k])
110 110
111 111 @classmethod
112 112 def query(cls):
113 113 return Session().query(cls)
114 114
115 115 @classmethod
116 116 def get(cls, id_):
117 117 if id_:
118 118 return cls.query().get(id_)
119 119
120 120 @classmethod
121 121 def get_or_404(cls, id_):
122 122 try:
123 123 id_ = int(id_)
124 124 except (TypeError, ValueError):
125 125 raise HTTPNotFound
126 126
127 127 res = cls.query().get(id_)
128 128 if not res:
129 129 raise HTTPNotFound
130 130 return res
131 131
132 132 @classmethod
133 133 def getAll(cls):
134 134 return cls.query().all()
135 135
136 136 @classmethod
137 137 def delete(cls, id_):
138 138 obj = cls.query().get(id_)
139 139 Session().delete(obj)
140 140
141 141 def __repr__(self):
142 142 if hasattr(self, '__unicode__'):
143 143 # python repr needs to return str
144 144 return safe_str(self.__unicode__())
145 145 return '<DB:%s>' % (self.__class__.__name__)
146 146
147 147
148 148 class RhodeCodeSetting(Base, BaseModel):
149 149 __tablename__ = 'rhodecode_settings'
150 150 __table_args__ = (
151 151 UniqueConstraint('app_settings_name'),
152 152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
153 153 'mysql_charset': 'utf8'}
154 154 )
155 155 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
156 156 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 157 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
158 158
159 159 def __init__(self, k='', v=''):
160 160 self.app_settings_name = k
161 161 self.app_settings_value = v
162 162
163 163 @validates('_app_settings_value')
164 164 def validate_settings_value(self, key, val):
165 165 assert type(val) == unicode
166 166 return val
167 167
168 168 @hybrid_property
169 169 def app_settings_value(self):
170 170 v = self._app_settings_value
171 171 if self.app_settings_name in ["ldap_active",
172 172 "default_repo_enable_statistics",
173 173 "default_repo_enable_locking",
174 174 "default_repo_private",
175 175 "default_repo_enable_downloads"]:
176 176 v = str2bool(v)
177 177 return v
178 178
179 179 @app_settings_value.setter
180 180 def app_settings_value(self, val):
181 181 """
182 182 Setter that will always make sure we use unicode in app_settings_value
183 183
184 184 :param val:
185 185 """
186 186 self._app_settings_value = safe_unicode(val)
187 187
188 188 def __unicode__(self):
189 189 return u"<%s('%s:%s')>" % (
190 190 self.__class__.__name__,
191 191 self.app_settings_name, self.app_settings_value
192 192 )
193 193
194 194 @classmethod
195 195 def get_by_name(cls, key):
196 196 return cls.query()\
197 197 .filter(cls.app_settings_name == key).scalar()
198 198
199 199 @classmethod
200 200 def get_by_name_or_create(cls, key):
201 201 res = cls.get_by_name(key)
202 202 if not res:
203 203 res = cls(key)
204 204 return res
205 205
206 206 @classmethod
207 207 def get_app_settings(cls, cache=False):
208 208
209 209 ret = cls.query()
210 210
211 211 if cache:
212 212 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
213 213
214 214 if not ret:
215 215 raise Exception('Could not get application settings !')
216 216 settings = {}
217 217 for each in ret:
218 218 settings['rhodecode_' + each.app_settings_name] = \
219 219 each.app_settings_value
220 220
221 221 return settings
222 222
223 223 @classmethod
224 224 def get_ldap_settings(cls, cache=False):
225 225 ret = cls.query()\
226 226 .filter(cls.app_settings_name.startswith('ldap_')).all()
227 227 fd = {}
228 228 for row in ret:
229 229 fd.update({row.app_settings_name: row.app_settings_value})
230 230
231 231 return fd
232 232
233 233 @classmethod
234 234 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
235 235 ret = cls.query()\
236 236 .filter(cls.app_settings_name.startswith('default_')).all()
237 237 fd = {}
238 238 for row in ret:
239 239 key = row.app_settings_name
240 240 if strip_prefix:
241 241 key = remove_prefix(key, prefix='default_')
242 242 fd.update({key: row.app_settings_value})
243 243
244 244 return fd
245 245
246 246
247 247 class RhodeCodeUi(Base, BaseModel):
248 248 __tablename__ = 'rhodecode_ui'
249 249 __table_args__ = (
250 250 UniqueConstraint('ui_key'),
251 251 {'extend_existing': True, 'mysql_engine': 'InnoDB',
252 252 'mysql_charset': 'utf8'}
253 253 )
254 254
255 255 HOOK_UPDATE = 'changegroup.update'
256 256 HOOK_REPO_SIZE = 'changegroup.repo_size'
257 257 HOOK_PUSH = 'changegroup.push_logger'
258 258 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
259 259 HOOK_PULL = 'outgoing.pull_logger'
260 260 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
261 261
262 262 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
263 263 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 264 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 265 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
266 266 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
267 267
268 268 @classmethod
269 269 def get_by_key(cls, key):
270 270 return cls.query().filter(cls.ui_key == key).scalar()
271 271
272 272 @classmethod
273 273 def get_builtin_hooks(cls):
274 274 q = cls.query()
275 275 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
276 276 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
277 277 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
278 278 return q.all()
279 279
280 280 @classmethod
281 281 def get_custom_hooks(cls):
282 282 q = cls.query()
283 283 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
284 284 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
285 285 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
286 286 q = q.filter(cls.ui_section == 'hooks')
287 287 return q.all()
288 288
289 289 @classmethod
290 290 def get_repos_location(cls):
291 291 return cls.get_by_key('/').ui_value
292 292
293 293 @classmethod
294 294 def create_or_update_hook(cls, key, val):
295 295 new_ui = cls.get_by_key(key) or cls()
296 296 new_ui.ui_section = 'hooks'
297 297 new_ui.ui_active = True
298 298 new_ui.ui_key = key
299 299 new_ui.ui_value = val
300 300
301 301 Session().add(new_ui)
302 302
303 303 def __repr__(self):
304 304 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
305 305 self.ui_value)
306 306
307 307
308 308 class User(Base, BaseModel):
309 309 __tablename__ = 'users'
310 310 __table_args__ = (
311 311 UniqueConstraint('username'), UniqueConstraint('email'),
312 312 Index('u_username_idx', 'username'),
313 313 Index('u_email_idx', 'email'),
314 314 {'extend_existing': True, 'mysql_engine': 'InnoDB',
315 315 'mysql_charset': 'utf8'}
316 316 )
317 317 DEFAULT_USER = 'default'
318 318 DEFAULT_PERMISSIONS = [
319 319 'hg.register.manual_activate', 'hg.create.repository',
320 320 'hg.fork.repository', 'repository.read', 'group.read'
321 321 ]
322 322 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
323 323 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 324 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
325 325 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
326 326 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
327 327 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 328 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 329 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
330 330 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
331 331 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
333 333 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
334 334
335 335 user_log = relationship('UserLog')
336 336 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
337 337
338 338 repositories = relationship('Repository')
339 339 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
340 340 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
341 341
342 342 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
343 343 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
344 344
345 345 group_member = relationship('UserGroupMember', cascade='all')
346 346
347 347 notifications = relationship('UserNotification', cascade='all')
348 348 # notifications assigned to this user
349 349 user_created_notifications = relationship('Notification', cascade='all')
350 350 # comments created by this user
351 351 user_comments = relationship('ChangesetComment', cascade='all')
352 352 #extra emails for this user
353 353 user_emails = relationship('UserEmailMap', cascade='all')
354 354
355 355 @hybrid_property
356 356 def email(self):
357 357 return self._email
358 358
359 359 @email.setter
360 360 def email(self, val):
361 361 self._email = val.lower() if val else None
362 362
363 363 @property
364 364 def firstname(self):
365 365 # alias for future
366 366 return self.name
367 367
368 368 @property
369 369 def emails(self):
370 370 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
371 371 return [self.email] + [x.email for x in other]
372 372
373 373 @property
374 374 def ip_addresses(self):
375 375 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
376 376 return [x.ip_addr for x in ret]
377 377
378 378 @property
379 379 def username_and_name(self):
380 380 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
381 381
382 382 @property
383 383 def full_name(self):
384 384 return '%s %s' % (self.firstname, self.lastname)
385 385
386 386 @property
387 387 def full_name_or_username(self):
388 388 return ('%s %s' % (self.firstname, self.lastname)
389 389 if (self.firstname and self.lastname) else self.username)
390 390
391 391 @property
392 392 def full_contact(self):
393 393 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
394 394
395 395 @property
396 396 def short_contact(self):
397 397 return '%s %s' % (self.firstname, self.lastname)
398 398
399 399 @property
400 400 def is_admin(self):
401 401 return self.admin
402 402
403 403 @property
404 404 def AuthUser(self):
405 405 """
406 406 Returns instance of AuthUser for this user
407 407 """
408 408 from rhodecode.lib.auth import AuthUser
409 409 return AuthUser(user_id=self.user_id, api_key=self.api_key,
410 410 username=self.username)
411 411
412 412 def __unicode__(self):
413 413 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
414 414 self.user_id, self.username)
415 415
416 416 @classmethod
417 417 def get_by_username(cls, username, case_insensitive=False, cache=False):
418 418 if case_insensitive:
419 419 q = cls.query().filter(cls.username.ilike(username))
420 420 else:
421 421 q = cls.query().filter(cls.username == username)
422 422
423 423 if cache:
424 424 q = q.options(FromCache(
425 425 "sql_cache_short",
426 426 "get_user_%s" % _hash_key(username)
427 427 )
428 428 )
429 429 return q.scalar()
430 430
431 431 @classmethod
432 432 def get_by_api_key(cls, api_key, cache=False):
433 433 q = cls.query().filter(cls.api_key == api_key)
434 434
435 435 if cache:
436 436 q = q.options(FromCache("sql_cache_short",
437 437 "get_api_key_%s" % api_key))
438 438 return q.scalar()
439 439
440 440 @classmethod
441 441 def get_by_email(cls, email, case_insensitive=False, cache=False):
442 442 if case_insensitive:
443 443 q = cls.query().filter(cls.email.ilike(email))
444 444 else:
445 445 q = cls.query().filter(cls.email == email)
446 446
447 447 if cache:
448 448 q = q.options(FromCache("sql_cache_short",
449 449 "get_email_key_%s" % email))
450 450
451 451 ret = q.scalar()
452 452 if ret is None:
453 453 q = UserEmailMap.query()
454 454 # try fetching in alternate email map
455 455 if case_insensitive:
456 456 q = q.filter(UserEmailMap.email.ilike(email))
457 457 else:
458 458 q = q.filter(UserEmailMap.email == email)
459 459 q = q.options(joinedload(UserEmailMap.user))
460 460 if cache:
461 461 q = q.options(FromCache("sql_cache_short",
462 462 "get_email_map_key_%s" % email))
463 463 ret = getattr(q.scalar(), 'user', None)
464 464
465 465 return ret
466 466
467 467 @classmethod
468 468 def get_from_cs_author(cls, author):
469 469 """
470 470 Tries to get User objects out of commit author string
471 471
472 472 :param author:
473 473 """
474 474 from rhodecode.lib.helpers import email, author_name
475 475 # Valid email in the attribute passed, see if they're in the system
476 476 _email = email(author)
477 477 if _email:
478 478 user = cls.get_by_email(_email, case_insensitive=True)
479 479 if user:
480 480 return user
481 481 # Maybe we can match by username?
482 482 _author = author_name(author)
483 483 user = cls.get_by_username(_author, case_insensitive=True)
484 484 if user:
485 485 return user
486 486
487 487 def update_lastlogin(self):
488 488 """Update user lastlogin"""
489 489 self.last_login = datetime.datetime.now()
490 490 Session().add(self)
491 491 log.debug('updated user %s lastlogin' % self.username)
492 492
493 493 def get_api_data(self):
494 494 """
495 495 Common function for generating user related data for API
496 496 """
497 497 user = self
498 498 data = dict(
499 499 user_id=user.user_id,
500 500 username=user.username,
501 501 firstname=user.name,
502 502 lastname=user.lastname,
503 503 email=user.email,
504 504 emails=user.emails,
505 505 api_key=user.api_key,
506 506 active=user.active,
507 507 admin=user.admin,
508 508 ldap_dn=user.ldap_dn,
509 509 last_login=user.last_login,
510 510 ip_addresses=user.ip_addresses
511 511 )
512 512 return data
513 513
514 514 def __json__(self):
515 515 data = dict(
516 516 full_name=self.full_name,
517 517 full_name_or_username=self.full_name_or_username,
518 518 short_contact=self.short_contact,
519 519 full_contact=self.full_contact
520 520 )
521 521 data.update(self.get_api_data())
522 522 return data
523 523
524 524
525 525 class UserEmailMap(Base, BaseModel):
526 526 __tablename__ = 'user_email_map'
527 527 __table_args__ = (
528 528 Index('uem_email_idx', 'email'),
529 529 UniqueConstraint('email'),
530 530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
531 531 'mysql_charset': 'utf8'}
532 532 )
533 533 __mapper_args__ = {}
534 534
535 535 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
536 536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
537 537 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
538 538 user = relationship('User', lazy='joined')
539 539
540 540 @validates('_email')
541 541 def validate_email(self, key, email):
542 542 # check if this email is not main one
543 543 main_email = Session().query(User).filter(User.email == email).scalar()
544 544 if main_email is not None:
545 545 raise AttributeError('email %s is present is user table' % email)
546 546 return email
547 547
548 548 @hybrid_property
549 549 def email(self):
550 550 return self._email
551 551
552 552 @email.setter
553 553 def email(self, val):
554 554 self._email = val.lower() if val else None
555 555
556 556
557 557 class UserIpMap(Base, BaseModel):
558 558 __tablename__ = 'user_ip_map'
559 559 __table_args__ = (
560 560 UniqueConstraint('user_id', 'ip_addr'),
561 561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
562 562 'mysql_charset': 'utf8'}
563 563 )
564 564 __mapper_args__ = {}
565 565
566 566 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
567 567 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
568 568 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
569 569 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
570 570 user = relationship('User', lazy='joined')
571 571
572 572 @classmethod
573 573 def _get_ip_range(cls, ip_addr):
574 574 from rhodecode.lib import ipaddr
575 575 net = ipaddr.IPNetwork(address=ip_addr)
576 576 return [str(net.network), str(net.broadcast)]
577 577
578 578 def __json__(self):
579 579 return dict(
580 580 ip_addr=self.ip_addr,
581 581 ip_range=self._get_ip_range(self.ip_addr)
582 582 )
583 583
584 584
585 585 class UserLog(Base, BaseModel):
586 586 __tablename__ = 'user_logs'
587 587 __table_args__ = (
588 588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
589 589 'mysql_charset': 'utf8'},
590 590 )
591 591 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
592 592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
593 593 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
594 594 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
595 595 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
596 596 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
597 597 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
598 598 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 599
600 600 @property
601 601 def action_as_day(self):
602 602 return datetime.date(*self.action_date.timetuple()[:3])
603 603
604 604 user = relationship('User')
605 605 repository = relationship('Repository', cascade='')
606 606
607 607
608 608 class UserGroup(Base, BaseModel):
609 609 __tablename__ = 'users_groups'
610 610 __table_args__ = (
611 611 {'extend_existing': True, 'mysql_engine': 'InnoDB',
612 612 'mysql_charset': 'utf8'},
613 613 )
614 614
615 615 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
616 616 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
617 617 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
618 618 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
619 619
620 620 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
621 621 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
622 622 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
623 623
624 624 def __unicode__(self):
625 625 return u'<userGroup(%s)>' % (self.users_group_name)
626 626
627 627 @classmethod
628 628 def get_by_group_name(cls, group_name, cache=False,
629 629 case_insensitive=False):
630 630 if case_insensitive:
631 631 q = cls.query().filter(cls.users_group_name.ilike(group_name))
632 632 else:
633 633 q = cls.query().filter(cls.users_group_name == group_name)
634 634 if cache:
635 635 q = q.options(FromCache(
636 636 "sql_cache_short",
637 637 "get_user_%s" % _hash_key(group_name)
638 638 )
639 639 )
640 640 return q.scalar()
641 641
642 642 @classmethod
643 643 def get(cls, users_group_id, cache=False):
644 644 users_group = cls.query()
645 645 if cache:
646 646 users_group = users_group.options(FromCache("sql_cache_short",
647 647 "get_users_group_%s" % users_group_id))
648 648 return users_group.get(users_group_id)
649 649
650 650 def get_api_data(self):
651 651 users_group = self
652 652
653 653 data = dict(
654 654 users_group_id=users_group.users_group_id,
655 655 group_name=users_group.users_group_name,
656 656 active=users_group.users_group_active,
657 657 )
658 658
659 659 return data
660 660
661 661
662 662 class UserGroupMember(Base, BaseModel):
663 663 __tablename__ = 'users_groups_members'
664 664 __table_args__ = (
665 665 {'extend_existing': True, 'mysql_engine': 'InnoDB',
666 666 'mysql_charset': 'utf8'},
667 667 )
668 668
669 669 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
670 670 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
671 671 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
672 672
673 673 user = relationship('User', lazy='joined')
674 674 users_group = relationship('UserGroup')
675 675
676 676 def __init__(self, gr_id='', u_id=''):
677 677 self.users_group_id = gr_id
678 678 self.user_id = u_id
679 679
680 680
681 681 class RepositoryField(Base, BaseModel):
682 682 __tablename__ = 'repositories_fields'
683 683 __table_args__ = (
684 684 UniqueConstraint('repository_id', 'field_key'), # no-multi field
685 685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
686 686 'mysql_charset': 'utf8'},
687 687 )
688 688 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
689 689
690 690 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
691 691 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
692 692 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
693 693 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
694 694 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
695 695 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
696 696 field_type = Column("field_type", String(256), nullable=False, unique=None)
697 697 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
698 698
699 699 repository = relationship('Repository')
700 700
701 701 @property
702 702 def field_key_prefixed(self):
703 703 return 'ex_%s' % self.field_key
704 704
705 705 @classmethod
706 706 def un_prefix_key(cls, key):
707 707 if key.startswith(cls.PREFIX):
708 708 return key[len(cls.PREFIX):]
709 709 return key
710 710
711 711 @classmethod
712 712 def get_by_key_name(cls, key, repo):
713 713 row = cls.query()\
714 714 .filter(cls.repository == repo)\
715 715 .filter(cls.field_key == key).scalar()
716 716 return row
717 717
718 718
719 719 class Repository(Base, BaseModel):
720 720 __tablename__ = 'repositories'
721 721 __table_args__ = (
722 722 UniqueConstraint('repo_name'),
723 723 Index('r_repo_name_idx', 'repo_name'),
724 724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
725 725 'mysql_charset': 'utf8'},
726 726 )
727 727
728 728 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
729 729 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
730 730 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
731 731 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
732 732 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
733 733 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
734 734 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
735 735 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
736 736 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
737 737 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
738 738 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
739 739 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
740 740 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
741 741 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
742 742 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
743 743
744 744 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
745 745 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
746 746
747 747 user = relationship('User')
748 748 fork = relationship('Repository', remote_side=repo_id)
749 749 group = relationship('RepoGroup')
750 750 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
751 751 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
752 752 stats = relationship('Statistics', cascade='all', uselist=False)
753 753
754 754 followers = relationship('UserFollowing',
755 755 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
756 756 cascade='all')
757 757 extra_fields = relationship('RepositoryField',
758 758 cascade="all, delete, delete-orphan")
759 759
760 760 logs = relationship('UserLog')
761 761 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
762 762
763 763 pull_requests_org = relationship('PullRequest',
764 764 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
765 765 cascade="all, delete, delete-orphan")
766 766
767 767 pull_requests_other = relationship('PullRequest',
768 768 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
769 769 cascade="all, delete, delete-orphan")
770 770
771 771 def __unicode__(self):
772 772 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
773 773 self.repo_name)
774 774
775 775 @hybrid_property
776 776 def locked(self):
777 777 # always should return [user_id, timelocked]
778 778 if self._locked:
779 779 _lock_info = self._locked.split(':')
780 780 return int(_lock_info[0]), _lock_info[1]
781 781 return [None, None]
782 782
783 783 @locked.setter
784 784 def locked(self, val):
785 785 if val and isinstance(val, (list, tuple)):
786 786 self._locked = ':'.join(map(str, val))
787 787 else:
788 788 self._locked = None
789 789
790 790 @hybrid_property
791 791 def changeset_cache(self):
792 792 from rhodecode.lib.vcs.backends.base import EmptyChangeset
793 793 dummy = EmptyChangeset().__json__()
794 794 if not self._changeset_cache:
795 795 return dummy
796 796 try:
797 797 return json.loads(self._changeset_cache)
798 798 except TypeError:
799 799 return dummy
800 800
801 801 @changeset_cache.setter
802 802 def changeset_cache(self, val):
803 803 try:
804 804 self._changeset_cache = json.dumps(val)
805 805 except:
806 806 log.error(traceback.format_exc())
807 807
808 808 @classmethod
809 809 def url_sep(cls):
810 810 return URL_SEP
811 811
812 812 @classmethod
813 813 def normalize_repo_name(cls, repo_name):
814 814 """
815 815 Normalizes os specific repo_name to the format internally stored inside
816 816 dabatabase using URL_SEP
817 817
818 818 :param cls:
819 819 :param repo_name:
820 820 """
821 821 return cls.url_sep().join(repo_name.split(os.sep))
822 822
823 823 @classmethod
824 824 def get_by_repo_name(cls, repo_name):
825 825 q = Session().query(cls).filter(cls.repo_name == repo_name)
826 826 q = q.options(joinedload(Repository.fork))\
827 827 .options(joinedload(Repository.user))\
828 828 .options(joinedload(Repository.group))
829 829 return q.scalar()
830 830
831 831 @classmethod
832 832 def get_by_full_path(cls, repo_full_path):
833 833 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
834 834 repo_name = cls.normalize_repo_name(repo_name)
835 835 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
836 836
837 837 @classmethod
838 838 def get_repo_forks(cls, repo_id):
839 839 return cls.query().filter(Repository.fork_id == repo_id)
840 840
841 841 @classmethod
842 842 def base_path(cls):
843 843 """
844 844 Returns base path when all repos are stored
845 845
846 846 :param cls:
847 847 """
848 848 q = Session().query(RhodeCodeUi)\
849 849 .filter(RhodeCodeUi.ui_key == cls.url_sep())
850 850 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
851 851 return q.one().ui_value
852 852
853 853 @property
854 854 def forks(self):
855 855 """
856 856 Return forks of this repo
857 857 """
858 858 return Repository.get_repo_forks(self.repo_id)
859 859
860 860 @property
861 861 def parent(self):
862 862 """
863 863 Returns fork parent
864 864 """
865 865 return self.fork
866 866
867 867 @property
868 868 def just_name(self):
869 869 return self.repo_name.split(Repository.url_sep())[-1]
870 870
871 871 @property
872 872 def groups_with_parents(self):
873 873 groups = []
874 874 if self.group is None:
875 875 return groups
876 876
877 877 cur_gr = self.group
878 878 groups.insert(0, cur_gr)
879 879 while 1:
880 880 gr = getattr(cur_gr, 'parent_group', None)
881 881 cur_gr = cur_gr.parent_group
882 882 if gr is None:
883 883 break
884 884 groups.insert(0, gr)
885 885
886 886 return groups
887 887
888 888 @property
889 889 def groups_and_repo(self):
890 890 return self.groups_with_parents, self.just_name
891 891
892 892 @LazyProperty
893 893 def repo_path(self):
894 894 """
895 895 Returns base full path for that repository means where it actually
896 896 exists on a filesystem
897 897 """
898 898 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
899 899 Repository.url_sep())
900 900 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
901 901 return q.one().ui_value
902 902
903 903 @property
904 904 def repo_full_path(self):
905 905 p = [self.repo_path]
906 906 # we need to split the name by / since this is how we store the
907 907 # names in the database, but that eventually needs to be converted
908 908 # into a valid system path
909 909 p += self.repo_name.split(Repository.url_sep())
910 910 return os.path.join(*p)
911 911
912 912 @property
913 913 def cache_keys(self):
914 914 """
915 915 Returns associated cache keys for that repo
916 916 """
917 917 return CacheInvalidation.query()\
918 918 .filter(CacheInvalidation.cache_args == self.repo_name)\
919 919 .order_by(CacheInvalidation.cache_key)\
920 920 .all()
921 921
922 922 def get_new_name(self, repo_name):
923 923 """
924 924 returns new full repository name based on assigned group and new new
925 925
926 926 :param group_name:
927 927 """
928 928 path_prefix = self.group.full_path_splitted if self.group else []
929 929 return Repository.url_sep().join(path_prefix + [repo_name])
930 930
931 931 @property
932 932 def _ui(self):
933 933 """
934 934 Creates an db based ui object for this repository
935 935 """
936 936 from rhodecode.lib.utils import make_ui
937 937 return make_ui('db', clear_session=False)
938 938
939 939 @classmethod
940 940 def inject_ui(cls, repo, extras={}):
941 941 repo.inject_ui(extras)
942 942
943 943 @classmethod
944 944 def is_valid(cls, repo_name):
945 945 """
946 946 returns True if given repo name is a valid filesystem repository
947 947
948 948 :param cls:
949 949 :param repo_name:
950 950 """
951 951 from rhodecode.lib.utils import is_valid_repo
952 952
953 953 return is_valid_repo(repo_name, cls.base_path())
954 954
955 955 def get_api_data(self):
956 956 """
957 957 Common function for generating repo api data
958 958
959 959 """
960 960 repo = self
961 961 data = dict(
962 962 repo_id=repo.repo_id,
963 963 repo_name=repo.repo_name,
964 964 repo_type=repo.repo_type,
965 965 clone_uri=repo.clone_uri,
966 966 private=repo.private,
967 967 created_on=repo.created_on,
968 968 description=repo.description,
969 969 landing_rev=repo.landing_rev,
970 970 owner=repo.user.username,
971 971 fork_of=repo.fork.repo_name if repo.fork else None,
972 972 enable_statistics=repo.enable_statistics,
973 973 enable_locking=repo.enable_locking,
974 974 enable_downloads=repo.enable_downloads,
975 975 last_changeset=repo.changeset_cache
976 976 )
977 977 rc_config = RhodeCodeSetting.get_app_settings()
978 978 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
979 979 if repository_fields:
980 980 for f in self.extra_fields:
981 981 data[f.field_key_prefixed] = f.field_value
982 982
983 983 return data
984 984
985 985 @classmethod
986 986 def lock(cls, repo, user_id):
987 987 repo.locked = [user_id, time.time()]
988 988 Session().add(repo)
989 989 Session().commit()
990 990
991 991 @classmethod
992 992 def unlock(cls, repo):
993 993 repo.locked = None
994 994 Session().add(repo)
995 995 Session().commit()
996 996
997 997 @classmethod
998 998 def getlock(cls, repo):
999 999 return repo.locked
1000 1000
1001 1001 @property
1002 1002 def last_db_change(self):
1003 1003 return self.updated_on
1004 1004
1005 1005 def clone_url(self, **override):
1006 1006 from pylons import url
1007 1007 from urlparse import urlparse
1008 1008 import urllib
1009 1009 parsed_url = urlparse(url('home', qualified=True))
1010 1010 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1011 1011 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1012 1012 args = {
1013 1013 'user': '',
1014 1014 'pass': '',
1015 1015 'scheme': parsed_url.scheme,
1016 1016 'netloc': parsed_url.netloc,
1017 1017 'prefix': decoded_path,
1018 1018 'path': self.repo_name
1019 1019 }
1020 1020
1021 1021 args.update(override)
1022 1022 return default_clone_uri % args
1023 1023
1024 1024 #==========================================================================
1025 1025 # SCM PROPERTIES
1026 1026 #==========================================================================
1027 1027
1028 1028 def get_changeset(self, rev=None):
1029 1029 return get_changeset_safe(self.scm_instance, rev)
1030 1030
1031 1031 def get_landing_changeset(self):
1032 1032 """
1033 1033 Returns landing changeset, or if that doesn't exist returns the tip
1034 1034 """
1035 1035 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1036 1036 return cs
1037 1037
1038 1038 def update_changeset_cache(self, cs_cache=None):
1039 1039 """
1040 1040 Update cache of last changeset for repository, keys should be::
1041 1041
1042 1042 short_id
1043 1043 raw_id
1044 1044 revision
1045 1045 message
1046 1046 date
1047 1047 author
1048 1048
1049 1049 :param cs_cache:
1050 1050 """
1051 1051 from rhodecode.lib.vcs.backends.base import BaseChangeset
1052 1052 if cs_cache is None:
1053 1053 cs_cache = EmptyChangeset()
1054 1054 # use no-cache version here
1055 1055 scm_repo = self.scm_instance_no_cache
1056 1056 if scm_repo:
1057 1057 cs_cache = scm_repo.get_changeset()
1058 1058
1059 1059 if isinstance(cs_cache, BaseChangeset):
1060 1060 cs_cache = cs_cache.__json__()
1061 1061
1062 1062 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1063 1063 _default = datetime.datetime.fromtimestamp(0)
1064 1064 last_change = cs_cache.get('date') or _default
1065 1065 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
1066 1066 self.updated_on = last_change
1067 1067 self.changeset_cache = cs_cache
1068 1068 Session().add(self)
1069 1069 Session().commit()
1070 1070 else:
1071 1071 log.debug('Skipping repo:%s already with latest changes' % self)
1072 1072
1073 1073 @property
1074 1074 def tip(self):
1075 1075 return self.get_changeset('tip')
1076 1076
1077 1077 @property
1078 1078 def author(self):
1079 1079 return self.tip.author
1080 1080
1081 1081 @property
1082 1082 def last_change(self):
1083 1083 return self.scm_instance.last_change
1084 1084
1085 1085 def get_comments(self, revisions=None):
1086 1086 """
1087 1087 Returns comments for this repository grouped by revisions
1088 1088
1089 1089 :param revisions: filter query by revisions only
1090 1090 """
1091 1091 cmts = ChangesetComment.query()\
1092 1092 .filter(ChangesetComment.repo == self)
1093 1093 if revisions:
1094 1094 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1095 1095 grouped = defaultdict(list)
1096 1096 for cmt in cmts.all():
1097 1097 grouped[cmt.revision].append(cmt)
1098 1098 return grouped
1099 1099
1100 1100 def statuses(self, revisions=None):
1101 1101 """
1102 1102 Returns statuses for this repository
1103 1103
1104 1104 :param revisions: list of revisions to get statuses for
1105 1105 :type revisions: list
1106 1106 """
1107 1107
1108 1108 statuses = ChangesetStatus.query()\
1109 1109 .filter(ChangesetStatus.repo == self)\
1110 1110 .filter(ChangesetStatus.version == 0)
1111 1111 if revisions:
1112 1112 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1113 1113 grouped = {}
1114 1114
1115 1115 #maybe we have open new pullrequest without a status ?
1116 1116 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1117 1117 status_lbl = ChangesetStatus.get_status_lbl(stat)
1118 1118 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1119 1119 for rev in pr.revisions:
1120 1120 pr_id = pr.pull_request_id
1121 1121 pr_repo = pr.other_repo.repo_name
1122 1122 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1123 1123
1124 1124 for stat in statuses.all():
1125 1125 pr_id = pr_repo = None
1126 1126 if stat.pull_request:
1127 1127 pr_id = stat.pull_request.pull_request_id
1128 1128 pr_repo = stat.pull_request.other_repo.repo_name
1129 1129 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1130 1130 pr_id, pr_repo]
1131 1131 return grouped
1132 1132
1133 1133 def _repo_size(self):
1134 1134 from rhodecode.lib import helpers as h
1135 1135 log.debug('calculating repository size...')
1136 1136 return h.format_byte_size(self.scm_instance.size)
1137 1137
1138 1138 #==========================================================================
1139 1139 # SCM CACHE INSTANCE
1140 1140 #==========================================================================
1141 1141
1142 1142 @property
1143 1143 def invalidate(self):
1144 1144 return CacheInvalidation.invalidate(self.repo_name)
1145 1145
1146 1146 def set_invalidate(self):
1147 1147 """
1148 1148 set a cache for invalidation for this instance
1149 1149 """
1150 1150 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1151 1151
1152 1152 @LazyProperty
1153 1153 def scm_instance_no_cache(self):
1154 1154 return self.__get_instance()
1155 1155
1156 1156 @LazyProperty
1157 1157 def scm_instance(self):
1158 1158 import rhodecode
1159 1159 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1160 1160 if full_cache:
1161 1161 return self.scm_instance_cached()
1162 1162 return self.__get_instance()
1163 1163
1164 1164 def scm_instance_cached(self, cache_map=None):
1165 1165 @cache_region('long_term')
1166 1166 def _c(repo_name):
1167 1167 return self.__get_instance()
1168 1168 rn = self.repo_name
1169 1169 log.debug('Getting cached instance of repo')
1170 1170
1171 1171 if cache_map:
1172 1172 # get using prefilled cache_map
1173 1173 invalidate_repo = cache_map[self.repo_name]
1174 1174 if invalidate_repo:
1175 1175 invalidate_repo = (None if invalidate_repo.cache_active
1176 1176 else invalidate_repo)
1177 1177 else:
1178 1178 # get from invalidate
1179 1179 invalidate_repo = self.invalidate
1180 1180
1181 1181 if invalidate_repo is not None:
1182 1182 region_invalidate(_c, None, rn)
1183 1183 # update our cache
1184 1184 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1185 1185 return _c(rn)
1186 1186
1187 1187 def __get_instance(self):
1188 1188 repo_full_path = self.repo_full_path
1189 1189 try:
1190 1190 alias = get_scm(repo_full_path)[0]
1191 1191 log.debug('Creating instance of %s repository from %s'
1192 1192 % (alias, repo_full_path))
1193 1193 backend = get_backend(alias)
1194 1194 except VCSError:
1195 1195 log.error(traceback.format_exc())
1196 1196 log.error('Perhaps this repository is in db and not in '
1197 1197 'filesystem run rescan repositories with '
1198 1198 '"destroy old data " option from admin panel')
1199 1199 return
1200 1200
1201 1201 if alias == 'hg':
1202 1202
1203 1203 repo = backend(safe_str(repo_full_path), create=False,
1204 1204 baseui=self._ui)
1205 1205 # skip hidden web repository
1206 1206 if repo._get_hidden():
1207 1207 return
1208 1208 else:
1209 1209 repo = backend(repo_full_path, create=False)
1210 1210
1211 1211 return repo
1212 1212
1213 1213
1214 1214 class RepoGroup(Base, BaseModel):
1215 1215 __tablename__ = 'groups'
1216 1216 __table_args__ = (
1217 1217 UniqueConstraint('group_name', 'group_parent_id'),
1218 1218 CheckConstraint('group_id != group_parent_id'),
1219 1219 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1220 1220 'mysql_charset': 'utf8'},
1221 1221 )
1222 1222 __mapper_args__ = {'order_by': 'group_name'}
1223 1223
1224 1224 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1225 1225 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1226 1226 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1227 1227 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1228 1228 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1229 1229
1230 1230 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1231 1231 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1232 1232
1233 1233 parent_group = relationship('RepoGroup', remote_side=group_id)
1234 1234
1235 1235 def __init__(self, group_name='', parent_group=None):
1236 1236 self.group_name = group_name
1237 1237 self.parent_group = parent_group
1238 1238
1239 1239 def __unicode__(self):
1240 1240 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1241 1241 self.group_name)
1242 1242
1243 1243 @classmethod
1244 1244 def groups_choices(cls, groups=None, show_empty_group=True):
1245 1245 from webhelpers.html import literal as _literal
1246 1246 if not groups:
1247 1247 groups = cls.query().all()
1248 1248
1249 1249 repo_groups = []
1250 1250 if show_empty_group:
1251 1251 repo_groups = [('-1', '-- no parent --')]
1252 1252 sep = ' &raquo; '
1253 1253 _name = lambda k: _literal(sep.join(k))
1254 1254
1255 1255 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1256 1256 for x in groups])
1257 1257
1258 1258 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1259 1259 return repo_groups
1260 1260
1261 1261 @classmethod
1262 1262 def url_sep(cls):
1263 1263 return URL_SEP
1264 1264
1265 1265 @classmethod
1266 1266 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1267 1267 if case_insensitive:
1268 1268 gr = cls.query()\
1269 1269 .filter(cls.group_name.ilike(group_name))
1270 1270 else:
1271 1271 gr = cls.query()\
1272 1272 .filter(cls.group_name == group_name)
1273 1273 if cache:
1274 1274 gr = gr.options(FromCache(
1275 1275 "sql_cache_short",
1276 1276 "get_group_%s" % _hash_key(group_name)
1277 1277 )
1278 1278 )
1279 1279 return gr.scalar()
1280 1280
1281 1281 @property
1282 1282 def parents(self):
1283 1283 parents_recursion_limit = 5
1284 1284 groups = []
1285 1285 if self.parent_group is None:
1286 1286 return groups
1287 1287 cur_gr = self.parent_group
1288 1288 groups.insert(0, cur_gr)
1289 1289 cnt = 0
1290 1290 while 1:
1291 1291 cnt += 1
1292 1292 gr = getattr(cur_gr, 'parent_group', None)
1293 1293 cur_gr = cur_gr.parent_group
1294 1294 if gr is None:
1295 1295 break
1296 1296 if cnt == parents_recursion_limit:
1297 1297 # this will prevent accidental infinit loops
1298 1298 log.error('group nested more than %s' %
1299 1299 parents_recursion_limit)
1300 1300 break
1301 1301
1302 1302 groups.insert(0, gr)
1303 1303 return groups
1304 1304
1305 1305 @property
1306 1306 def children(self):
1307 1307 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1308 1308
1309 1309 @property
1310 1310 def name(self):
1311 1311 return self.group_name.split(RepoGroup.url_sep())[-1]
1312 1312
1313 1313 @property
1314 1314 def full_path(self):
1315 1315 return self.group_name
1316 1316
1317 1317 @property
1318 1318 def full_path_splitted(self):
1319 1319 return self.group_name.split(RepoGroup.url_sep())
1320 1320
1321 1321 @property
1322 1322 def repositories(self):
1323 1323 return Repository.query()\
1324 1324 .filter(Repository.group == self)\
1325 1325 .order_by(Repository.repo_name)
1326 1326
1327 1327 @property
1328 1328 def repositories_recursive_count(self):
1329 1329 cnt = self.repositories.count()
1330 1330
1331 1331 def children_count(group):
1332 1332 cnt = 0
1333 1333 for child in group.children:
1334 1334 cnt += child.repositories.count()
1335 1335 cnt += children_count(child)
1336 1336 return cnt
1337 1337
1338 1338 return cnt + children_count(self)
1339 1339
1340 1340 def _recursive_objects(self, include_repos=True):
1341 1341 all_ = []
1342 1342
1343 1343 def _get_members(root_gr):
1344 1344 if include_repos:
1345 1345 for r in root_gr.repositories:
1346 1346 all_.append(r)
1347 1347 childs = root_gr.children.all()
1348 1348 if childs:
1349 1349 for gr in childs:
1350 1350 all_.append(gr)
1351 1351 _get_members(gr)
1352 1352
1353 1353 _get_members(self)
1354 1354 return [self] + all_
1355 1355
1356 1356 def recursive_groups_and_repos(self):
1357 1357 """
1358 1358 Recursive return all groups, with repositories in those groups
1359 1359 """
1360 1360 return self._recursive_objects()
1361 1361
1362 1362 def recursive_groups(self):
1363 1363 """
1364 1364 Returns all children groups for this group including children of children
1365 1365 """
1366 1366 return self._recursive_objects(include_repos=False)
1367 1367
1368 1368 def get_new_name(self, group_name):
1369 1369 """
1370 1370 returns new full group name based on parent and new name
1371 1371
1372 1372 :param group_name:
1373 1373 """
1374 1374 path_prefix = (self.parent_group.full_path_splitted if
1375 1375 self.parent_group else [])
1376 1376 return RepoGroup.url_sep().join(path_prefix + [group_name])
1377 1377
1378 1378
1379 1379 class Permission(Base, BaseModel):
1380 1380 __tablename__ = 'permissions'
1381 1381 __table_args__ = (
1382 1382 Index('p_perm_name_idx', 'permission_name'),
1383 1383 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1384 1384 'mysql_charset': 'utf8'},
1385 1385 )
1386 1386 PERMS = [
1387 1387 ('repository.none', _('Repository no access')),
1388 1388 ('repository.read', _('Repository read access')),
1389 1389 ('repository.write', _('Repository write access')),
1390 1390 ('repository.admin', _('Repository admin access')),
1391 1391
1392 1392 ('group.none', _('Repository group no access')),
1393 1393 ('group.read', _('Repository group read access')),
1394 1394 ('group.write', _('Repository group write access')),
1395 1395 ('group.admin', _('Repository group admin access')),
1396 1396
1397 1397 ('hg.admin', _('RhodeCode Administrator')),
1398 1398 ('hg.create.none', _('Repository creation disabled')),
1399 1399 ('hg.create.repository', _('Repository creation enabled')),
1400 1400 ('hg.fork.none', _('Repository forking disabled')),
1401 1401 ('hg.fork.repository', _('Repository forking enabled')),
1402 1402 ('hg.register.none', _('Register disabled')),
1403 1403 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1404 1404 'with manual activation')),
1405 1405
1406 1406 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1407 1407 'with auto activation')),
1408 1408 ]
1409 1409
1410 1410 # defines which permissions are more important higher the more important
1411 1411 PERM_WEIGHTS = {
1412 1412 'repository.none': 0,
1413 1413 'repository.read': 1,
1414 1414 'repository.write': 3,
1415 1415 'repository.admin': 4,
1416 1416
1417 1417 'group.none': 0,
1418 1418 'group.read': 1,
1419 1419 'group.write': 3,
1420 1420 'group.admin': 4,
1421 1421
1422 1422 'hg.fork.none': 0,
1423 1423 'hg.fork.repository': 1,
1424 1424 'hg.create.none': 0,
1425 1425 'hg.create.repository':1
1426 1426 }
1427 1427
1428 1428 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1429 1429 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1430 1430 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1431 1431
1432 1432 def __unicode__(self):
1433 1433 return u"<%s('%s:%s')>" % (
1434 1434 self.__class__.__name__, self.permission_id, self.permission_name
1435 1435 )
1436 1436
1437 1437 @classmethod
1438 1438 def get_by_key(cls, key):
1439 1439 return cls.query().filter(cls.permission_name == key).scalar()
1440 1440
1441 1441 @classmethod
1442 1442 def get_default_perms(cls, default_user_id):
1443 1443 q = Session().query(UserRepoToPerm, Repository, cls)\
1444 1444 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1445 1445 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1446 1446 .filter(UserRepoToPerm.user_id == default_user_id)
1447 1447
1448 1448 return q.all()
1449 1449
1450 1450 @classmethod
1451 1451 def get_default_group_perms(cls, default_user_id):
1452 1452 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1453 1453 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1454 1454 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1455 1455 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1456 1456
1457 1457 return q.all()
1458 1458
1459 1459
1460 1460 class UserRepoToPerm(Base, BaseModel):
1461 1461 __tablename__ = 'repo_to_perm'
1462 1462 __table_args__ = (
1463 1463 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1464 1464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1465 1465 'mysql_charset': 'utf8'}
1466 1466 )
1467 1467 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1468 1468 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1469 1469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1470 1470 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1471 1471
1472 1472 user = relationship('User')
1473 1473 repository = relationship('Repository')
1474 1474 permission = relationship('Permission')
1475 1475
1476 1476 @classmethod
1477 1477 def create(cls, user, repository, permission):
1478 1478 n = cls()
1479 1479 n.user = user
1480 1480 n.repository = repository
1481 1481 n.permission = permission
1482 1482 Session().add(n)
1483 1483 return n
1484 1484
1485 1485 def __unicode__(self):
1486 1486 return u'<user:%s => %s >' % (self.user, self.repository)
1487 1487
1488 1488
1489 1489 class UserToPerm(Base, BaseModel):
1490 1490 __tablename__ = 'user_to_perm'
1491 1491 __table_args__ = (
1492 1492 UniqueConstraint('user_id', 'permission_id'),
1493 1493 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1494 1494 'mysql_charset': 'utf8'}
1495 1495 )
1496 1496 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1497 1497 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1498 1498 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1499 1499
1500 1500 user = relationship('User')
1501 1501 permission = relationship('Permission', lazy='joined')
1502 1502
1503 1503
1504 1504 class UserGroupRepoToPerm(Base, BaseModel):
1505 1505 __tablename__ = 'users_group_repo_to_perm'
1506 1506 __table_args__ = (
1507 1507 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1508 1508 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1509 1509 'mysql_charset': 'utf8'}
1510 1510 )
1511 1511 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1512 1512 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1513 1513 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1514 1514 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1515 1515
1516 1516 users_group = relationship('UserGroup')
1517 1517 permission = relationship('Permission')
1518 1518 repository = relationship('Repository')
1519 1519
1520 1520 @classmethod
1521 1521 def create(cls, users_group, repository, permission):
1522 1522 n = cls()
1523 1523 n.users_group = users_group
1524 1524 n.repository = repository
1525 1525 n.permission = permission
1526 1526 Session().add(n)
1527 1527 return n
1528 1528
1529 1529 def __unicode__(self):
1530 1530 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1531 1531
1532 1532
1533 1533 class UserGroupToPerm(Base, BaseModel):
1534 1534 __tablename__ = 'users_group_to_perm'
1535 1535 __table_args__ = (
1536 1536 UniqueConstraint('users_group_id', 'permission_id',),
1537 1537 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1538 1538 'mysql_charset': 'utf8'}
1539 1539 )
1540 1540 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1541 1541 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1542 1542 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1543 1543
1544 1544 users_group = relationship('UserGroup')
1545 1545 permission = relationship('Permission')
1546 1546
1547 1547
1548 1548 class UserRepoGroupToPerm(Base, BaseModel):
1549 1549 __tablename__ = 'user_repo_group_to_perm'
1550 1550 __table_args__ = (
1551 1551 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1552 1552 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1553 1553 'mysql_charset': 'utf8'}
1554 1554 )
1555 1555
1556 1556 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1557 1557 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1558 1558 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1559 1559 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1560 1560
1561 1561 user = relationship('User')
1562 1562 group = relationship('RepoGroup')
1563 1563 permission = relationship('Permission')
1564 1564
1565 1565
1566 1566 class UserGroupRepoGroupToPerm(Base, BaseModel):
1567 1567 __tablename__ = 'users_group_repo_group_to_perm'
1568 1568 __table_args__ = (
1569 1569 UniqueConstraint('users_group_id', 'group_id'),
1570 1570 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1571 1571 'mysql_charset': 'utf8'}
1572 1572 )
1573 1573
1574 1574 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1575 1575 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1576 1576 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1577 1577 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1578 1578
1579 1579 users_group = relationship('UserGroup')
1580 1580 permission = relationship('Permission')
1581 1581 group = relationship('RepoGroup')
1582 1582
1583 1583
1584 1584 class Statistics(Base, BaseModel):
1585 1585 __tablename__ = 'statistics'
1586 1586 __table_args__ = (
1587 1587 UniqueConstraint('repository_id'),
1588 1588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1589 1589 'mysql_charset': 'utf8'}
1590 1590 )
1591 1591 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1592 1592 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1593 1593 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1594 1594 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1595 1595 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1596 1596 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1597 1597
1598 1598 repository = relationship('Repository', single_parent=True)
1599 1599
1600 1600
1601 1601 class UserFollowing(Base, BaseModel):
1602 1602 __tablename__ = 'user_followings'
1603 1603 __table_args__ = (
1604 1604 UniqueConstraint('user_id', 'follows_repository_id'),
1605 1605 UniqueConstraint('user_id', 'follows_user_id'),
1606 1606 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1607 1607 'mysql_charset': 'utf8'}
1608 1608 )
1609 1609
1610 1610 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1611 1611 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1612 1612 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1613 1613 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1614 1614 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1615 1615
1616 1616 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1617 1617
1618 1618 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1619 1619 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1620 1620
1621 1621 @classmethod
1622 1622 def get_repo_followers(cls, repo_id):
1623 1623 return cls.query().filter(cls.follows_repo_id == repo_id)
1624 1624
1625 1625
1626 1626 class CacheInvalidation(Base, BaseModel):
1627 1627 __tablename__ = 'cache_invalidation'
1628 1628 __table_args__ = (
1629 1629 UniqueConstraint('cache_key'),
1630 1630 Index('key_idx', 'cache_key'),
1631 1631 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1632 1632 'mysql_charset': 'utf8'},
1633 1633 )
1634 1634 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 1635 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1636 1636 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1637 1637 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1638 1638
1639 1639 def __init__(self, cache_key, cache_args=''):
1640 1640 self.cache_key = cache_key
1641 1641 self.cache_args = cache_args
1642 1642 self.cache_active = False
1643 1643
1644 1644 def __unicode__(self):
1645 1645 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1646 1646 self.cache_id, self.cache_key)
1647 1647
1648 1648 @property
1649 1649 def prefix(self):
1650 1650 _split = self.cache_key.split(self.cache_args, 1)
1651 1651 if _split and len(_split) == 2:
1652 1652 return _split[0]
1653 1653 return ''
1654 1654
1655 1655 @classmethod
1656 1656 def clear_cache(cls):
1657 1657 cls.query().delete()
1658 1658
1659 1659 @classmethod
1660 1660 def _get_key(cls, key):
1661 1661 """
1662 1662 Wrapper for generating a key, together with a prefix
1663 1663
1664 1664 :param key:
1665 1665 """
1666 1666 import rhodecode
1667 1667 prefix = ''
1668 1668 org_key = key
1669 1669 iid = rhodecode.CONFIG.get('instance_id')
1670 1670 if iid:
1671 1671 prefix = iid
1672 1672
1673 1673 return "%s%s" % (prefix, key), prefix, org_key
1674 1674
1675 1675 @classmethod
1676 1676 def get_by_key(cls, key):
1677 1677 return cls.query().filter(cls.cache_key == key).scalar()
1678 1678
1679 1679 @classmethod
1680 1680 def get_by_repo_name(cls, repo_name):
1681 1681 return cls.query().filter(cls.cache_args == repo_name).all()
1682 1682
1683 1683 @classmethod
1684 1684 def _get_or_create_key(cls, key, repo_name, commit=True):
1685 1685 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1686 1686 if not inv_obj:
1687 1687 try:
1688 1688 inv_obj = CacheInvalidation(key, repo_name)
1689 1689 Session().add(inv_obj)
1690 1690 if commit:
1691 1691 Session().commit()
1692 1692 except Exception:
1693 1693 log.error(traceback.format_exc())
1694 1694 Session().rollback()
1695 1695 return inv_obj
1696 1696
1697 1697 @classmethod
1698 1698 def invalidate(cls, key):
1699 1699 """
1700 1700 Returns Invalidation object if this given key should be invalidated
1701 1701 None otherwise. `cache_active = False` means that this cache
1702 1702 state is not valid and needs to be invalidated
1703 1703
1704 1704 :param key:
1705 1705 """
1706 1706 repo_name = key
1707 1707 repo_name = remove_suffix(repo_name, '_README')
1708 1708 repo_name = remove_suffix(repo_name, '_RSS')
1709 1709 repo_name = remove_suffix(repo_name, '_ATOM')
1710 1710
1711 1711 # adds instance prefix
1712 1712 key, _prefix, _org_key = cls._get_key(key)
1713 1713 inv = cls._get_or_create_key(key, repo_name)
1714 1714
1715 1715 if inv and inv.cache_active is False:
1716 1716 return inv
1717 1717
1718 1718 @classmethod
1719 1719 def set_invalidate(cls, key=None, repo_name=None):
1720 1720 """
1721 1721 Mark this Cache key for invalidation, either by key or whole
1722 1722 cache sets based on repo_name
1723 1723
1724 1724 :param key:
1725 1725 """
1726 1726 invalidated_keys = []
1727 1727 if key:
1728 1728 key, _prefix, _org_key = cls._get_key(key)
1729 1729 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1730 1730 elif repo_name:
1731 1731 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1732 1732
1733 1733 try:
1734 1734 for inv_obj in inv_objs:
1735 1735 inv_obj.cache_active = False
1736 1736 log.debug('marking %s key for invalidation based on key=%s,repo_name=%s'
1737 % (inv_obj, key, repo_name))
1737 % (inv_obj, key, safe_str(repo_name)))
1738 1738 invalidated_keys.append(inv_obj.cache_key)
1739 1739 Session().add(inv_obj)
1740 1740 Session().commit()
1741 1741 except Exception:
1742 1742 log.error(traceback.format_exc())
1743 1743 Session().rollback()
1744 1744 return invalidated_keys
1745 1745
1746 1746 @classmethod
1747 1747 def set_valid(cls, key):
1748 1748 """
1749 1749 Mark this cache key as active and currently cached
1750 1750
1751 1751 :param key:
1752 1752 """
1753 1753 inv_obj = cls.get_by_key(key)
1754 1754 inv_obj.cache_active = True
1755 1755 Session().add(inv_obj)
1756 1756 Session().commit()
1757 1757
1758 1758 @classmethod
1759 1759 def get_cache_map(cls):
1760 1760
1761 1761 class cachemapdict(dict):
1762 1762
1763 1763 def __init__(self, *args, **kwargs):
1764 1764 fixkey = kwargs.get('fixkey')
1765 1765 if fixkey:
1766 1766 del kwargs['fixkey']
1767 1767 self.fixkey = fixkey
1768 1768 super(cachemapdict, self).__init__(*args, **kwargs)
1769 1769
1770 1770 def __getattr__(self, name):
1771 1771 key = name
1772 1772 if self.fixkey:
1773 1773 key, _prefix, _org_key = cls._get_key(key)
1774 1774 if key in self.__dict__:
1775 1775 return self.__dict__[key]
1776 1776 else:
1777 1777 return self[key]
1778 1778
1779 1779 def __getitem__(self, key):
1780 1780 if self.fixkey:
1781 1781 key, _prefix, _org_key = cls._get_key(key)
1782 1782 try:
1783 1783 return super(cachemapdict, self).__getitem__(key)
1784 1784 except KeyError:
1785 1785 return
1786 1786
1787 1787 cache_map = cachemapdict(fixkey=True)
1788 1788 for obj in cls.query().all():
1789 1789 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1790 1790 return cache_map
1791 1791
1792 1792
1793 1793 class ChangesetComment(Base, BaseModel):
1794 1794 __tablename__ = 'changeset_comments'
1795 1795 __table_args__ = (
1796 1796 Index('cc_revision_idx', 'revision'),
1797 1797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1798 1798 'mysql_charset': 'utf8'},
1799 1799 )
1800 1800 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1801 1801 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1802 1802 revision = Column('revision', String(40), nullable=True)
1803 1803 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1804 1804 line_no = Column('line_no', Unicode(10), nullable=True)
1805 1805 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1806 1806 f_path = Column('f_path', Unicode(1000), nullable=True)
1807 1807 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1808 1808 text = Column('text', UnicodeText(25000), nullable=False)
1809 1809 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1810 1810 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1811 1811
1812 1812 author = relationship('User', lazy='joined')
1813 1813 repo = relationship('Repository')
1814 1814 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1815 1815 pull_request = relationship('PullRequest', lazy='joined')
1816 1816
1817 1817 @classmethod
1818 1818 def get_users(cls, revision=None, pull_request_id=None):
1819 1819 """
1820 1820 Returns user associated with this ChangesetComment. ie those
1821 1821 who actually commented
1822 1822
1823 1823 :param cls:
1824 1824 :param revision:
1825 1825 """
1826 1826 q = Session().query(User)\
1827 1827 .join(ChangesetComment.author)
1828 1828 if revision:
1829 1829 q = q.filter(cls.revision == revision)
1830 1830 elif pull_request_id:
1831 1831 q = q.filter(cls.pull_request_id == pull_request_id)
1832 1832 return q.all()
1833 1833
1834 1834
1835 1835 class ChangesetStatus(Base, BaseModel):
1836 1836 __tablename__ = 'changeset_statuses'
1837 1837 __table_args__ = (
1838 1838 Index('cs_revision_idx', 'revision'),
1839 1839 Index('cs_version_idx', 'version'),
1840 1840 UniqueConstraint('repo_id', 'revision', 'version'),
1841 1841 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1842 1842 'mysql_charset': 'utf8'}
1843 1843 )
1844 1844 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1845 1845 STATUS_APPROVED = 'approved'
1846 1846 STATUS_REJECTED = 'rejected'
1847 1847 STATUS_UNDER_REVIEW = 'under_review'
1848 1848
1849 1849 STATUSES = [
1850 1850 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1851 1851 (STATUS_APPROVED, _("Approved")),
1852 1852 (STATUS_REJECTED, _("Rejected")),
1853 1853 (STATUS_UNDER_REVIEW, _("Under Review")),
1854 1854 ]
1855 1855
1856 1856 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1857 1857 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1858 1858 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1859 1859 revision = Column('revision', String(40), nullable=False)
1860 1860 status = Column('status', String(128), nullable=False, default=DEFAULT)
1861 1861 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1862 1862 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1863 1863 version = Column('version', Integer(), nullable=False, default=0)
1864 1864 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1865 1865
1866 1866 author = relationship('User', lazy='joined')
1867 1867 repo = relationship('Repository')
1868 1868 comment = relationship('ChangesetComment', lazy='joined')
1869 1869 pull_request = relationship('PullRequest', lazy='joined')
1870 1870
1871 1871 def __unicode__(self):
1872 1872 return u"<%s('%s:%s')>" % (
1873 1873 self.__class__.__name__,
1874 1874 self.status, self.author
1875 1875 )
1876 1876
1877 1877 @classmethod
1878 1878 def get_status_lbl(cls, value):
1879 1879 return dict(cls.STATUSES).get(value)
1880 1880
1881 1881 @property
1882 1882 def status_lbl(self):
1883 1883 return ChangesetStatus.get_status_lbl(self.status)
1884 1884
1885 1885
1886 1886 class PullRequest(Base, BaseModel):
1887 1887 __tablename__ = 'pull_requests'
1888 1888 __table_args__ = (
1889 1889 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1890 1890 'mysql_charset': 'utf8'},
1891 1891 )
1892 1892
1893 1893 STATUS_NEW = u'new'
1894 1894 STATUS_OPEN = u'open'
1895 1895 STATUS_CLOSED = u'closed'
1896 1896
1897 1897 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1898 1898 title = Column('title', Unicode(256), nullable=True)
1899 1899 description = Column('description', UnicodeText(10240), nullable=True)
1900 1900 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1901 1901 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1902 1902 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1903 1903 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1904 1904 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1905 1905 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1906 1906 org_ref = Column('org_ref', Unicode(256), nullable=False)
1907 1907 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1908 1908 other_ref = Column('other_ref', Unicode(256), nullable=False)
1909 1909
1910 1910 @hybrid_property
1911 1911 def revisions(self):
1912 1912 return self._revisions.split(':')
1913 1913
1914 1914 @revisions.setter
1915 1915 def revisions(self, val):
1916 1916 self._revisions = ':'.join(val)
1917 1917
1918 1918 @property
1919 1919 def org_ref_parts(self):
1920 1920 return self.org_ref.split(':')
1921 1921
1922 1922 @property
1923 1923 def other_ref_parts(self):
1924 1924 return self.other_ref.split(':')
1925 1925
1926 1926 author = relationship('User', lazy='joined')
1927 1927 reviewers = relationship('PullRequestReviewers',
1928 1928 cascade="all, delete, delete-orphan")
1929 1929 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1930 1930 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1931 1931 statuses = relationship('ChangesetStatus')
1932 1932 comments = relationship('ChangesetComment',
1933 1933 cascade="all, delete, delete-orphan")
1934 1934
1935 1935 def is_closed(self):
1936 1936 return self.status == self.STATUS_CLOSED
1937 1937
1938 1938 @property
1939 1939 def last_review_status(self):
1940 1940 return self.statuses[-1].status if self.statuses else ''
1941 1941
1942 1942 def __json__(self):
1943 1943 return dict(
1944 1944 revisions=self.revisions
1945 1945 )
1946 1946
1947 1947
1948 1948 class PullRequestReviewers(Base, BaseModel):
1949 1949 __tablename__ = 'pull_request_reviewers'
1950 1950 __table_args__ = (
1951 1951 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1952 1952 'mysql_charset': 'utf8'},
1953 1953 )
1954 1954
1955 1955 def __init__(self, user=None, pull_request=None):
1956 1956 self.user = user
1957 1957 self.pull_request = pull_request
1958 1958
1959 1959 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1960 1960 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1961 1961 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1962 1962
1963 1963 user = relationship('User')
1964 1964 pull_request = relationship('PullRequest')
1965 1965
1966 1966
1967 1967 class Notification(Base, BaseModel):
1968 1968 __tablename__ = 'notifications'
1969 1969 __table_args__ = (
1970 1970 Index('notification_type_idx', 'type'),
1971 1971 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1972 1972 'mysql_charset': 'utf8'},
1973 1973 )
1974 1974
1975 1975 TYPE_CHANGESET_COMMENT = u'cs_comment'
1976 1976 TYPE_MESSAGE = u'message'
1977 1977 TYPE_MENTION = u'mention'
1978 1978 TYPE_REGISTRATION = u'registration'
1979 1979 TYPE_PULL_REQUEST = u'pull_request'
1980 1980 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1981 1981
1982 1982 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1983 1983 subject = Column('subject', Unicode(512), nullable=True)
1984 1984 body = Column('body', UnicodeText(50000), nullable=True)
1985 1985 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1986 1986 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1987 1987 type_ = Column('type', Unicode(256))
1988 1988
1989 1989 created_by_user = relationship('User')
1990 1990 notifications_to_users = relationship('UserNotification', lazy='joined',
1991 1991 cascade="all, delete, delete-orphan")
1992 1992
1993 1993 @property
1994 1994 def recipients(self):
1995 1995 return [x.user for x in UserNotification.query()\
1996 1996 .filter(UserNotification.notification == self)\
1997 1997 .order_by(UserNotification.user_id.asc()).all()]
1998 1998
1999 1999 @classmethod
2000 2000 def create(cls, created_by, subject, body, recipients, type_=None):
2001 2001 if type_ is None:
2002 2002 type_ = Notification.TYPE_MESSAGE
2003 2003
2004 2004 notification = cls()
2005 2005 notification.created_by_user = created_by
2006 2006 notification.subject = subject
2007 2007 notification.body = body
2008 2008 notification.type_ = type_
2009 2009 notification.created_on = datetime.datetime.now()
2010 2010
2011 2011 for u in recipients:
2012 2012 assoc = UserNotification()
2013 2013 assoc.notification = notification
2014 2014 u.notifications.append(assoc)
2015 2015 Session().add(notification)
2016 2016 return notification
2017 2017
2018 2018 @property
2019 2019 def description(self):
2020 2020 from rhodecode.model.notification import NotificationModel
2021 2021 return NotificationModel().make_description(self)
2022 2022
2023 2023
2024 2024 class UserNotification(Base, BaseModel):
2025 2025 __tablename__ = 'user_to_notification'
2026 2026 __table_args__ = (
2027 2027 UniqueConstraint('user_id', 'notification_id'),
2028 2028 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2029 2029 'mysql_charset': 'utf8'}
2030 2030 )
2031 2031 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2032 2032 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2033 2033 read = Column('read', Boolean, default=False)
2034 2034 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2035 2035
2036 2036 user = relationship('User', lazy="joined")
2037 2037 notification = relationship('Notification', lazy="joined",
2038 2038 order_by=lambda: Notification.created_on.desc(),)
2039 2039
2040 2040 def mark_as_read(self):
2041 2041 self.read = True
2042 2042 Session().add(self)
2043 2043
2044 2044
2045 2045 class DbMigrateVersion(Base, BaseModel):
2046 2046 __tablename__ = 'db_migrate_version'
2047 2047 __table_args__ = (
2048 2048 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2049 2049 'mysql_charset': 'utf8'},
2050 2050 )
2051 2051 repository_id = Column('repository_id', String(250), primary_key=True)
2052 2052 repository_path = Column('repository_path', Text)
2053 2053 version = Column('version', Integer)
General Comments 0
You need to be logged in to leave comments. Login now