##// END OF EJS Templates
Moved inject ui into base vcs classe
marcink -
r3477:951aa274 beta
parent child Browse files
Show More
@@ -1,997 +1,1018 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.base
4 4 ~~~~~~~~~~~~~~~~~
5 5
6 6 Base for all available scm backends
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12
13 13 from itertools import chain
14 14 from rhodecode.lib.vcs.utils import author_name, author_email
15 15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
17 17 from rhodecode.lib.vcs.conf import settings
18 18
19 19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
20 20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
21 21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
22 22 RepositoryError
23 23
24 24
25 25 class BaseRepository(object):
26 26 """
27 27 Base Repository for final backends
28 28
29 29 **Attributes**
30 30
31 31 ``DEFAULT_BRANCH_NAME``
32 32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33 33
34 34 ``scm``
35 35 alias of scm, i.e. *git* or *hg*
36 36
37 37 ``repo``
38 38 object from external api
39 39
40 40 ``revisions``
41 41 list of all available revisions' ids, in ascending order
42 42
43 43 ``changesets``
44 44 storage dict caching returned changesets
45 45
46 46 ``path``
47 47 absolute path to the repository
48 48
49 49 ``branches``
50 50 branches as list of changesets
51 51
52 52 ``tags``
53 53 tags as list of changesets
54 54 """
55 55 scm = None
56 56 DEFAULT_BRANCH_NAME = None
57 57 EMPTY_CHANGESET = '0' * 40
58 58
59 59 def __init__(self, repo_path, create=False, **kwargs):
60 60 """
61 61 Initializes repository. Raises RepositoryError if repository could
62 62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 63 exists and ``create`` is set to True.
64 64
65 65 :param repo_path: local path of the repository
66 66 :param create=False: if set to True, would try to craete repository.
67 67 :param src_url=None: if set, should be proper url from which repository
68 68 would be cloned; requires ``create`` parameter to be set to True -
69 69 raises RepositoryError if src_url is set and create evaluates to
70 70 False
71 71 """
72 72 raise NotImplementedError
73 73
74 74 def __str__(self):
75 75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76 76
77 77 def __repr__(self):
78 78 return self.__str__()
79 79
80 80 def __len__(self):
81 81 return self.count()
82 82
83 83 @LazyProperty
84 84 def alias(self):
85 85 for k, v in settings.BACKENDS.items():
86 86 if v.split('.')[-1] == str(self.__class__.__name__):
87 87 return k
88 88
89 89 @LazyProperty
90 90 def name(self):
91 91 raise NotImplementedError
92 92
93 93 @LazyProperty
94 94 def owner(self):
95 95 raise NotImplementedError
96 96
97 97 @LazyProperty
98 98 def description(self):
99 99 raise NotImplementedError
100 100
101 101 @LazyProperty
102 102 def size(self):
103 103 """
104 104 Returns combined size in bytes for all repository files
105 105 """
106 106
107 107 size = 0
108 108 try:
109 109 tip = self.get_changeset()
110 110 for topnode, dirs, files in tip.walk('/'):
111 111 for f in files:
112 112 size += tip.get_file_size(f.path)
113 113 for dir in dirs:
114 114 for f in files:
115 115 size += tip.get_file_size(f.path)
116 116
117 117 except RepositoryError, e:
118 118 pass
119 119 return size
120 120
121 121 def is_valid(self):
122 122 """
123 123 Validates repository.
124 124 """
125 125 raise NotImplementedError
126 126
127 127 def get_last_change(self):
128 128 self.get_changesets()
129 129
130 130 #==========================================================================
131 131 # CHANGESETS
132 132 #==========================================================================
133 133
134 134 def get_changeset(self, revision=None):
135 135 """
136 136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 137 recent changeset is returned.
138 138
139 139 :raises ``EmptyRepositoryError``: if there are no revisions
140 140 """
141 141 raise NotImplementedError
142 142
143 143 def __iter__(self):
144 144 """
145 145 Allows Repository objects to be iterated.
146 146
147 147 *Requires* implementation of ``__getitem__`` method.
148 148 """
149 149 for revision in self.revisions:
150 150 yield self.get_changeset(revision)
151 151
152 152 def get_changesets(self, start=None, end=None, start_date=None,
153 153 end_date=None, branch_name=None, reverse=False):
154 154 """
155 155 Returns iterator of ``MercurialChangeset`` objects from start to end
156 156 not inclusive This should behave just like a list, ie. end is not
157 157 inclusive
158 158
159 159 :param start: None or str
160 160 :param end: None or str
161 161 :param start_date:
162 162 :param end_date:
163 163 :param branch_name:
164 164 :param reversed:
165 165 """
166 166 raise NotImplementedError
167 167
168 168 def __getslice__(self, i, j):
169 169 """
170 170 Returns a iterator of sliced repository
171 171 """
172 172 for rev in self.revisions[i:j]:
173 173 yield self.get_changeset(rev)
174 174
175 175 def __getitem__(self, key):
176 176 return self.get_changeset(key)
177 177
178 178 def count(self):
179 179 return len(self.revisions)
180 180
181 181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 182 """
183 183 Creates and returns a tag for the given ``revision``.
184 184
185 185 :param name: name for new tag
186 186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 187 :param revision: changeset id for which new tag would be created
188 188 :param message: message of the tag's commit
189 189 :param date: date of tag's commit
190 190
191 191 :raises TagAlreadyExistError: if tag with same name already exists
192 192 """
193 193 raise NotImplementedError
194 194
195 195 def remove_tag(self, name, user, message=None, date=None):
196 196 """
197 197 Removes tag with the given ``name``.
198 198
199 199 :param name: name of the tag to be removed
200 200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 201 :param message: message of the tag's removal commit
202 202 :param date: date of tag's removal commit
203 203
204 204 :raises TagDoesNotExistError: if tag with given name does not exists
205 205 """
206 206 raise NotImplementedError
207 207
208 208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 209 context=3):
210 210 """
211 211 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 212 ``rev2`` since ``rev1``.
213 213
214 214 :param rev1: Entry point from which diff is shown. Can be
215 215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 216 the changes since empty state of the repository until ``rev2``
217 217 :param rev2: Until which revision changes should be shown.
218 218 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 219 changes. Defaults to ``False``.
220 220 :param context: How many lines before/after changed lines should be
221 221 shown. Defaults to ``3``.
222 222 """
223 223 raise NotImplementedError
224 224
225 225 # ========== #
226 226 # COMMIT API #
227 227 # ========== #
228 228
229 229 @LazyProperty
230 230 def in_memory_changeset(self):
231 231 """
232 232 Returns ``InMemoryChangeset`` object for this repository.
233 233 """
234 234 raise NotImplementedError
235 235
236 236 def add(self, filenode, **kwargs):
237 237 """
238 238 Commit api function that will add given ``FileNode`` into this
239 239 repository.
240 240
241 241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 242 already in repository
243 243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 244 *added*
245 245 """
246 246 raise NotImplementedError
247 247
248 248 def remove(self, filenode, **kwargs):
249 249 """
250 250 Commit api function that will remove given ``FileNode`` into this
251 251 repository.
252 252
253 253 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 254 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 255 """
256 256 raise NotImplementedError
257 257
258 258 def commit(self, message, **kwargs):
259 259 """
260 260 Persists current changes made on this repository and returns newly
261 261 created changeset.
262 262
263 263 :raises ``NothingChangedError``: if no changes has been made
264 264 """
265 265 raise NotImplementedError
266 266
267 267 def get_state(self):
268 268 """
269 269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 270 containing ``FileNode`` objects.
271 271 """
272 272 raise NotImplementedError
273 273
274 274 def get_config_value(self, section, name, config_file=None):
275 275 """
276 276 Returns configuration value for a given [``section``] and ``name``.
277 277
278 278 :param section: Section we want to retrieve value from
279 279 :param name: Name of configuration we want to retrieve
280 280 :param config_file: A path to file which should be used to retrieve
281 281 configuration from (might also be a list of file paths)
282 282 """
283 283 raise NotImplementedError
284 284
285 285 def get_user_name(self, config_file=None):
286 286 """
287 287 Returns user's name from global configuration file.
288 288
289 289 :param config_file: A path to file which should be used to retrieve
290 290 configuration from (might also be a list of file paths)
291 291 """
292 292 raise NotImplementedError
293 293
294 294 def get_user_email(self, config_file=None):
295 295 """
296 296 Returns user's email from global configuration file.
297 297
298 298 :param config_file: A path to file which should be used to retrieve
299 299 configuration from (might also be a list of file paths)
300 300 """
301 301 raise NotImplementedError
302 302
303 303 # =========== #
304 304 # WORKDIR API #
305 305 # =========== #
306 306
307 307 @LazyProperty
308 308 def workdir(self):
309 309 """
310 310 Returns ``Workdir`` instance for this repository.
311 311 """
312 312 raise NotImplementedError
313 313
314 def inject_ui(self, **extras):
315 """
316 Injects extra parameters into UI object of this repo
317 """
318 required_extras = [
319 'ip',
320 'username',
321 'action',
322 'repository',
323 'scm',
324 'config',
325 'server_url',
326 'make_lock',
327 'locked_by',
328 ]
329 for req in required_extras:
330 if req not in extras:
331 raise AttributeError('Missing attribute %s in extras' % (req))
332 for k, v in extras.items():
333 self._repo.ui.setconfig('rhodecode_extras', k, v)
334
314 335
315 336 class BaseChangeset(object):
316 337 """
317 338 Each backend should implement it's changeset representation.
318 339
319 340 **Attributes**
320 341
321 342 ``repository``
322 343 repository object within which changeset exists
323 344
324 345 ``id``
325 346 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
326 347
327 348 ``raw_id``
328 349 raw changeset representation (i.e. full 40 length sha for git
329 350 backend)
330 351
331 352 ``short_id``
332 353 shortened (if apply) version of ``raw_id``; it would be simple
333 354 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
334 355 as ``raw_id`` for subversion
335 356
336 357 ``revision``
337 358 revision number as integer
338 359
339 360 ``files``
340 361 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
341 362
342 363 ``dirs``
343 364 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
344 365
345 366 ``nodes``
346 367 combined list of ``Node`` objects
347 368
348 369 ``author``
349 370 author of the changeset, as unicode
350 371
351 372 ``message``
352 373 message of the changeset, as unicode
353 374
354 375 ``parents``
355 376 list of parent changesets
356 377
357 378 ``last``
358 379 ``True`` if this is last changeset in repository, ``False``
359 380 otherwise; trying to access this attribute while there is no
360 381 changesets would raise ``EmptyRepositoryError``
361 382 """
362 383 def __str__(self):
363 384 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
364 385 self.short_id)
365 386
366 387 def __repr__(self):
367 388 return self.__str__()
368 389
369 390 def __unicode__(self):
370 391 return u'%s:%s' % (self.revision, self.short_id)
371 392
372 393 def __eq__(self, other):
373 394 return self.raw_id == other.raw_id
374 395
375 396 def __json__(self):
376 397 return dict(
377 398 short_id=self.short_id,
378 399 raw_id=self.raw_id,
379 400 revision=self.revision,
380 401 message=self.message,
381 402 date=self.date,
382 403 author=self.author,
383 404 )
384 405
385 406 @LazyProperty
386 407 def last(self):
387 408 if self.repository is None:
388 409 raise ChangesetError("Cannot check if it's most recent revision")
389 410 return self.raw_id == self.repository.revisions[-1]
390 411
391 412 @LazyProperty
392 413 def parents(self):
393 414 """
394 415 Returns list of parents changesets.
395 416 """
396 417 raise NotImplementedError
397 418
398 419 @LazyProperty
399 420 def children(self):
400 421 """
401 422 Returns list of children changesets.
402 423 """
403 424 raise NotImplementedError
404 425
405 426 @LazyProperty
406 427 def id(self):
407 428 """
408 429 Returns string identifying this changeset.
409 430 """
410 431 raise NotImplementedError
411 432
412 433 @LazyProperty
413 434 def raw_id(self):
414 435 """
415 436 Returns raw string identifying this changeset.
416 437 """
417 438 raise NotImplementedError
418 439
419 440 @LazyProperty
420 441 def short_id(self):
421 442 """
422 443 Returns shortened version of ``raw_id`` attribute, as string,
423 444 identifying this changeset, useful for web representation.
424 445 """
425 446 raise NotImplementedError
426 447
427 448 @LazyProperty
428 449 def revision(self):
429 450 """
430 451 Returns integer identifying this changeset.
431 452
432 453 """
433 454 raise NotImplementedError
434 455
435 456 @LazyProperty
436 457 def commiter(self):
437 458 """
438 459 Returns Commiter for given commit
439 460 """
440 461
441 462 raise NotImplementedError
442 463
443 464 @LazyProperty
444 465 def commiter_name(self):
445 466 """
446 467 Returns Author name for given commit
447 468 """
448 469
449 470 return author_name(self.commiter)
450 471
451 472 @LazyProperty
452 473 def commiter_email(self):
453 474 """
454 475 Returns Author email address for given commit
455 476 """
456 477
457 478 return author_email(self.commiter)
458 479
459 480 @LazyProperty
460 481 def author(self):
461 482 """
462 483 Returns Author for given commit
463 484 """
464 485
465 486 raise NotImplementedError
466 487
467 488 @LazyProperty
468 489 def author_name(self):
469 490 """
470 491 Returns Author name for given commit
471 492 """
472 493
473 494 return author_name(self.author)
474 495
475 496 @LazyProperty
476 497 def author_email(self):
477 498 """
478 499 Returns Author email address for given commit
479 500 """
480 501
481 502 return author_email(self.author)
482 503
483 504 def get_file_mode(self, path):
484 505 """
485 506 Returns stat mode of the file at the given ``path``.
486 507 """
487 508 raise NotImplementedError
488 509
489 510 def get_file_content(self, path):
490 511 """
491 512 Returns content of the file at the given ``path``.
492 513 """
493 514 raise NotImplementedError
494 515
495 516 def get_file_size(self, path):
496 517 """
497 518 Returns size of the file at the given ``path``.
498 519 """
499 520 raise NotImplementedError
500 521
501 522 def get_file_changeset(self, path):
502 523 """
503 524 Returns last commit of the file at the given ``path``.
504 525 """
505 526 raise NotImplementedError
506 527
507 528 def get_file_history(self, path):
508 529 """
509 530 Returns history of file as reversed list of ``Changeset`` objects for
510 531 which file at given ``path`` has been modified.
511 532 """
512 533 raise NotImplementedError
513 534
514 535 def get_nodes(self, path):
515 536 """
516 537 Returns combined ``DirNode`` and ``FileNode`` objects list representing
517 538 state of changeset at the given ``path``.
518 539
519 540 :raises ``ChangesetError``: if node at the given ``path`` is not
520 541 instance of ``DirNode``
521 542 """
522 543 raise NotImplementedError
523 544
524 545 def get_node(self, path):
525 546 """
526 547 Returns ``Node`` object from the given ``path``.
527 548
528 549 :raises ``NodeDoesNotExistError``: if there is no node at the given
529 550 ``path``
530 551 """
531 552 raise NotImplementedError
532 553
533 554 def fill_archive(self, stream=None, kind='tgz', prefix=None):
534 555 """
535 556 Fills up given stream.
536 557
537 558 :param stream: file like object.
538 559 :param kind: one of following: ``zip``, ``tar``, ``tgz``
539 560 or ``tbz2``. Default: ``tgz``.
540 561 :param prefix: name of root directory in archive.
541 562 Default is repository name and changeset's raw_id joined with dash.
542 563
543 564 repo-tip.<kind>
544 565 """
545 566
546 567 raise NotImplementedError
547 568
548 569 def get_chunked_archive(self, **kwargs):
549 570 """
550 571 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
551 572
552 573 :param chunk_size: extra parameter which controls size of returned
553 574 chunks. Default:8k.
554 575 """
555 576
556 577 chunk_size = kwargs.pop('chunk_size', 8192)
557 578 stream = kwargs.get('stream')
558 579 self.fill_archive(**kwargs)
559 580 while True:
560 581 data = stream.read(chunk_size)
561 582 if not data:
562 583 break
563 584 yield data
564 585
565 586 @LazyProperty
566 587 def root(self):
567 588 """
568 589 Returns ``RootNode`` object for this changeset.
569 590 """
570 591 return self.get_node('')
571 592
572 593 def next(self, branch=None):
573 594 """
574 595 Returns next changeset from current, if branch is gives it will return
575 596 next changeset belonging to this branch
576 597
577 598 :param branch: show changesets within the given named branch
578 599 """
579 600 raise NotImplementedError
580 601
581 602 def prev(self, branch=None):
582 603 """
583 604 Returns previous changeset from current, if branch is gives it will
584 605 return previous changeset belonging to this branch
585 606
586 607 :param branch: show changesets within the given named branch
587 608 """
588 609 raise NotImplementedError
589 610
590 611 @LazyProperty
591 612 def added(self):
592 613 """
593 614 Returns list of added ``FileNode`` objects.
594 615 """
595 616 raise NotImplementedError
596 617
597 618 @LazyProperty
598 619 def changed(self):
599 620 """
600 621 Returns list of modified ``FileNode`` objects.
601 622 """
602 623 raise NotImplementedError
603 624
604 625 @LazyProperty
605 626 def removed(self):
606 627 """
607 628 Returns list of removed ``FileNode`` objects.
608 629 """
609 630 raise NotImplementedError
610 631
611 632 @LazyProperty
612 633 def size(self):
613 634 """
614 635 Returns total number of bytes from contents of all filenodes.
615 636 """
616 637 return sum((node.size for node in self.get_filenodes_generator()))
617 638
618 639 def walk(self, topurl=''):
619 640 """
620 641 Similar to os.walk method. Insted of filesystem it walks through
621 642 changeset starting at given ``topurl``. Returns generator of tuples
622 643 (topnode, dirnodes, filenodes).
623 644 """
624 645 topnode = self.get_node(topurl)
625 646 yield (topnode, topnode.dirs, topnode.files)
626 647 for dirnode in topnode.dirs:
627 648 for tup in self.walk(dirnode.path):
628 649 yield tup
629 650
630 651 def get_filenodes_generator(self):
631 652 """
632 653 Returns generator that yields *all* file nodes.
633 654 """
634 655 for topnode, dirs, files in self.walk():
635 656 for node in files:
636 657 yield node
637 658
638 659 def as_dict(self):
639 660 """
640 661 Returns dictionary with changeset's attributes and their values.
641 662 """
642 663 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
643 664 'revision', 'date', 'message'])
644 665 data['author'] = {'name': self.author_name, 'email': self.author_email}
645 666 data['added'] = [node.path for node in self.added]
646 667 data['changed'] = [node.path for node in self.changed]
647 668 data['removed'] = [node.path for node in self.removed]
648 669 return data
649 670
650 671
651 672 class BaseWorkdir(object):
652 673 """
653 674 Working directory representation of single repository.
654 675
655 676 :attribute: repository: repository object of working directory
656 677 """
657 678
658 679 def __init__(self, repository):
659 680 self.repository = repository
660 681
661 682 def get_branch(self):
662 683 """
663 684 Returns name of current branch.
664 685 """
665 686 raise NotImplementedError
666 687
667 688 def get_changeset(self):
668 689 """
669 690 Returns current changeset.
670 691 """
671 692 raise NotImplementedError
672 693
673 694 def get_added(self):
674 695 """
675 696 Returns list of ``FileNode`` objects marked as *new* in working
676 697 directory.
677 698 """
678 699 raise NotImplementedError
679 700
680 701 def get_changed(self):
681 702 """
682 703 Returns list of ``FileNode`` objects *changed* in working directory.
683 704 """
684 705 raise NotImplementedError
685 706
686 707 def get_removed(self):
687 708 """
688 709 Returns list of ``RemovedFileNode`` objects marked as *removed* in
689 710 working directory.
690 711 """
691 712 raise NotImplementedError
692 713
693 714 def get_untracked(self):
694 715 """
695 716 Returns list of ``FileNode`` objects which are present within working
696 717 directory however are not tracked by repository.
697 718 """
698 719 raise NotImplementedError
699 720
700 721 def get_status(self):
701 722 """
702 723 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
703 724 lists.
704 725 """
705 726 raise NotImplementedError
706 727
707 728 def commit(self, message, **kwargs):
708 729 """
709 730 Commits local (from working directory) changes and returns newly
710 731 created
711 732 ``Changeset``. Updates repository's ``revisions`` list.
712 733
713 734 :raises ``CommitError``: if any error occurs while committing
714 735 """
715 736 raise NotImplementedError
716 737
717 738 def update(self, revision=None):
718 739 """
719 740 Fetches content of the given revision and populates it within working
720 741 directory.
721 742 """
722 743 raise NotImplementedError
723 744
724 745 def checkout_branch(self, branch=None):
725 746 """
726 747 Checks out ``branch`` or the backend's default branch.
727 748
728 749 Raises ``BranchDoesNotExistError`` if the branch does not exist.
729 750 """
730 751 raise NotImplementedError
731 752
732 753
733 754 class BaseInMemoryChangeset(object):
734 755 """
735 756 Represents differences between repository's state (most recent head) and
736 757 changes made *in place*.
737 758
738 759 **Attributes**
739 760
740 761 ``repository``
741 762 repository object for this in-memory-changeset
742 763
743 764 ``added``
744 765 list of ``FileNode`` objects marked as *added*
745 766
746 767 ``changed``
747 768 list of ``FileNode`` objects marked as *changed*
748 769
749 770 ``removed``
750 771 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
751 772 *removed*
752 773
753 774 ``parents``
754 775 list of ``Changeset`` representing parents of in-memory changeset.
755 776 Should always be 2-element sequence.
756 777
757 778 """
758 779
759 780 def __init__(self, repository):
760 781 self.repository = repository
761 782 self.added = []
762 783 self.changed = []
763 784 self.removed = []
764 785 self.parents = []
765 786
766 787 def add(self, *filenodes):
767 788 """
768 789 Marks given ``FileNode`` objects as *to be committed*.
769 790
770 791 :raises ``NodeAlreadyExistsError``: if node with same path exists at
771 792 latest changeset
772 793 :raises ``NodeAlreadyAddedError``: if node with same path is already
773 794 marked as *added*
774 795 """
775 796 # Check if not already marked as *added* first
776 797 for node in filenodes:
777 798 if node.path in (n.path for n in self.added):
778 799 raise NodeAlreadyAddedError("Such FileNode %s is already "
779 800 "marked for addition" % node.path)
780 801 for node in filenodes:
781 802 self.added.append(node)
782 803
783 804 def change(self, *filenodes):
784 805 """
785 806 Marks given ``FileNode`` objects to be *changed* in next commit.
786 807
787 808 :raises ``EmptyRepositoryError``: if there are no changesets yet
788 809 :raises ``NodeAlreadyExistsError``: if node with same path is already
789 810 marked to be *changed*
790 811 :raises ``NodeAlreadyRemovedError``: if node with same path is already
791 812 marked to be *removed*
792 813 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
793 814 changeset
794 815 :raises ``NodeNotChangedError``: if node hasn't really be changed
795 816 """
796 817 for node in filenodes:
797 818 if node.path in (n.path for n in self.removed):
798 819 raise NodeAlreadyRemovedError("Node at %s is already marked "
799 820 "as removed" % node.path)
800 821 try:
801 822 self.repository.get_changeset()
802 823 except EmptyRepositoryError:
803 824 raise EmptyRepositoryError("Nothing to change - try to *add* new "
804 825 "nodes rather than changing them")
805 826 for node in filenodes:
806 827 if node.path in (n.path for n in self.changed):
807 828 raise NodeAlreadyChangedError("Node at '%s' is already "
808 829 "marked as changed" % node.path)
809 830 self.changed.append(node)
810 831
811 832 def remove(self, *filenodes):
812 833 """
813 834 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
814 835 *removed* in next commit.
815 836
816 837 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
817 838 be *removed*
818 839 :raises ``NodeAlreadyChangedError``: if node has been already marked to
819 840 be *changed*
820 841 """
821 842 for node in filenodes:
822 843 if node.path in (n.path for n in self.removed):
823 844 raise NodeAlreadyRemovedError("Node is already marked to "
824 845 "for removal at %s" % node.path)
825 846 if node.path in (n.path for n in self.changed):
826 847 raise NodeAlreadyChangedError("Node is already marked to "
827 848 "be changed at %s" % node.path)
828 849 # We only mark node as *removed* - real removal is done by
829 850 # commit method
830 851 self.removed.append(node)
831 852
832 853 def reset(self):
833 854 """
834 855 Resets this instance to initial state (cleans ``added``, ``changed``
835 856 and ``removed`` lists).
836 857 """
837 858 self.added = []
838 859 self.changed = []
839 860 self.removed = []
840 861 self.parents = []
841 862
842 863 def get_ipaths(self):
843 864 """
844 865 Returns generator of paths from nodes marked as added, changed or
845 866 removed.
846 867 """
847 868 for node in chain(self.added, self.changed, self.removed):
848 869 yield node.path
849 870
850 871 def get_paths(self):
851 872 """
852 873 Returns list of paths from nodes marked as added, changed or removed.
853 874 """
854 875 return list(self.get_ipaths())
855 876
856 877 def check_integrity(self, parents=None):
857 878 """
858 879 Checks in-memory changeset's integrity. Also, sets parents if not
859 880 already set.
860 881
861 882 :raises CommitError: if any error occurs (i.e.
862 883 ``NodeDoesNotExistError``).
863 884 """
864 885 if not self.parents:
865 886 parents = parents or []
866 887 if len(parents) == 0:
867 888 try:
868 889 parents = [self.repository.get_changeset(), None]
869 890 except EmptyRepositoryError:
870 891 parents = [None, None]
871 892 elif len(parents) == 1:
872 893 parents += [None]
873 894 self.parents = parents
874 895
875 896 # Local parents, only if not None
876 897 parents = [p for p in self.parents if p]
877 898
878 899 # Check nodes marked as added
879 900 for p in parents:
880 901 for node in self.added:
881 902 try:
882 903 p.get_node(node.path)
883 904 except NodeDoesNotExistError:
884 905 pass
885 906 else:
886 907 raise NodeAlreadyExistsError("Node at %s already exists "
887 908 "at %s" % (node.path, p))
888 909
889 910 # Check nodes marked as changed
890 911 missing = set(self.changed)
891 912 not_changed = set(self.changed)
892 913 if self.changed and not parents:
893 914 raise NodeDoesNotExistError(str(self.changed[0].path))
894 915 for p in parents:
895 916 for node in self.changed:
896 917 try:
897 918 old = p.get_node(node.path)
898 919 missing.remove(node)
899 920 if old.content != node.content:
900 921 not_changed.remove(node)
901 922 except NodeDoesNotExistError:
902 923 pass
903 924 if self.changed and missing:
904 925 raise NodeDoesNotExistError("Node at %s is missing "
905 926 "(parents: %s)" % (node.path, parents))
906 927
907 928 if self.changed and not_changed:
908 929 raise NodeNotChangedError("Node at %s wasn't actually changed "
909 930 "since parents' changesets: %s" % (not_changed.pop().path,
910 931 parents)
911 932 )
912 933
913 934 # Check nodes marked as removed
914 935 if self.removed and not parents:
915 936 raise NodeDoesNotExistError("Cannot remove node at %s as there "
916 937 "were no parents specified" % self.removed[0].path)
917 938 really_removed = set()
918 939 for p in parents:
919 940 for node in self.removed:
920 941 try:
921 942 p.get_node(node.path)
922 943 really_removed.add(node)
923 944 except ChangesetError:
924 945 pass
925 946 not_removed = set(self.removed) - really_removed
926 947 if not_removed:
927 948 raise NodeDoesNotExistError("Cannot remove node at %s from "
928 949 "following parents: %s" % (not_removed[0], parents))
929 950
930 951 def commit(self, message, author, parents=None, branch=None, date=None,
931 952 **kwargs):
932 953 """
933 954 Performs in-memory commit (doesn't check workdir in any way) and
934 955 returns newly created ``Changeset``. Updates repository's
935 956 ``revisions``.
936 957
937 958 .. note::
938 959 While overriding this method each backend's should call
939 960 ``self.check_integrity(parents)`` in the first place.
940 961
941 962 :param message: message of the commit
942 963 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
943 964 :param parents: single parent or sequence of parents from which commit
944 965 would be derieved
945 966 :param date: ``datetime.datetime`` instance. Defaults to
946 967 ``datetime.datetime.now()``.
947 968 :param branch: branch name, as string. If none given, default backend's
948 969 branch would be used.
949 970
950 971 :raises ``CommitError``: if any error occurs while committing
951 972 """
952 973 raise NotImplementedError
953 974
954 975
955 976 class EmptyChangeset(BaseChangeset):
956 977 """
957 978 An dummy empty changeset. It's possible to pass hash when creating
958 979 an EmptyChangeset
959 980 """
960 981
961 982 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
962 983 alias=None, revision=-1, message='', author='', date=''):
963 984 self._empty_cs = cs
964 985 self.revision = revision
965 986 self.message = message
966 987 self.author = author
967 988 self.date = date
968 989 self.repository = repo
969 990 self.requested_revision = requested_revision
970 991 self.alias = alias
971 992
972 993 @LazyProperty
973 994 def raw_id(self):
974 995 """
975 996 Returns raw string identifying this changeset, useful for web
976 997 representation.
977 998 """
978 999
979 1000 return self._empty_cs
980 1001
981 1002 @LazyProperty
982 1003 def branch(self):
983 1004 from rhodecode.lib.vcs.backends import get_backend
984 1005 return get_backend(self.alias).DEFAULT_BRANCH_NAME
985 1006
986 1007 @LazyProperty
987 1008 def short_id(self):
988 1009 return self.raw_id[:12]
989 1010
990 1011 def get_file_changeset(self, path):
991 1012 return self
992 1013
993 1014 def get_file_content(self, path):
994 1015 return u''
995 1016
996 1017 def get_file_size(self, path):
997 1018 return 0
@@ -1,2061 +1,2053 b''
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 from rhodecode.lib.vcs.backends.hg import MercurialRepository
942 from rhodecode.lib.vcs.backends.git import GitRepository
943 required = (MercurialRepository, GitRepository)
944 if not isinstance(repo, required):
945 raise Exception('repo must be instance of %s' % required)
946
947 # inject ui extra param to log this action via push logger
948 for k, v in extras.items():
949 repo._repo.ui.setconfig('rhodecode_extras', k, v)
941 repo.inject_ui(extras)
950 942
951 943 @classmethod
952 944 def is_valid(cls, repo_name):
953 945 """
954 946 returns True if given repo name is a valid filesystem repository
955 947
956 948 :param cls:
957 949 :param repo_name:
958 950 """
959 951 from rhodecode.lib.utils import is_valid_repo
960 952
961 953 return is_valid_repo(repo_name, cls.base_path())
962 954
963 955 def get_api_data(self):
964 956 """
965 957 Common function for generating repo api data
966 958
967 959 """
968 960 repo = self
969 961 data = dict(
970 962 repo_id=repo.repo_id,
971 963 repo_name=repo.repo_name,
972 964 repo_type=repo.repo_type,
973 965 clone_uri=repo.clone_uri,
974 966 private=repo.private,
975 967 created_on=repo.created_on,
976 968 description=repo.description,
977 969 landing_rev=repo.landing_rev,
978 970 owner=repo.user.username,
979 971 fork_of=repo.fork.repo_name if repo.fork else None,
980 972 enable_statistics=repo.enable_statistics,
981 973 enable_locking=repo.enable_locking,
982 974 enable_downloads=repo.enable_downloads,
983 975 last_changeset=repo.changeset_cache
984 976 )
985 977 rc_config = RhodeCodeSetting.get_app_settings()
986 978 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
987 979 if repository_fields:
988 980 for f in self.extra_fields:
989 981 data[f.field_key_prefixed] = f.field_value
990 982
991 983 return data
992 984
993 985 @classmethod
994 986 def lock(cls, repo, user_id):
995 987 repo.locked = [user_id, time.time()]
996 988 Session().add(repo)
997 989 Session().commit()
998 990
999 991 @classmethod
1000 992 def unlock(cls, repo):
1001 993 repo.locked = None
1002 994 Session().add(repo)
1003 995 Session().commit()
1004 996
1005 997 @classmethod
1006 998 def getlock(cls, repo):
1007 999 return repo.locked
1008 1000
1009 1001 @property
1010 1002 def last_db_change(self):
1011 1003 return self.updated_on
1012 1004
1013 1005 def clone_url(self, **override):
1014 1006 from pylons import url
1015 1007 from urlparse import urlparse
1016 1008 import urllib
1017 1009 parsed_url = urlparse(url('home', qualified=True))
1018 1010 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1019 1011 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1020 1012 args = {
1021 1013 'user': '',
1022 1014 'pass': '',
1023 1015 'scheme': parsed_url.scheme,
1024 1016 'netloc': parsed_url.netloc,
1025 1017 'prefix': decoded_path,
1026 1018 'path': self.repo_name
1027 1019 }
1028 1020
1029 1021 args.update(override)
1030 1022 return default_clone_uri % args
1031 1023
1032 1024 #==========================================================================
1033 1025 # SCM PROPERTIES
1034 1026 #==========================================================================
1035 1027
1036 1028 def get_changeset(self, rev=None):
1037 1029 return get_changeset_safe(self.scm_instance, rev)
1038 1030
1039 1031 def get_landing_changeset(self):
1040 1032 """
1041 1033 Returns landing changeset, or if that doesn't exist returns the tip
1042 1034 """
1043 1035 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1044 1036 return cs
1045 1037
1046 1038 def update_changeset_cache(self, cs_cache=None):
1047 1039 """
1048 1040 Update cache of last changeset for repository, keys should be::
1049 1041
1050 1042 short_id
1051 1043 raw_id
1052 1044 revision
1053 1045 message
1054 1046 date
1055 1047 author
1056 1048
1057 1049 :param cs_cache:
1058 1050 """
1059 1051 from rhodecode.lib.vcs.backends.base import BaseChangeset
1060 1052 if cs_cache is None:
1061 1053 cs_cache = EmptyChangeset()
1062 1054 # use no-cache version here
1063 1055 scm_repo = self.scm_instance_no_cache
1064 1056 if scm_repo:
1065 1057 cs_cache = scm_repo.get_changeset()
1066 1058
1067 1059 if isinstance(cs_cache, BaseChangeset):
1068 1060 cs_cache = cs_cache.__json__()
1069 1061
1070 1062 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1071 1063 _default = datetime.datetime.fromtimestamp(0)
1072 1064 last_change = cs_cache.get('date') or _default
1073 1065 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
1074 1066 self.updated_on = last_change
1075 1067 self.changeset_cache = cs_cache
1076 1068 Session().add(self)
1077 1069 Session().commit()
1078 1070 else:
1079 1071 log.debug('Skipping repo:%s already with latest changes' % self)
1080 1072
1081 1073 @property
1082 1074 def tip(self):
1083 1075 return self.get_changeset('tip')
1084 1076
1085 1077 @property
1086 1078 def author(self):
1087 1079 return self.tip.author
1088 1080
1089 1081 @property
1090 1082 def last_change(self):
1091 1083 return self.scm_instance.last_change
1092 1084
1093 1085 def get_comments(self, revisions=None):
1094 1086 """
1095 1087 Returns comments for this repository grouped by revisions
1096 1088
1097 1089 :param revisions: filter query by revisions only
1098 1090 """
1099 1091 cmts = ChangesetComment.query()\
1100 1092 .filter(ChangesetComment.repo == self)
1101 1093 if revisions:
1102 1094 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1103 1095 grouped = defaultdict(list)
1104 1096 for cmt in cmts.all():
1105 1097 grouped[cmt.revision].append(cmt)
1106 1098 return grouped
1107 1099
1108 1100 def statuses(self, revisions=None):
1109 1101 """
1110 1102 Returns statuses for this repository
1111 1103
1112 1104 :param revisions: list of revisions to get statuses for
1113 1105 :type revisions: list
1114 1106 """
1115 1107
1116 1108 statuses = ChangesetStatus.query()\
1117 1109 .filter(ChangesetStatus.repo == self)\
1118 1110 .filter(ChangesetStatus.version == 0)
1119 1111 if revisions:
1120 1112 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1121 1113 grouped = {}
1122 1114
1123 1115 #maybe we have open new pullrequest without a status ?
1124 1116 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1125 1117 status_lbl = ChangesetStatus.get_status_lbl(stat)
1126 1118 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1127 1119 for rev in pr.revisions:
1128 1120 pr_id = pr.pull_request_id
1129 1121 pr_repo = pr.other_repo.repo_name
1130 1122 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1131 1123
1132 1124 for stat in statuses.all():
1133 1125 pr_id = pr_repo = None
1134 1126 if stat.pull_request:
1135 1127 pr_id = stat.pull_request.pull_request_id
1136 1128 pr_repo = stat.pull_request.other_repo.repo_name
1137 1129 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1138 1130 pr_id, pr_repo]
1139 1131 return grouped
1140 1132
1141 1133 def _repo_size(self):
1142 1134 from rhodecode.lib import helpers as h
1143 1135 log.debug('calculating repository size...')
1144 1136 return h.format_byte_size(self.scm_instance.size)
1145 1137
1146 1138 #==========================================================================
1147 1139 # SCM CACHE INSTANCE
1148 1140 #==========================================================================
1149 1141
1150 1142 @property
1151 1143 def invalidate(self):
1152 1144 return CacheInvalidation.invalidate(self.repo_name)
1153 1145
1154 1146 def set_invalidate(self):
1155 1147 """
1156 1148 set a cache for invalidation for this instance
1157 1149 """
1158 1150 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1159 1151
1160 1152 @LazyProperty
1161 1153 def scm_instance_no_cache(self):
1162 1154 return self.__get_instance()
1163 1155
1164 1156 @LazyProperty
1165 1157 def scm_instance(self):
1166 1158 import rhodecode
1167 1159 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1168 1160 if full_cache:
1169 1161 return self.scm_instance_cached()
1170 1162 return self.__get_instance()
1171 1163
1172 1164 def scm_instance_cached(self, cache_map=None):
1173 1165 @cache_region('long_term')
1174 1166 def _c(repo_name):
1175 1167 return self.__get_instance()
1176 1168 rn = self.repo_name
1177 1169 log.debug('Getting cached instance of repo')
1178 1170
1179 1171 if cache_map:
1180 1172 # get using prefilled cache_map
1181 1173 invalidate_repo = cache_map[self.repo_name]
1182 1174 if invalidate_repo:
1183 1175 invalidate_repo = (None if invalidate_repo.cache_active
1184 1176 else invalidate_repo)
1185 1177 else:
1186 1178 # get from invalidate
1187 1179 invalidate_repo = self.invalidate
1188 1180
1189 1181 if invalidate_repo is not None:
1190 1182 region_invalidate(_c, None, rn)
1191 1183 # update our cache
1192 1184 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1193 1185 return _c(rn)
1194 1186
1195 1187 def __get_instance(self):
1196 1188 repo_full_path = self.repo_full_path
1197 1189 try:
1198 1190 alias = get_scm(repo_full_path)[0]
1199 1191 log.debug('Creating instance of %s repository from %s'
1200 1192 % (alias, repo_full_path))
1201 1193 backend = get_backend(alias)
1202 1194 except VCSError:
1203 1195 log.error(traceback.format_exc())
1204 1196 log.error('Perhaps this repository is in db and not in '
1205 1197 'filesystem run rescan repositories with '
1206 1198 '"destroy old data " option from admin panel')
1207 1199 return
1208 1200
1209 1201 if alias == 'hg':
1210 1202
1211 1203 repo = backend(safe_str(repo_full_path), create=False,
1212 1204 baseui=self._ui)
1213 1205 # skip hidden web repository
1214 1206 if repo._get_hidden():
1215 1207 return
1216 1208 else:
1217 1209 repo = backend(repo_full_path, create=False)
1218 1210
1219 1211 return repo
1220 1212
1221 1213
1222 1214 class RepoGroup(Base, BaseModel):
1223 1215 __tablename__ = 'groups'
1224 1216 __table_args__ = (
1225 1217 UniqueConstraint('group_name', 'group_parent_id'),
1226 1218 CheckConstraint('group_id != group_parent_id'),
1227 1219 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1228 1220 'mysql_charset': 'utf8'},
1229 1221 )
1230 1222 __mapper_args__ = {'order_by': 'group_name'}
1231 1223
1232 1224 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1233 1225 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1234 1226 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1235 1227 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1236 1228 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1237 1229
1238 1230 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1239 1231 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1240 1232
1241 1233 parent_group = relationship('RepoGroup', remote_side=group_id)
1242 1234
1243 1235 def __init__(self, group_name='', parent_group=None):
1244 1236 self.group_name = group_name
1245 1237 self.parent_group = parent_group
1246 1238
1247 1239 def __unicode__(self):
1248 1240 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1249 1241 self.group_name)
1250 1242
1251 1243 @classmethod
1252 1244 def groups_choices(cls, groups=None, show_empty_group=True):
1253 1245 from webhelpers.html import literal as _literal
1254 1246 if not groups:
1255 1247 groups = cls.query().all()
1256 1248
1257 1249 repo_groups = []
1258 1250 if show_empty_group:
1259 1251 repo_groups = [('-1', '-- no parent --')]
1260 1252 sep = ' &raquo; '
1261 1253 _name = lambda k: _literal(sep.join(k))
1262 1254
1263 1255 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1264 1256 for x in groups])
1265 1257
1266 1258 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1267 1259 return repo_groups
1268 1260
1269 1261 @classmethod
1270 1262 def url_sep(cls):
1271 1263 return URL_SEP
1272 1264
1273 1265 @classmethod
1274 1266 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1275 1267 if case_insensitive:
1276 1268 gr = cls.query()\
1277 1269 .filter(cls.group_name.ilike(group_name))
1278 1270 else:
1279 1271 gr = cls.query()\
1280 1272 .filter(cls.group_name == group_name)
1281 1273 if cache:
1282 1274 gr = gr.options(FromCache(
1283 1275 "sql_cache_short",
1284 1276 "get_group_%s" % _hash_key(group_name)
1285 1277 )
1286 1278 )
1287 1279 return gr.scalar()
1288 1280
1289 1281 @property
1290 1282 def parents(self):
1291 1283 parents_recursion_limit = 5
1292 1284 groups = []
1293 1285 if self.parent_group is None:
1294 1286 return groups
1295 1287 cur_gr = self.parent_group
1296 1288 groups.insert(0, cur_gr)
1297 1289 cnt = 0
1298 1290 while 1:
1299 1291 cnt += 1
1300 1292 gr = getattr(cur_gr, 'parent_group', None)
1301 1293 cur_gr = cur_gr.parent_group
1302 1294 if gr is None:
1303 1295 break
1304 1296 if cnt == parents_recursion_limit:
1305 1297 # this will prevent accidental infinit loops
1306 1298 log.error('group nested more than %s' %
1307 1299 parents_recursion_limit)
1308 1300 break
1309 1301
1310 1302 groups.insert(0, gr)
1311 1303 return groups
1312 1304
1313 1305 @property
1314 1306 def children(self):
1315 1307 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1316 1308
1317 1309 @property
1318 1310 def name(self):
1319 1311 return self.group_name.split(RepoGroup.url_sep())[-1]
1320 1312
1321 1313 @property
1322 1314 def full_path(self):
1323 1315 return self.group_name
1324 1316
1325 1317 @property
1326 1318 def full_path_splitted(self):
1327 1319 return self.group_name.split(RepoGroup.url_sep())
1328 1320
1329 1321 @property
1330 1322 def repositories(self):
1331 1323 return Repository.query()\
1332 1324 .filter(Repository.group == self)\
1333 1325 .order_by(Repository.repo_name)
1334 1326
1335 1327 @property
1336 1328 def repositories_recursive_count(self):
1337 1329 cnt = self.repositories.count()
1338 1330
1339 1331 def children_count(group):
1340 1332 cnt = 0
1341 1333 for child in group.children:
1342 1334 cnt += child.repositories.count()
1343 1335 cnt += children_count(child)
1344 1336 return cnt
1345 1337
1346 1338 return cnt + children_count(self)
1347 1339
1348 1340 def _recursive_objects(self, include_repos=True):
1349 1341 all_ = []
1350 1342
1351 1343 def _get_members(root_gr):
1352 1344 if include_repos:
1353 1345 for r in root_gr.repositories:
1354 1346 all_.append(r)
1355 1347 childs = root_gr.children.all()
1356 1348 if childs:
1357 1349 for gr in childs:
1358 1350 all_.append(gr)
1359 1351 _get_members(gr)
1360 1352
1361 1353 _get_members(self)
1362 1354 return [self] + all_
1363 1355
1364 1356 def recursive_groups_and_repos(self):
1365 1357 """
1366 1358 Recursive return all groups, with repositories in those groups
1367 1359 """
1368 1360 return self._recursive_objects()
1369 1361
1370 1362 def recursive_groups(self):
1371 1363 """
1372 1364 Returns all children groups for this group including children of children
1373 1365 """
1374 1366 return self._recursive_objects(include_repos=False)
1375 1367
1376 1368 def get_new_name(self, group_name):
1377 1369 """
1378 1370 returns new full group name based on parent and new name
1379 1371
1380 1372 :param group_name:
1381 1373 """
1382 1374 path_prefix = (self.parent_group.full_path_splitted if
1383 1375 self.parent_group else [])
1384 1376 return RepoGroup.url_sep().join(path_prefix + [group_name])
1385 1377
1386 1378
1387 1379 class Permission(Base, BaseModel):
1388 1380 __tablename__ = 'permissions'
1389 1381 __table_args__ = (
1390 1382 Index('p_perm_name_idx', 'permission_name'),
1391 1383 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1392 1384 'mysql_charset': 'utf8'},
1393 1385 )
1394 1386 PERMS = [
1395 1387 ('repository.none', _('Repository no access')),
1396 1388 ('repository.read', _('Repository read access')),
1397 1389 ('repository.write', _('Repository write access')),
1398 1390 ('repository.admin', _('Repository admin access')),
1399 1391
1400 1392 ('group.none', _('Repository group no access')),
1401 1393 ('group.read', _('Repository group read access')),
1402 1394 ('group.write', _('Repository group write access')),
1403 1395 ('group.admin', _('Repository group admin access')),
1404 1396
1405 1397 ('hg.admin', _('RhodeCode Administrator')),
1406 1398 ('hg.create.none', _('Repository creation disabled')),
1407 1399 ('hg.create.repository', _('Repository creation enabled')),
1408 1400 ('hg.fork.none', _('Repository forking disabled')),
1409 1401 ('hg.fork.repository', _('Repository forking enabled')),
1410 1402 ('hg.register.none', _('Register disabled')),
1411 1403 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1412 1404 'with manual activation')),
1413 1405
1414 1406 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1415 1407 'with auto activation')),
1416 1408 ]
1417 1409
1418 1410 # defines which permissions are more important higher the more important
1419 1411 PERM_WEIGHTS = {
1420 1412 'repository.none': 0,
1421 1413 'repository.read': 1,
1422 1414 'repository.write': 3,
1423 1415 'repository.admin': 4,
1424 1416
1425 1417 'group.none': 0,
1426 1418 'group.read': 1,
1427 1419 'group.write': 3,
1428 1420 'group.admin': 4,
1429 1421
1430 1422 'hg.fork.none': 0,
1431 1423 'hg.fork.repository': 1,
1432 1424 'hg.create.none': 0,
1433 1425 'hg.create.repository':1
1434 1426 }
1435 1427
1436 1428 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1437 1429 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1438 1430 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1439 1431
1440 1432 def __unicode__(self):
1441 1433 return u"<%s('%s:%s')>" % (
1442 1434 self.__class__.__name__, self.permission_id, self.permission_name
1443 1435 )
1444 1436
1445 1437 @classmethod
1446 1438 def get_by_key(cls, key):
1447 1439 return cls.query().filter(cls.permission_name == key).scalar()
1448 1440
1449 1441 @classmethod
1450 1442 def get_default_perms(cls, default_user_id):
1451 1443 q = Session().query(UserRepoToPerm, Repository, cls)\
1452 1444 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1453 1445 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1454 1446 .filter(UserRepoToPerm.user_id == default_user_id)
1455 1447
1456 1448 return q.all()
1457 1449
1458 1450 @classmethod
1459 1451 def get_default_group_perms(cls, default_user_id):
1460 1452 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1461 1453 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1462 1454 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1463 1455 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1464 1456
1465 1457 return q.all()
1466 1458
1467 1459
1468 1460 class UserRepoToPerm(Base, BaseModel):
1469 1461 __tablename__ = 'repo_to_perm'
1470 1462 __table_args__ = (
1471 1463 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1472 1464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1473 1465 'mysql_charset': 'utf8'}
1474 1466 )
1475 1467 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1476 1468 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1477 1469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1478 1470 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1479 1471
1480 1472 user = relationship('User')
1481 1473 repository = relationship('Repository')
1482 1474 permission = relationship('Permission')
1483 1475
1484 1476 @classmethod
1485 1477 def create(cls, user, repository, permission):
1486 1478 n = cls()
1487 1479 n.user = user
1488 1480 n.repository = repository
1489 1481 n.permission = permission
1490 1482 Session().add(n)
1491 1483 return n
1492 1484
1493 1485 def __unicode__(self):
1494 1486 return u'<user:%s => %s >' % (self.user, self.repository)
1495 1487
1496 1488
1497 1489 class UserToPerm(Base, BaseModel):
1498 1490 __tablename__ = 'user_to_perm'
1499 1491 __table_args__ = (
1500 1492 UniqueConstraint('user_id', 'permission_id'),
1501 1493 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1502 1494 'mysql_charset': 'utf8'}
1503 1495 )
1504 1496 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1505 1497 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1506 1498 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1507 1499
1508 1500 user = relationship('User')
1509 1501 permission = relationship('Permission', lazy='joined')
1510 1502
1511 1503
1512 1504 class UserGroupRepoToPerm(Base, BaseModel):
1513 1505 __tablename__ = 'users_group_repo_to_perm'
1514 1506 __table_args__ = (
1515 1507 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1516 1508 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1517 1509 'mysql_charset': 'utf8'}
1518 1510 )
1519 1511 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1520 1512 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1521 1513 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1522 1514 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1523 1515
1524 1516 users_group = relationship('UserGroup')
1525 1517 permission = relationship('Permission')
1526 1518 repository = relationship('Repository')
1527 1519
1528 1520 @classmethod
1529 1521 def create(cls, users_group, repository, permission):
1530 1522 n = cls()
1531 1523 n.users_group = users_group
1532 1524 n.repository = repository
1533 1525 n.permission = permission
1534 1526 Session().add(n)
1535 1527 return n
1536 1528
1537 1529 def __unicode__(self):
1538 1530 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1539 1531
1540 1532
1541 1533 class UserGroupToPerm(Base, BaseModel):
1542 1534 __tablename__ = 'users_group_to_perm'
1543 1535 __table_args__ = (
1544 1536 UniqueConstraint('users_group_id', 'permission_id',),
1545 1537 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1546 1538 'mysql_charset': 'utf8'}
1547 1539 )
1548 1540 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1549 1541 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1550 1542 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1551 1543
1552 1544 users_group = relationship('UserGroup')
1553 1545 permission = relationship('Permission')
1554 1546
1555 1547
1556 1548 class UserRepoGroupToPerm(Base, BaseModel):
1557 1549 __tablename__ = 'user_repo_group_to_perm'
1558 1550 __table_args__ = (
1559 1551 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1560 1552 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1561 1553 'mysql_charset': 'utf8'}
1562 1554 )
1563 1555
1564 1556 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1565 1557 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1566 1558 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1567 1559 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1568 1560
1569 1561 user = relationship('User')
1570 1562 group = relationship('RepoGroup')
1571 1563 permission = relationship('Permission')
1572 1564
1573 1565
1574 1566 class UserGroupRepoGroupToPerm(Base, BaseModel):
1575 1567 __tablename__ = 'users_group_repo_group_to_perm'
1576 1568 __table_args__ = (
1577 1569 UniqueConstraint('users_group_id', 'group_id'),
1578 1570 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1579 1571 'mysql_charset': 'utf8'}
1580 1572 )
1581 1573
1582 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)
1583 1575 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1584 1576 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1585 1577 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1586 1578
1587 1579 users_group = relationship('UserGroup')
1588 1580 permission = relationship('Permission')
1589 1581 group = relationship('RepoGroup')
1590 1582
1591 1583
1592 1584 class Statistics(Base, BaseModel):
1593 1585 __tablename__ = 'statistics'
1594 1586 __table_args__ = (
1595 1587 UniqueConstraint('repository_id'),
1596 1588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1597 1589 'mysql_charset': 'utf8'}
1598 1590 )
1599 1591 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1600 1592 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1601 1593 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1602 1594 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1603 1595 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1604 1596 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1605 1597
1606 1598 repository = relationship('Repository', single_parent=True)
1607 1599
1608 1600
1609 1601 class UserFollowing(Base, BaseModel):
1610 1602 __tablename__ = 'user_followings'
1611 1603 __table_args__ = (
1612 1604 UniqueConstraint('user_id', 'follows_repository_id'),
1613 1605 UniqueConstraint('user_id', 'follows_user_id'),
1614 1606 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1615 1607 'mysql_charset': 'utf8'}
1616 1608 )
1617 1609
1618 1610 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1619 1611 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1620 1612 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1621 1613 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1622 1614 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1623 1615
1624 1616 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1625 1617
1626 1618 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1627 1619 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1628 1620
1629 1621 @classmethod
1630 1622 def get_repo_followers(cls, repo_id):
1631 1623 return cls.query().filter(cls.follows_repo_id == repo_id)
1632 1624
1633 1625
1634 1626 class CacheInvalidation(Base, BaseModel):
1635 1627 __tablename__ = 'cache_invalidation'
1636 1628 __table_args__ = (
1637 1629 UniqueConstraint('cache_key'),
1638 1630 Index('key_idx', 'cache_key'),
1639 1631 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1640 1632 'mysql_charset': 'utf8'},
1641 1633 )
1642 1634 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1643 1635 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1644 1636 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1645 1637 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1646 1638
1647 1639 def __init__(self, cache_key, cache_args=''):
1648 1640 self.cache_key = cache_key
1649 1641 self.cache_args = cache_args
1650 1642 self.cache_active = False
1651 1643
1652 1644 def __unicode__(self):
1653 1645 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1654 1646 self.cache_id, self.cache_key)
1655 1647
1656 1648 @property
1657 1649 def prefix(self):
1658 1650 _split = self.cache_key.split(self.cache_args, 1)
1659 1651 if _split and len(_split) == 2:
1660 1652 return _split[0]
1661 1653 return ''
1662 1654
1663 1655 @classmethod
1664 1656 def clear_cache(cls):
1665 1657 cls.query().delete()
1666 1658
1667 1659 @classmethod
1668 1660 def _get_key(cls, key):
1669 1661 """
1670 1662 Wrapper for generating a key, together with a prefix
1671 1663
1672 1664 :param key:
1673 1665 """
1674 1666 import rhodecode
1675 1667 prefix = ''
1676 1668 org_key = key
1677 1669 iid = rhodecode.CONFIG.get('instance_id')
1678 1670 if iid:
1679 1671 prefix = iid
1680 1672
1681 1673 return "%s%s" % (prefix, key), prefix, org_key
1682 1674
1683 1675 @classmethod
1684 1676 def get_by_key(cls, key):
1685 1677 return cls.query().filter(cls.cache_key == key).scalar()
1686 1678
1687 1679 @classmethod
1688 1680 def get_by_repo_name(cls, repo_name):
1689 1681 return cls.query().filter(cls.cache_args == repo_name).all()
1690 1682
1691 1683 @classmethod
1692 1684 def _get_or_create_key(cls, key, repo_name, commit=True):
1693 1685 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1694 1686 if not inv_obj:
1695 1687 try:
1696 1688 inv_obj = CacheInvalidation(key, repo_name)
1697 1689 Session().add(inv_obj)
1698 1690 if commit:
1699 1691 Session().commit()
1700 1692 except Exception:
1701 1693 log.error(traceback.format_exc())
1702 1694 Session().rollback()
1703 1695 return inv_obj
1704 1696
1705 1697 @classmethod
1706 1698 def invalidate(cls, key):
1707 1699 """
1708 1700 Returns Invalidation object if this given key should be invalidated
1709 1701 None otherwise. `cache_active = False` means that this cache
1710 1702 state is not valid and needs to be invalidated
1711 1703
1712 1704 :param key:
1713 1705 """
1714 1706 repo_name = key
1715 1707 repo_name = remove_suffix(repo_name, '_README')
1716 1708 repo_name = remove_suffix(repo_name, '_RSS')
1717 1709 repo_name = remove_suffix(repo_name, '_ATOM')
1718 1710
1719 1711 # adds instance prefix
1720 1712 key, _prefix, _org_key = cls._get_key(key)
1721 1713 inv = cls._get_or_create_key(key, repo_name)
1722 1714
1723 1715 if inv and inv.cache_active is False:
1724 1716 return inv
1725 1717
1726 1718 @classmethod
1727 1719 def set_invalidate(cls, key=None, repo_name=None):
1728 1720 """
1729 1721 Mark this Cache key for invalidation, either by key or whole
1730 1722 cache sets based on repo_name
1731 1723
1732 1724 :param key:
1733 1725 """
1734 1726 invalidated_keys = []
1735 1727 if key:
1736 1728 key, _prefix, _org_key = cls._get_key(key)
1737 1729 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1738 1730 elif repo_name:
1739 1731 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1740 1732
1741 1733 try:
1742 1734 for inv_obj in inv_objs:
1743 1735 inv_obj.cache_active = False
1744 1736 log.debug('marking %s key for invalidation based on key=%s,repo_name=%s'
1745 1737 % (inv_obj, key, repo_name))
1746 1738 invalidated_keys.append(inv_obj.cache_key)
1747 1739 Session().add(inv_obj)
1748 1740 Session().commit()
1749 1741 except Exception:
1750 1742 log.error(traceback.format_exc())
1751 1743 Session().rollback()
1752 1744 return invalidated_keys
1753 1745
1754 1746 @classmethod
1755 1747 def set_valid(cls, key):
1756 1748 """
1757 1749 Mark this cache key as active and currently cached
1758 1750
1759 1751 :param key:
1760 1752 """
1761 1753 inv_obj = cls.get_by_key(key)
1762 1754 inv_obj.cache_active = True
1763 1755 Session().add(inv_obj)
1764 1756 Session().commit()
1765 1757
1766 1758 @classmethod
1767 1759 def get_cache_map(cls):
1768 1760
1769 1761 class cachemapdict(dict):
1770 1762
1771 1763 def __init__(self, *args, **kwargs):
1772 1764 fixkey = kwargs.get('fixkey')
1773 1765 if fixkey:
1774 1766 del kwargs['fixkey']
1775 1767 self.fixkey = fixkey
1776 1768 super(cachemapdict, self).__init__(*args, **kwargs)
1777 1769
1778 1770 def __getattr__(self, name):
1779 1771 key = name
1780 1772 if self.fixkey:
1781 1773 key, _prefix, _org_key = cls._get_key(key)
1782 1774 if key in self.__dict__:
1783 1775 return self.__dict__[key]
1784 1776 else:
1785 1777 return self[key]
1786 1778
1787 1779 def __getitem__(self, key):
1788 1780 if self.fixkey:
1789 1781 key, _prefix, _org_key = cls._get_key(key)
1790 1782 try:
1791 1783 return super(cachemapdict, self).__getitem__(key)
1792 1784 except KeyError:
1793 1785 return
1794 1786
1795 1787 cache_map = cachemapdict(fixkey=True)
1796 1788 for obj in cls.query().all():
1797 1789 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1798 1790 return cache_map
1799 1791
1800 1792
1801 1793 class ChangesetComment(Base, BaseModel):
1802 1794 __tablename__ = 'changeset_comments'
1803 1795 __table_args__ = (
1804 1796 Index('cc_revision_idx', 'revision'),
1805 1797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1806 1798 'mysql_charset': 'utf8'},
1807 1799 )
1808 1800 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1809 1801 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1810 1802 revision = Column('revision', String(40), nullable=True)
1811 1803 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1812 1804 line_no = Column('line_no', Unicode(10), nullable=True)
1813 1805 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1814 1806 f_path = Column('f_path', Unicode(1000), nullable=True)
1815 1807 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1816 1808 text = Column('text', UnicodeText(25000), nullable=False)
1817 1809 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1818 1810 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1819 1811
1820 1812 author = relationship('User', lazy='joined')
1821 1813 repo = relationship('Repository')
1822 1814 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1823 1815 pull_request = relationship('PullRequest', lazy='joined')
1824 1816
1825 1817 @classmethod
1826 1818 def get_users(cls, revision=None, pull_request_id=None):
1827 1819 """
1828 1820 Returns user associated with this ChangesetComment. ie those
1829 1821 who actually commented
1830 1822
1831 1823 :param cls:
1832 1824 :param revision:
1833 1825 """
1834 1826 q = Session().query(User)\
1835 1827 .join(ChangesetComment.author)
1836 1828 if revision:
1837 1829 q = q.filter(cls.revision == revision)
1838 1830 elif pull_request_id:
1839 1831 q = q.filter(cls.pull_request_id == pull_request_id)
1840 1832 return q.all()
1841 1833
1842 1834
1843 1835 class ChangesetStatus(Base, BaseModel):
1844 1836 __tablename__ = 'changeset_statuses'
1845 1837 __table_args__ = (
1846 1838 Index('cs_revision_idx', 'revision'),
1847 1839 Index('cs_version_idx', 'version'),
1848 1840 UniqueConstraint('repo_id', 'revision', 'version'),
1849 1841 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1850 1842 'mysql_charset': 'utf8'}
1851 1843 )
1852 1844 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1853 1845 STATUS_APPROVED = 'approved'
1854 1846 STATUS_REJECTED = 'rejected'
1855 1847 STATUS_UNDER_REVIEW = 'under_review'
1856 1848
1857 1849 STATUSES = [
1858 1850 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1859 1851 (STATUS_APPROVED, _("Approved")),
1860 1852 (STATUS_REJECTED, _("Rejected")),
1861 1853 (STATUS_UNDER_REVIEW, _("Under Review")),
1862 1854 ]
1863 1855
1864 1856 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1865 1857 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1866 1858 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1867 1859 revision = Column('revision', String(40), nullable=False)
1868 1860 status = Column('status', String(128), nullable=False, default=DEFAULT)
1869 1861 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1870 1862 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1871 1863 version = Column('version', Integer(), nullable=False, default=0)
1872 1864 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1873 1865
1874 1866 author = relationship('User', lazy='joined')
1875 1867 repo = relationship('Repository')
1876 1868 comment = relationship('ChangesetComment', lazy='joined')
1877 1869 pull_request = relationship('PullRequest', lazy='joined')
1878 1870
1879 1871 def __unicode__(self):
1880 1872 return u"<%s('%s:%s')>" % (
1881 1873 self.__class__.__name__,
1882 1874 self.status, self.author
1883 1875 )
1884 1876
1885 1877 @classmethod
1886 1878 def get_status_lbl(cls, value):
1887 1879 return dict(cls.STATUSES).get(value)
1888 1880
1889 1881 @property
1890 1882 def status_lbl(self):
1891 1883 return ChangesetStatus.get_status_lbl(self.status)
1892 1884
1893 1885
1894 1886 class PullRequest(Base, BaseModel):
1895 1887 __tablename__ = 'pull_requests'
1896 1888 __table_args__ = (
1897 1889 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1898 1890 'mysql_charset': 'utf8'},
1899 1891 )
1900 1892
1901 1893 STATUS_NEW = u'new'
1902 1894 STATUS_OPEN = u'open'
1903 1895 STATUS_CLOSED = u'closed'
1904 1896
1905 1897 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1906 1898 title = Column('title', Unicode(256), nullable=True)
1907 1899 description = Column('description', UnicodeText(10240), nullable=True)
1908 1900 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1909 1901 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1910 1902 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1911 1903 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1912 1904 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1913 1905 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1914 1906 org_ref = Column('org_ref', Unicode(256), nullable=False)
1915 1907 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1916 1908 other_ref = Column('other_ref', Unicode(256), nullable=False)
1917 1909
1918 1910 @hybrid_property
1919 1911 def revisions(self):
1920 1912 return self._revisions.split(':')
1921 1913
1922 1914 @revisions.setter
1923 1915 def revisions(self, val):
1924 1916 self._revisions = ':'.join(val)
1925 1917
1926 1918 @property
1927 1919 def org_ref_parts(self):
1928 1920 return self.org_ref.split(':')
1929 1921
1930 1922 @property
1931 1923 def other_ref_parts(self):
1932 1924 return self.other_ref.split(':')
1933 1925
1934 1926 author = relationship('User', lazy='joined')
1935 1927 reviewers = relationship('PullRequestReviewers',
1936 1928 cascade="all, delete, delete-orphan")
1937 1929 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1938 1930 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1939 1931 statuses = relationship('ChangesetStatus')
1940 1932 comments = relationship('ChangesetComment',
1941 1933 cascade="all, delete, delete-orphan")
1942 1934
1943 1935 def is_closed(self):
1944 1936 return self.status == self.STATUS_CLOSED
1945 1937
1946 1938 @property
1947 1939 def last_review_status(self):
1948 1940 return self.statuses[-1].status if self.statuses else ''
1949 1941
1950 1942 def __json__(self):
1951 1943 return dict(
1952 1944 revisions=self.revisions
1953 1945 )
1954 1946
1955 1947
1956 1948 class PullRequestReviewers(Base, BaseModel):
1957 1949 __tablename__ = 'pull_request_reviewers'
1958 1950 __table_args__ = (
1959 1951 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1960 1952 'mysql_charset': 'utf8'},
1961 1953 )
1962 1954
1963 1955 def __init__(self, user=None, pull_request=None):
1964 1956 self.user = user
1965 1957 self.pull_request = pull_request
1966 1958
1967 1959 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1968 1960 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1969 1961 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1970 1962
1971 1963 user = relationship('User')
1972 1964 pull_request = relationship('PullRequest')
1973 1965
1974 1966
1975 1967 class Notification(Base, BaseModel):
1976 1968 __tablename__ = 'notifications'
1977 1969 __table_args__ = (
1978 1970 Index('notification_type_idx', 'type'),
1979 1971 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1980 1972 'mysql_charset': 'utf8'},
1981 1973 )
1982 1974
1983 1975 TYPE_CHANGESET_COMMENT = u'cs_comment'
1984 1976 TYPE_MESSAGE = u'message'
1985 1977 TYPE_MENTION = u'mention'
1986 1978 TYPE_REGISTRATION = u'registration'
1987 1979 TYPE_PULL_REQUEST = u'pull_request'
1988 1980 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1989 1981
1990 1982 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1991 1983 subject = Column('subject', Unicode(512), nullable=True)
1992 1984 body = Column('body', UnicodeText(50000), nullable=True)
1993 1985 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1994 1986 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1995 1987 type_ = Column('type', Unicode(256))
1996 1988
1997 1989 created_by_user = relationship('User')
1998 1990 notifications_to_users = relationship('UserNotification', lazy='joined',
1999 1991 cascade="all, delete, delete-orphan")
2000 1992
2001 1993 @property
2002 1994 def recipients(self):
2003 1995 return [x.user for x in UserNotification.query()\
2004 1996 .filter(UserNotification.notification == self)\
2005 1997 .order_by(UserNotification.user_id.asc()).all()]
2006 1998
2007 1999 @classmethod
2008 2000 def create(cls, created_by, subject, body, recipients, type_=None):
2009 2001 if type_ is None:
2010 2002 type_ = Notification.TYPE_MESSAGE
2011 2003
2012 2004 notification = cls()
2013 2005 notification.created_by_user = created_by
2014 2006 notification.subject = subject
2015 2007 notification.body = body
2016 2008 notification.type_ = type_
2017 2009 notification.created_on = datetime.datetime.now()
2018 2010
2019 2011 for u in recipients:
2020 2012 assoc = UserNotification()
2021 2013 assoc.notification = notification
2022 2014 u.notifications.append(assoc)
2023 2015 Session().add(notification)
2024 2016 return notification
2025 2017
2026 2018 @property
2027 2019 def description(self):
2028 2020 from rhodecode.model.notification import NotificationModel
2029 2021 return NotificationModel().make_description(self)
2030 2022
2031 2023
2032 2024 class UserNotification(Base, BaseModel):
2033 2025 __tablename__ = 'user_to_notification'
2034 2026 __table_args__ = (
2035 2027 UniqueConstraint('user_id', 'notification_id'),
2036 2028 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2037 2029 'mysql_charset': 'utf8'}
2038 2030 )
2039 2031 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2040 2032 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2041 2033 read = Column('read', Boolean, default=False)
2042 2034 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2043 2035
2044 2036 user = relationship('User', lazy="joined")
2045 2037 notification = relationship('Notification', lazy="joined",
2046 2038 order_by=lambda: Notification.created_on.desc(),)
2047 2039
2048 2040 def mark_as_read(self):
2049 2041 self.read = True
2050 2042 Session().add(self)
2051 2043
2052 2044
2053 2045 class DbMigrateVersion(Base, BaseModel):
2054 2046 __tablename__ = 'db_migrate_version'
2055 2047 __table_args__ = (
2056 2048 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2057 2049 'mysql_charset': 'utf8'},
2058 2050 )
2059 2051 repository_id = Column('repository_id', String(250), primary_key=True)
2060 2052 repository_path = Column('repository_path', Text)
2061 2053 version = Column('version', Integer)
General Comments 0
You need to be logged in to leave comments. Login now