##// END OF EJS Templates
api: Add an entry for pr shadow repositories to api functions.
Martin Bornhold -
r893:b7927ff7 default
parent child Browse files
Show More
@@ -1,660 +1,666 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from rhodecode.api import jsonrpc_method, JSONRPCError
25 25 from rhodecode.api.utils import (
26 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 28 has_repo_permissions, resolve_ref_or_error)
29 29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 30 from rhodecode.lib.base import vcs_operation_context
31 31 from rhodecode.lib.utils2 import str2bool
32 32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 33 from rhodecode.model.comment import ChangesetCommentsModel
34 34 from rhodecode.model.db import Session, ChangesetStatus
35 35 from rhodecode.model.pull_request import PullRequestModel
36 36 from rhodecode.model.settings import SettingsModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 @jsonrpc_method()
42 42 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 43 """
44 44 Get a pull request based on the given ID.
45 45
46 46 :param apiuser: This is filled automatically from the |authtoken|.
47 47 :type apiuser: AuthUser
48 48 :param repoid: Repository name or repository ID from where the pull
49 49 request was opened.
50 50 :type repoid: str or int
51 51 :param pullrequestid: ID of the requested pull request.
52 52 :type pullrequestid: int
53 53
54 54 Example output:
55 55
56 56 .. code-block:: bash
57 57
58 58 "id": <id_given_in_input>,
59 59 "result":
60 60 {
61 61 "pull_request_id": "<pull_request_id>",
62 62 "url": "<url>",
63 63 "title": "<title>",
64 64 "description": "<description>",
65 65 "status" : "<status>",
66 66 "created_on": "<date_time_created>",
67 67 "updated_on": "<date_time_updated>",
68 68 "commit_ids": [
69 69 ...
70 70 "<commit_id>",
71 71 "<commit_id>",
72 72 ...
73 73 ],
74 74 "review_status": "<review_status>",
75 75 "mergeable": {
76 76 "status": "<bool>",
77 77 "message": "<message>",
78 78 },
79 79 "source": {
80 80 "clone_url": "<clone_url>",
81 81 "repository": "<repository_name>",
82 82 "reference":
83 83 {
84 84 "name": "<name>",
85 85 "type": "<type>",
86 86 "commit_id": "<commit_id>",
87 87 }
88 88 },
89 89 "target": {
90 90 "clone_url": "<clone_url>",
91 91 "repository": "<repository_name>",
92 92 "reference":
93 93 {
94 94 "name": "<name>",
95 95 "type": "<type>",
96 96 "commit_id": "<commit_id>",
97 97 }
98 98 },
99 "shadow": {
100 "clone_url": "<clone_url>",
101 },
99 102 "author": <user_obj>,
100 103 "reviewers": [
101 104 ...
102 105 {
103 106 "user": "<user_obj>",
104 107 "review_status": "<review_status>",
105 108 }
106 109 ...
107 110 ]
108 111 },
109 112 "error": null
110 113 """
111 114 get_repo_or_error(repoid)
112 115 pull_request = get_pull_request_or_error(pullrequestid)
113 116 if not PullRequestModel().check_user_read(
114 117 pull_request, apiuser, api=True):
115 118 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
116 119 data = pull_request.get_api_data()
117 120 return data
118 121
119 122
120 123 @jsonrpc_method()
121 124 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
122 125 """
123 126 Get all pull requests from the repository specified in `repoid`.
124 127
125 128 :param apiuser: This is filled automatically from the |authtoken|.
126 129 :type apiuser: AuthUser
127 130 :param repoid: Repository name or repository ID.
128 131 :type repoid: str or int
129 132 :param status: Only return pull requests with the specified status.
130 133 Valid options are.
131 134 * ``new`` (default)
132 135 * ``open``
133 136 * ``closed``
134 137 :type status: str
135 138
136 139 Example output:
137 140
138 141 .. code-block:: bash
139 142
140 143 "id": <id_given_in_input>,
141 144 "result":
142 145 [
143 146 ...
144 147 {
145 148 "pull_request_id": "<pull_request_id>",
146 149 "url": "<url>",
147 150 "title" : "<title>",
148 151 "description": "<description>",
149 152 "status": "<status>",
150 153 "created_on": "<date_time_created>",
151 154 "updated_on": "<date_time_updated>",
152 155 "commit_ids": [
153 156 ...
154 157 "<commit_id>",
155 158 "<commit_id>",
156 159 ...
157 160 ],
158 161 "review_status": "<review_status>",
159 162 "mergeable": {
160 163 "status": "<bool>",
161 164 "message: "<message>",
162 165 },
163 166 "source": {
164 167 "clone_url": "<clone_url>",
165 168 "reference":
166 169 {
167 170 "name": "<name>",
168 171 "type": "<type>",
169 172 "commit_id": "<commit_id>",
170 173 }
171 174 },
172 175 "target": {
173 176 "clone_url": "<clone_url>",
174 177 "reference":
175 178 {
176 179 "name": "<name>",
177 180 "type": "<type>",
178 181 "commit_id": "<commit_id>",
179 182 }
180 183 },
184 "shadow": {
185 "clone_url": "<clone_url>",
186 },
181 187 "author": <user_obj>,
182 188 "reviewers": [
183 189 ...
184 190 {
185 191 "user": "<user_obj>",
186 192 "review_status": "<review_status>",
187 193 }
188 194 ...
189 195 ]
190 196 }
191 197 ...
192 198 ],
193 199 "error": null
194 200
195 201 """
196 202 repo = get_repo_or_error(repoid)
197 203 if not has_superadmin_permission(apiuser):
198 204 _perms = (
199 205 'repository.admin', 'repository.write', 'repository.read',)
200 206 has_repo_permissions(apiuser, repoid, repo, _perms)
201 207
202 208 status = Optional.extract(status)
203 209 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
204 210 data = [pr.get_api_data() for pr in pull_requests]
205 211 return data
206 212
207 213
208 214 @jsonrpc_method()
209 215 def merge_pull_request(request, apiuser, repoid, pullrequestid,
210 216 userid=Optional(OAttr('apiuser'))):
211 217 """
212 218 Merge the pull request specified by `pullrequestid` into its target
213 219 repository.
214 220
215 221 :param apiuser: This is filled automatically from the |authtoken|.
216 222 :type apiuser: AuthUser
217 223 :param repoid: The Repository name or repository ID of the
218 224 target repository to which the |pr| is to be merged.
219 225 :type repoid: str or int
220 226 :param pullrequestid: ID of the pull request which shall be merged.
221 227 :type pullrequestid: int
222 228 :param userid: Merge the pull request as this user.
223 229 :type userid: Optional(str or int)
224 230
225 231 Example output:
226 232
227 233 .. code-block:: bash
228 234
229 235 "id": <id_given_in_input>,
230 236 "result":
231 237 {
232 238 "executed": "<bool>",
233 239 "failure_reason": "<int>",
234 240 "merge_commit_id": "<merge_commit_id>",
235 241 "possible": "<bool>"
236 242 },
237 243 "error": null
238 244
239 245 """
240 246 repo = get_repo_or_error(repoid)
241 247 if not isinstance(userid, Optional):
242 248 if (has_superadmin_permission(apiuser) or
243 249 HasRepoPermissionAnyApi('repository.admin')(
244 250 user=apiuser, repo_name=repo.repo_name)):
245 251 apiuser = get_user_or_error(userid)
246 252 else:
247 253 raise JSONRPCError('userid is not the same as your user')
248 254
249 255 pull_request = get_pull_request_or_error(pullrequestid)
250 256 if not PullRequestModel().check_user_merge(
251 257 pull_request, apiuser, api=True):
252 258 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
253 259 if pull_request.is_closed():
254 260 raise JSONRPCError(
255 261 'pull request `%s` merge failed, pull request is closed' % (
256 262 pullrequestid,))
257 263
258 264 target_repo = pull_request.target_repo
259 265 extras = vcs_operation_context(
260 266 request.environ, repo_name=target_repo.repo_name,
261 267 username=apiuser.username, action='push',
262 268 scm=target_repo.repo_type)
263 269 data = PullRequestModel().merge(pull_request, apiuser, extras=extras)
264 270 if data.executed:
265 271 PullRequestModel().close_pull_request(
266 272 pull_request.pull_request_id, apiuser)
267 273
268 274 Session().commit()
269 275 return data
270 276
271 277
272 278 @jsonrpc_method()
273 279 def close_pull_request(request, apiuser, repoid, pullrequestid,
274 280 userid=Optional(OAttr('apiuser'))):
275 281 """
276 282 Close the pull request specified by `pullrequestid`.
277 283
278 284 :param apiuser: This is filled automatically from the |authtoken|.
279 285 :type apiuser: AuthUser
280 286 :param repoid: Repository name or repository ID to which the pull
281 287 request belongs.
282 288 :type repoid: str or int
283 289 :param pullrequestid: ID of the pull request to be closed.
284 290 :type pullrequestid: int
285 291 :param userid: Close the pull request as this user.
286 292 :type userid: Optional(str or int)
287 293
288 294 Example output:
289 295
290 296 .. code-block:: bash
291 297
292 298 "id": <id_given_in_input>,
293 299 "result":
294 300 {
295 301 "pull_request_id": "<int>",
296 302 "closed": "<bool>"
297 303 },
298 304 "error": null
299 305
300 306 """
301 307 repo = get_repo_or_error(repoid)
302 308 if not isinstance(userid, Optional):
303 309 if (has_superadmin_permission(apiuser) or
304 310 HasRepoPermissionAnyApi('repository.admin')(
305 311 user=apiuser, repo_name=repo.repo_name)):
306 312 apiuser = get_user_or_error(userid)
307 313 else:
308 314 raise JSONRPCError('userid is not the same as your user')
309 315
310 316 pull_request = get_pull_request_or_error(pullrequestid)
311 317 if not PullRequestModel().check_user_update(
312 318 pull_request, apiuser, api=True):
313 319 raise JSONRPCError(
314 320 'pull request `%s` close failed, no permission to close.' % (
315 321 pullrequestid,))
316 322 if pull_request.is_closed():
317 323 raise JSONRPCError(
318 324 'pull request `%s` is already closed' % (pullrequestid,))
319 325
320 326 PullRequestModel().close_pull_request(
321 327 pull_request.pull_request_id, apiuser)
322 328 Session().commit()
323 329 data = {
324 330 'pull_request_id': pull_request.pull_request_id,
325 331 'closed': True,
326 332 }
327 333 return data
328 334
329 335
330 336 @jsonrpc_method()
331 337 def comment_pull_request(request, apiuser, repoid, pullrequestid,
332 338 message=Optional(None), status=Optional(None),
333 339 userid=Optional(OAttr('apiuser'))):
334 340 """
335 341 Comment on the pull request specified with the `pullrequestid`,
336 342 in the |repo| specified by the `repoid`, and optionally change the
337 343 review status.
338 344
339 345 :param apiuser: This is filled automatically from the |authtoken|.
340 346 :type apiuser: AuthUser
341 347 :param repoid: The repository name or repository ID.
342 348 :type repoid: str or int
343 349 :param pullrequestid: The pull request ID.
344 350 :type pullrequestid: int
345 351 :param message: The text content of the comment.
346 352 :type message: str
347 353 :param status: (**Optional**) Set the approval status of the pull
348 354 request. Valid options are:
349 355 * not_reviewed
350 356 * approved
351 357 * rejected
352 358 * under_review
353 359 :type status: str
354 360 :param userid: Comment on the pull request as this user
355 361 :type userid: Optional(str or int)
356 362
357 363 Example output:
358 364
359 365 .. code-block:: bash
360 366
361 367 id : <id_given_in_input>
362 368 result :
363 369 {
364 370 "pull_request_id": "<Integer>",
365 371 "comment_id": "<Integer>"
366 372 }
367 373 error : null
368 374 """
369 375 repo = get_repo_or_error(repoid)
370 376 if not isinstance(userid, Optional):
371 377 if (has_superadmin_permission(apiuser) or
372 378 HasRepoPermissionAnyApi('repository.admin')(
373 379 user=apiuser, repo_name=repo.repo_name)):
374 380 apiuser = get_user_or_error(userid)
375 381 else:
376 382 raise JSONRPCError('userid is not the same as your user')
377 383
378 384 pull_request = get_pull_request_or_error(pullrequestid)
379 385 if not PullRequestModel().check_user_read(
380 386 pull_request, apiuser, api=True):
381 387 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
382 388 message = Optional.extract(message)
383 389 status = Optional.extract(status)
384 390 if not message and not status:
385 391 raise JSONRPCError('message and status parameter missing')
386 392
387 393 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
388 394 status is not None):
389 395 raise JSONRPCError('unknown comment status`%s`' % status)
390 396
391 397 allowed_to_change_status = PullRequestModel().check_user_change_status(
392 398 pull_request, apiuser)
393 399 text = message
394 400 if status and allowed_to_change_status:
395 401 st_message = (('Status change %(transition_icon)s %(status)s')
396 402 % {'transition_icon': '>',
397 403 'status': ChangesetStatus.get_status_lbl(status)})
398 404 text = message or st_message
399 405
400 406 rc_config = SettingsModel().get_all_settings()
401 407 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
402 408 comment = ChangesetCommentsModel().create(
403 409 text=text,
404 410 repo=pull_request.target_repo.repo_id,
405 411 user=apiuser.user_id,
406 412 pull_request=pull_request.pull_request_id,
407 413 f_path=None,
408 414 line_no=None,
409 415 status_change=(ChangesetStatus.get_status_lbl(status)
410 416 if status and allowed_to_change_status else None),
411 417 status_change_type=(status
412 418 if status and allowed_to_change_status else None),
413 419 closing_pr=False,
414 420 renderer=renderer
415 421 )
416 422
417 423 if allowed_to_change_status and status:
418 424 ChangesetStatusModel().set_status(
419 425 pull_request.target_repo.repo_id,
420 426 status,
421 427 apiuser.user_id,
422 428 comment,
423 429 pull_request=pull_request.pull_request_id
424 430 )
425 431 Session().flush()
426 432
427 433 Session().commit()
428 434 data = {
429 435 'pull_request_id': pull_request.pull_request_id,
430 436 'comment_id': comment.comment_id,
431 437 'status': status
432 438 }
433 439 return data
434 440
435 441
436 442 @jsonrpc_method()
437 443 def create_pull_request(
438 444 request, apiuser, source_repo, target_repo, source_ref, target_ref,
439 445 title, description=Optional(''), reviewers=Optional(None)):
440 446 """
441 447 Creates a new pull request.
442 448
443 449 Accepts refs in the following formats:
444 450
445 451 * branch:<branch_name>:<sha>
446 452 * branch:<branch_name>
447 453 * bookmark:<bookmark_name>:<sha> (Mercurial only)
448 454 * bookmark:<bookmark_name> (Mercurial only)
449 455
450 456 :param apiuser: This is filled automatically from the |authtoken|.
451 457 :type apiuser: AuthUser
452 458 :param source_repo: Set the source repository name.
453 459 :type source_repo: str
454 460 :param target_repo: Set the target repository name.
455 461 :type target_repo: str
456 462 :param source_ref: Set the source ref name.
457 463 :type source_ref: str
458 464 :param target_ref: Set the target ref name.
459 465 :type target_ref: str
460 466 :param title: Set the pull request title.
461 467 :type title: str
462 468 :param description: Set the pull request description.
463 469 :type description: Optional(str)
464 470 :param reviewers: Set the new pull request reviewers list.
465 471 :type reviewers: Optional(list)
466 472 Accepts username strings or objects of the format:
467 473 {
468 474 'username': 'nick', 'reasons': ['original author']
469 475 }
470 476 """
471 477
472 478 source = get_repo_or_error(source_repo)
473 479 target = get_repo_or_error(target_repo)
474 480 if not has_superadmin_permission(apiuser):
475 481 _perms = ('repository.admin', 'repository.write', 'repository.read',)
476 482 has_repo_permissions(apiuser, source_repo, source, _perms)
477 483
478 484 full_source_ref = resolve_ref_or_error(source_ref, source)
479 485 full_target_ref = resolve_ref_or_error(target_ref, target)
480 486 source_commit = get_commit_or_error(full_source_ref, source)
481 487 target_commit = get_commit_or_error(full_target_ref, target)
482 488 source_scm = source.scm_instance()
483 489 target_scm = target.scm_instance()
484 490
485 491 commit_ranges = target_scm.compare(
486 492 target_commit.raw_id, source_commit.raw_id, source_scm,
487 493 merge=True, pre_load=[])
488 494
489 495 ancestor = target_scm.get_common_ancestor(
490 496 target_commit.raw_id, source_commit.raw_id, source_scm)
491 497
492 498 if not commit_ranges:
493 499 raise JSONRPCError('no commits found')
494 500
495 501 if not ancestor:
496 502 raise JSONRPCError('no common ancestor found')
497 503
498 504 reviewer_objects = Optional.extract(reviewers) or []
499 505 if not isinstance(reviewer_objects, list):
500 506 raise JSONRPCError('reviewers should be specified as a list')
501 507
502 508 reviewers_reasons = []
503 509 for reviewer_object in reviewer_objects:
504 510 reviewer_reasons = []
505 511 if isinstance(reviewer_object, (basestring, int)):
506 512 reviewer_username = reviewer_object
507 513 else:
508 514 reviewer_username = reviewer_object['username']
509 515 reviewer_reasons = reviewer_object.get('reasons', [])
510 516
511 517 user = get_user_or_error(reviewer_username)
512 518 reviewers_reasons.append((user.user_id, reviewer_reasons))
513 519
514 520 pull_request_model = PullRequestModel()
515 521 pull_request = pull_request_model.create(
516 522 created_by=apiuser.user_id,
517 523 source_repo=source_repo,
518 524 source_ref=full_source_ref,
519 525 target_repo=target_repo,
520 526 target_ref=full_target_ref,
521 527 revisions=reversed(
522 528 [commit.raw_id for commit in reversed(commit_ranges)]),
523 529 reviewers=reviewers_reasons,
524 530 title=title,
525 531 description=Optional.extract(description)
526 532 )
527 533
528 534 Session().commit()
529 535 data = {
530 536 'msg': 'Created new pull request `{}`'.format(title),
531 537 'pull_request_id': pull_request.pull_request_id,
532 538 }
533 539 return data
534 540
535 541
536 542 @jsonrpc_method()
537 543 def update_pull_request(
538 544 request, apiuser, repoid, pullrequestid, title=Optional(''),
539 545 description=Optional(''), reviewers=Optional(None),
540 546 update_commits=Optional(None), close_pull_request=Optional(None)):
541 547 """
542 548 Updates a pull request.
543 549
544 550 :param apiuser: This is filled automatically from the |authtoken|.
545 551 :type apiuser: AuthUser
546 552 :param repoid: The repository name or repository ID.
547 553 :type repoid: str or int
548 554 :param pullrequestid: The pull request ID.
549 555 :type pullrequestid: int
550 556 :param title: Set the pull request title.
551 557 :type title: str
552 558 :param description: Update pull request description.
553 559 :type description: Optional(str)
554 560 :param reviewers: Update pull request reviewers list with new value.
555 561 :type reviewers: Optional(list)
556 562 :param update_commits: Trigger update of commits for this pull request
557 563 :type: update_commits: Optional(bool)
558 564 :param close_pull_request: Close this pull request with rejected state
559 565 :type: close_pull_request: Optional(bool)
560 566
561 567 Example output:
562 568
563 569 .. code-block:: bash
564 570
565 571 id : <id_given_in_input>
566 572 result :
567 573 {
568 574 "msg": "Updated pull request `63`",
569 575 "pull_request": <pull_request_object>,
570 576 "updated_reviewers": {
571 577 "added": [
572 578 "username"
573 579 ],
574 580 "removed": []
575 581 },
576 582 "updated_commits": {
577 583 "added": [
578 584 "<sha1_hash>"
579 585 ],
580 586 "common": [
581 587 "<sha1_hash>",
582 588 "<sha1_hash>",
583 589 ],
584 590 "removed": []
585 591 }
586 592 }
587 593 error : null
588 594 """
589 595
590 596 repo = get_repo_or_error(repoid)
591 597 pull_request = get_pull_request_or_error(pullrequestid)
592 598 if not PullRequestModel().check_user_update(
593 599 pull_request, apiuser, api=True):
594 600 raise JSONRPCError(
595 601 'pull request `%s` update failed, no permission to update.' % (
596 602 pullrequestid,))
597 603 if pull_request.is_closed():
598 604 raise JSONRPCError(
599 605 'pull request `%s` update failed, pull request is closed' % (
600 606 pullrequestid,))
601 607
602 608 reviewer_objects = Optional.extract(reviewers) or []
603 609 if not isinstance(reviewer_objects, list):
604 610 raise JSONRPCError('reviewers should be specified as a list')
605 611
606 612 reviewers_reasons = []
607 613 reviewer_ids = set()
608 614 for reviewer_object in reviewer_objects:
609 615 reviewer_reasons = []
610 616 if isinstance(reviewer_object, (int, basestring)):
611 617 reviewer_username = reviewer_object
612 618 else:
613 619 reviewer_username = reviewer_object['username']
614 620 reviewer_reasons = reviewer_object.get('reasons', [])
615 621
616 622 user = get_user_or_error(reviewer_username)
617 623 reviewer_ids.add(user.user_id)
618 624 reviewers_reasons.append((user.user_id, reviewer_reasons))
619 625
620 626 title = Optional.extract(title)
621 627 description = Optional.extract(description)
622 628 if title or description:
623 629 PullRequestModel().edit(
624 630 pull_request, title or pull_request.title,
625 631 description or pull_request.description)
626 632 Session().commit()
627 633
628 634 commit_changes = {"added": [], "common": [], "removed": []}
629 635 if str2bool(Optional.extract(update_commits)):
630 636 if PullRequestModel().has_valid_update_type(pull_request):
631 637 _version, _commit_changes = PullRequestModel().update_commits(
632 638 pull_request)
633 639 commit_changes = _commit_changes or commit_changes
634 640 Session().commit()
635 641
636 642 reviewers_changes = {"added": [], "removed": []}
637 643 if reviewer_ids:
638 644 added_reviewers, removed_reviewers = \
639 645 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
640 646
641 647 reviewers_changes['added'] = sorted(
642 648 [get_user_or_error(n).username for n in added_reviewers])
643 649 reviewers_changes['removed'] = sorted(
644 650 [get_user_or_error(n).username for n in removed_reviewers])
645 651 Session().commit()
646 652
647 653 if str2bool(Optional.extract(close_pull_request)):
648 654 PullRequestModel().close_pull_request_with_comment(
649 655 pull_request, apiuser, repo)
650 656 Session().commit()
651 657
652 658 data = {
653 659 'msg': 'Updated pull request `{}`'.format(
654 660 pull_request.pull_request_id),
655 661 'pull_request': pull_request.get_api_data(),
656 662 'updated_commits': commit_changes,
657 663 'updated_reviewers': reviewers_changes
658 664 }
659 665 return data
660 666
@@ -1,509 +1,512 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import logging
28 28 import importlib
29 29 import re
30 30 from functools import wraps
31 31
32 32 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 33 from webob.exc import (
34 34 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
35 35
36 36 import rhodecode
37 37 from rhodecode.authentication.base import authenticate, VCS_TYPE
38 38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
39 39 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
40 40 from rhodecode.lib.exceptions import (
41 41 HTTPLockedRC, HTTPRequirementError, UserCreationError,
42 42 NotAllowedToCreateUserError)
43 43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 44 from rhodecode.lib.middleware import appenlight
45 45 from rhodecode.lib.middleware.utils import scm_app
46 46 from rhodecode.lib.utils import (
47 47 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
48 48 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
49 49 from rhodecode.lib.vcs.conf import settings as vcs_settings
50 50 from rhodecode.lib.vcs.backends import base
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.db import User, Repository
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def initialize_generator(factory):
60 60 """
61 61 Initializes the returned generator by draining its first element.
62 62
63 63 This can be used to give a generator an initializer, which is the code
64 64 up to the first yield statement. This decorator enforces that the first
65 65 produced element has the value ``"__init__"`` to make its special
66 66 purpose very explicit in the using code.
67 67 """
68 68
69 69 @wraps(factory)
70 70 def wrapper(*args, **kwargs):
71 71 gen = factory(*args, **kwargs)
72 72 try:
73 73 init = gen.next()
74 74 except StopIteration:
75 75 raise ValueError('Generator must yield at least one element.')
76 76 if init != "__init__":
77 77 raise ValueError('First yielded element must be "__init__".')
78 78 return gen
79 79 return wrapper
80 80
81 81
82 82 class SimpleVCS(object):
83 83 """Common functionality for SCM HTTP handlers."""
84 84
85 85 SCM = 'unknown'
86 86
87 87 acl_repo_name = None
88 88 url_repo_name = None
89 89 vcs_repo_name = None
90 90
91 91 def __init__(self, application, config, registry):
92 92 self.registry = registry
93 93 self.application = application
94 94 self.config = config
95 95 # re-populated by specialized middleware
96 96 self.repo_vcs_config = base.Config()
97 97
98 98 # base path of repo locations
99 99 self.basepath = get_rhodecode_base_path()
100 100 # authenticate this VCS request using authfunc
101 101 auth_ret_code_detection = \
102 102 str2bool(self.config.get('auth_ret_code_detection', False))
103 103 self.authenticate = BasicAuth(
104 104 '', authenticate, registry, config.get('auth_ret_code'),
105 105 auth_ret_code_detection)
106 106 self.ip_addr = '0.0.0.0'
107 107
108 108 def set_repo_names(self, environ):
109 109 """
110 110 This will populate the attributes acl_repo_name, url_repo_name,
111 111 vcs_repo_name and pr_id on the current instance.
112 112 """
113 # TODO: martinb: Unify generation/suffix of clone url. It is currently
114 # used here in the regex, in PullRequest in get_api_data() and
115 # indirectly in routing configuration.
113 116 # TODO: martinb: Move to class or module scope.
114 117 # TODO: martinb: Check if we have to use re.UNICODE.
115 118 # TODO: martinb: Check which chars are allowed for repo/group names.
116 119 # These chars are excluded: '`?=[]\;\'"<>,/~!@#$%^&*()+{}|: '
117 120 # Code from: rhodecode/lib/utils.py:repo_name_slug()
118 121 pr_regex = re.compile(
119 122 '(?P<base_name>(?:[\w-]+)(?:/[\w-]+)*)/' # repo groups
120 123 '(?P<repo_name>[\w-]+)' # target repo name
121 124 '/pull-request/(?P<pr_id>\d+)/repository') # pr suffix
122 125
123 126 # Get url repo name from environment.
124 127 self.url_repo_name = self._get_repository_name(environ)
125 128
126 129 # Check if this is a request to a shadow repository. In case of a
127 130 # shadow repo set vcs_repo_name to the file system path pointing to the
128 131 # shadow repo. And set acl_repo_name to the pull request target repo
129 132 # because we use the target repo for permission checks. Otherwise all
130 133 # names are equal.
131 134 match = pr_regex.match(self.url_repo_name)
132 135 if match:
133 136 # Get pull request instance.
134 137 match_dict = match.groupdict()
135 138 pr_id = match_dict['pr_id']
136 139 pull_request = PullRequest.get(pr_id)
137 140
138 141 # Get file system path to shadow repository.
139 142 workspace_id = PullRequestModel()._workspace_id(pull_request)
140 143 target_vcs = pull_request.target_repo.scm_instance()
141 144 vcs_repo_name = target_vcs._get_shadow_repository_path(
142 145 workspace_id)
143 146
144 147 # Store names for later usage.
145 148 self.pr_id = pr_id
146 149 self.vcs_repo_name = vcs_repo_name
147 150 self.acl_repo_name = pull_request.target_repo.repo_name
148 151 else:
149 152 # All names are equal for normal (non shadow) repositories.
150 153 self.acl_repo_name = self.url_repo_name
151 154 self.vcs_repo_name = self.url_repo_name
152 155 self.pr_id = None
153 156
154 157 @property
155 158 def repo_name(self):
156 159 # TODO: johbo: Remove, switch to correct repo name attribute
157 160 return self.acl_repo_name
158 161
159 162 @property
160 163 def scm_app(self):
161 164 custom_implementation = self.config.get('vcs.scm_app_implementation')
162 165 if custom_implementation and custom_implementation != 'pyro4':
163 166 log.info(
164 167 "Using custom implementation of scm_app: %s",
165 168 custom_implementation)
166 169 scm_app_impl = importlib.import_module(custom_implementation)
167 170 else:
168 171 scm_app_impl = scm_app
169 172 return scm_app_impl
170 173
171 174 def _get_by_id(self, repo_name):
172 175 """
173 176 Gets a special pattern _<ID> from clone url and tries to replace it
174 177 with a repository_name for support of _<ID> non changeable urls
175 178 """
176 179
177 180 data = repo_name.split('/')
178 181 if len(data) >= 2:
179 182 from rhodecode.model.repo import RepoModel
180 183 by_id_match = RepoModel().get_repo_by_id(repo_name)
181 184 if by_id_match:
182 185 data[1] = by_id_match.repo_name
183 186
184 187 return safe_str('/'.join(data))
185 188
186 189 def _invalidate_cache(self, repo_name):
187 190 """
188 191 Set's cache for this repository for invalidation on next access
189 192
190 193 :param repo_name: full repo name, also a cache key
191 194 """
192 195 ScmModel().mark_for_invalidation(repo_name)
193 196
194 197 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
195 198 db_repo = Repository.get_by_repo_name(repo_name)
196 199 if not db_repo:
197 200 log.debug('Repository `%s` not found inside the database.',
198 201 repo_name)
199 202 return False
200 203
201 204 if db_repo.repo_type != scm_type:
202 205 log.warning(
203 206 'Repository `%s` have incorrect scm_type, expected %s got %s',
204 207 repo_name, db_repo.repo_type, scm_type)
205 208 return False
206 209
207 210 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
208 211
209 212 def valid_and_active_user(self, user):
210 213 """
211 214 Checks if that user is not empty, and if it's actually object it checks
212 215 if he's active.
213 216
214 217 :param user: user object or None
215 218 :return: boolean
216 219 """
217 220 if user is None:
218 221 return False
219 222
220 223 elif user.active:
221 224 return True
222 225
223 226 return False
224 227
225 228 def _check_permission(self, action, user, repo_name, ip_addr=None):
226 229 """
227 230 Checks permissions using action (push/pull) user and repository
228 231 name
229 232
230 233 :param action: push or pull action
231 234 :param user: user instance
232 235 :param repo_name: repository name
233 236 """
234 237 # check IP
235 238 inherit = user.inherit_default_permissions
236 239 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
237 240 inherit_from_default=inherit)
238 241 if ip_allowed:
239 242 log.info('Access for IP:%s allowed', ip_addr)
240 243 else:
241 244 return False
242 245
243 246 if action == 'push':
244 247 if not HasPermissionAnyMiddleware('repository.write',
245 248 'repository.admin')(user,
246 249 repo_name):
247 250 return False
248 251
249 252 else:
250 253 # any other action need at least read permission
251 254 if not HasPermissionAnyMiddleware('repository.read',
252 255 'repository.write',
253 256 'repository.admin')(user,
254 257 repo_name):
255 258 return False
256 259
257 260 return True
258 261
259 262 def _check_ssl(self, environ, start_response):
260 263 """
261 264 Checks the SSL check flag and returns False if SSL is not present
262 265 and required True otherwise
263 266 """
264 267 org_proto = environ['wsgi._org_proto']
265 268 # check if we have SSL required ! if not it's a bad request !
266 269 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
267 270 if require_ssl and org_proto == 'http':
268 271 log.debug('proto is %s and SSL is required BAD REQUEST !',
269 272 org_proto)
270 273 return False
271 274 return True
272 275
273 276 def __call__(self, environ, start_response):
274 277 try:
275 278 return self._handle_request(environ, start_response)
276 279 except Exception:
277 280 log.exception("Exception while handling request")
278 281 appenlight.track_exception(environ)
279 282 return HTTPInternalServerError()(environ, start_response)
280 283 finally:
281 284 meta.Session.remove()
282 285
283 286 def _handle_request(self, environ, start_response):
284 287
285 288 if not self._check_ssl(environ, start_response):
286 289 reason = ('SSL required, while RhodeCode was unable '
287 290 'to detect this as SSL request')
288 291 log.debug('User not allowed to proceed, %s', reason)
289 292 return HTTPNotAcceptable(reason)(environ, start_response)
290 293
291 294 if not self.repo_name:
292 295 log.warning('Repository name is empty: %s', self.repo_name)
293 296 # failed to get repo name, we fail now
294 297 return HTTPNotFound()(environ, start_response)
295 298 log.debug('Extracted repo name is %s', self.repo_name)
296 299
297 300 ip_addr = get_ip_addr(environ)
298 301 username = None
299 302
300 303 # skip passing error to error controller
301 304 environ['pylons.status_code_redirect'] = True
302 305
303 306 # ======================================================================
304 307 # GET ACTION PULL or PUSH
305 308 # ======================================================================
306 309 action = self._get_action(environ)
307 310
308 311 # ======================================================================
309 312 # Check if this is a request to a shadow repository of a pull request.
310 313 # In this case only pull action is allowed.
311 314 # ======================================================================
312 315 if self.pr_id is not None and action != 'pull':
313 316 reason = 'Only pull action is allowed for shadow repositories.'
314 317 log.debug('User not allowed to proceed, %s', reason)
315 318 return HTTPNotAcceptable(reason)(environ, start_response)
316 319
317 320 # ======================================================================
318 321 # CHECK ANONYMOUS PERMISSION
319 322 # ======================================================================
320 323 if action in ['pull', 'push']:
321 324 anonymous_user = User.get_default_user()
322 325 username = anonymous_user.username
323 326 if anonymous_user.active:
324 327 # ONLY check permissions if the user is activated
325 328 anonymous_perm = self._check_permission(
326 329 action, anonymous_user, self.repo_name, ip_addr)
327 330 else:
328 331 anonymous_perm = False
329 332
330 333 if not anonymous_user.active or not anonymous_perm:
331 334 if not anonymous_user.active:
332 335 log.debug('Anonymous access is disabled, running '
333 336 'authentication')
334 337
335 338 if not anonymous_perm:
336 339 log.debug('Not enough credentials to access this '
337 340 'repository as anonymous user')
338 341
339 342 username = None
340 343 # ==============================================================
341 344 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
342 345 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
343 346 # ==============================================================
344 347
345 348 # try to auth based on environ, container auth methods
346 349 log.debug('Running PRE-AUTH for container based authentication')
347 350 pre_auth = authenticate(
348 351 '', '', environ, VCS_TYPE, registry=self.registry)
349 352 if pre_auth and pre_auth.get('username'):
350 353 username = pre_auth['username']
351 354 log.debug('PRE-AUTH got %s as username', username)
352 355
353 356 # If not authenticated by the container, running basic auth
354 357 if not username:
355 358 self.authenticate.realm = get_rhodecode_realm()
356 359
357 360 try:
358 361 result = self.authenticate(environ)
359 362 except (UserCreationError, NotAllowedToCreateUserError) as e:
360 363 log.error(e)
361 364 reason = safe_str(e)
362 365 return HTTPNotAcceptable(reason)(environ, start_response)
363 366
364 367 if isinstance(result, str):
365 368 AUTH_TYPE.update(environ, 'basic')
366 369 REMOTE_USER.update(environ, result)
367 370 username = result
368 371 else:
369 372 return result.wsgi_application(environ, start_response)
370 373
371 374 # ==============================================================
372 375 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
373 376 # ==============================================================
374 377 user = User.get_by_username(username)
375 378 if not self.valid_and_active_user(user):
376 379 return HTTPForbidden()(environ, start_response)
377 380 username = user.username
378 381 user.update_lastactivity()
379 382 meta.Session().commit()
380 383
381 384 # check user attributes for password change flag
382 385 user_obj = user
383 386 if user_obj and user_obj.username != User.DEFAULT_USER and \
384 387 user_obj.user_data.get('force_password_change'):
385 388 reason = 'password change required'
386 389 log.debug('User not allowed to authenticate, %s', reason)
387 390 return HTTPNotAcceptable(reason)(environ, start_response)
388 391
389 392 # check permissions for this repository
390 393 perm = self._check_permission(
391 394 action, user, self.repo_name, ip_addr)
392 395 if not perm:
393 396 return HTTPForbidden()(environ, start_response)
394 397
395 398 # extras are injected into UI object and later available
396 399 # in hooks executed by rhodecode
397 400 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
398 401 extras = vcs_operation_context(
399 402 environ, repo_name=self.repo_name, username=username,
400 403 action=action, scm=self.SCM,
401 404 check_locking=check_locking)
402 405
403 406 # ======================================================================
404 407 # REQUEST HANDLING
405 408 # ======================================================================
406 409 str_repo_name = safe_str(self.repo_name)
407 410 repo_path = os.path.join(
408 411 safe_str(self.basepath), safe_str(self.vcs_repo_name))
409 412 log.debug('Repository path is %s', repo_path)
410 413
411 414 fix_PATH()
412 415
413 416 log.info(
414 417 '%s action on %s repo "%s" by "%s" from %s',
415 418 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
416 419
417 420 return self._generate_vcs_response(
418 421 environ, start_response, repo_path, self.url_repo_name, extras, action)
419 422
420 423 @initialize_generator
421 424 def _generate_vcs_response(
422 425 self, environ, start_response, repo_path, repo_name, extras,
423 426 action):
424 427 """
425 428 Returns a generator for the response content.
426 429
427 430 This method is implemented as a generator, so that it can trigger
428 431 the cache validation after all content sent back to the client. It
429 432 also handles the locking exceptions which will be triggered when
430 433 the first chunk is produced by the underlying WSGI application.
431 434 """
432 435 callback_daemon, extras = self._prepare_callback_daemon(extras)
433 436 config = self._create_config(extras, self.acl_repo_name)
434 437 log.debug('HOOKS extras is %s', extras)
435 438 app = self._create_wsgi_app(repo_path, repo_name, config)
436 439
437 440 try:
438 441 with callback_daemon:
439 442 try:
440 443 response = app(environ, start_response)
441 444 finally:
442 445 # This statement works together with the decorator
443 446 # "initialize_generator" above. The decorator ensures that
444 447 # we hit the first yield statement before the generator is
445 448 # returned back to the WSGI server. This is needed to
446 449 # ensure that the call to "app" above triggers the
447 450 # needed callback to "start_response" before the
448 451 # generator is actually used.
449 452 yield "__init__"
450 453
451 454 for chunk in response:
452 455 yield chunk
453 456 except Exception as exc:
454 457 # TODO: johbo: Improve "translating" back the exception.
455 458 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
456 459 exc = HTTPLockedRC(*exc.args)
457 460 _code = rhodecode.CONFIG.get('lock_ret_code')
458 461 log.debug('Repository LOCKED ret code %s!', (_code,))
459 462 elif getattr(exc, '_vcs_kind', None) == 'requirement':
460 463 log.debug(
461 464 'Repository requires features unknown to this Mercurial')
462 465 exc = HTTPRequirementError(*exc.args)
463 466 else:
464 467 raise
465 468
466 469 for chunk in exc(environ, start_response):
467 470 yield chunk
468 471 finally:
469 472 # invalidate cache on push
470 473 try:
471 474 if action == 'push':
472 475 self._invalidate_cache(repo_name)
473 476 finally:
474 477 meta.Session.remove()
475 478
476 479 def _get_repository_name(self, environ):
477 480 """Get repository name out of the environmnent
478 481
479 482 :param environ: WSGI environment
480 483 """
481 484 raise NotImplementedError()
482 485
483 486 def _get_action(self, environ):
484 487 """Map request commands into a pull or push command.
485 488
486 489 :param environ: WSGI environment
487 490 """
488 491 raise NotImplementedError()
489 492
490 493 def _create_wsgi_app(self, repo_path, repo_name, config):
491 494 """Return the WSGI app that will finally handle the request."""
492 495 raise NotImplementedError()
493 496
494 497 def _create_config(self, extras, repo_name):
495 498 """Create a Pyro safe config representation."""
496 499 raise NotImplementedError()
497 500
498 501 def _prepare_callback_daemon(self, extras):
499 502 return prepare_callback_daemon(
500 503 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
501 504 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
502 505
503 506
504 507 def _should_check_locking(query_string):
505 508 # this is kind of hacky, but due to how mercurial handles client-server
506 509 # server see all operation on commit; bookmarks, phases and
507 510 # obsolescence marker in different transaction, we don't want to check
508 511 # locking on those
509 512 return query_string not in ['cmd=listkeys']
@@ -1,3658 +1,3663 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import sys
28 28 import time
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import warnings
33 33 import ipaddress
34 34 import functools
35 35 import traceback
36 36 import collections
37 37
38 38
39 39 from sqlalchemy import *
40 40 from sqlalchemy.exc import IntegrityError
41 41 from sqlalchemy.ext.declarative import declared_attr
42 42 from sqlalchemy.ext.hybrid import hybrid_property
43 43 from sqlalchemy.orm import (
44 44 relationship, joinedload, class_mapper, validates, aliased)
45 45 from sqlalchemy.sql.expression import true
46 46 from beaker.cache import cache_region, region_invalidate
47 47 from webob.exc import HTTPNotFound
48 48 from zope.cachedescriptors.property import Lazy as LazyProperty
49 49
50 50 from pylons import url
51 51 from pylons.i18n.translation import lazy_ugettext as _
52 52
53 53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
54 54 from rhodecode.lib.vcs.utils.helpers import get_scm
55 55 from rhodecode.lib.vcs.exceptions import VCSError
56 56 from rhodecode.lib.vcs.backends.base import (
57 57 EmptyCommit, Reference, MergeFailureReason)
58 58 from rhodecode.lib.utils2 import (
59 59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
60 60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 61 glob2re)
62 62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, JSONDict
63 63 from rhodecode.lib.ext_json import json
64 64 from rhodecode.lib.caching_query import FromCache
65 65 from rhodecode.lib.encrypt import AESCipher
66 66
67 67 from rhodecode.model.meta import Base, Session
68 68
69 69 URL_SEP = '/'
70 70 log = logging.getLogger(__name__)
71 71
72 72 # =============================================================================
73 73 # BASE CLASSES
74 74 # =============================================================================
75 75
76 76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 77 # beaker.session.secret if first is not set.
78 78 # and initialized at environment.py
79 79 ENCRYPTION_KEY = None
80 80
81 81 # used to sort permissions by types, '#' used here is not allowed to be in
82 82 # usernames, and it's very early in sorted string.printable table.
83 83 PERMISSION_TYPE_SORT = {
84 84 'admin': '####',
85 85 'write': '###',
86 86 'read': '##',
87 87 'none': '#',
88 88 }
89 89
90 90
91 91 def display_sort(obj):
92 92 """
93 93 Sort function used to sort permissions in .permissions() function of
94 94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 95 of all other resources
96 96 """
97 97
98 98 if obj.username == User.DEFAULT_USER:
99 99 return '#####'
100 100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 101 return prefix + obj.username
102 102
103 103
104 104 def _hash_key(k):
105 105 return md5_safe(k)
106 106
107 107
108 108 class EncryptedTextValue(TypeDecorator):
109 109 """
110 110 Special column for encrypted long text data, use like::
111 111
112 112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
113 113
114 114 This column is intelligent so if value is in unencrypted form it return
115 115 unencrypted form, but on save it always encrypts
116 116 """
117 117 impl = Text
118 118
119 119 def process_bind_param(self, value, dialect):
120 120 if not value:
121 121 return value
122 122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
123 123 # protect against double encrypting if someone manually starts
124 124 # doing
125 125 raise ValueError('value needs to be in unencrypted format, ie. '
126 126 'not starting with enc$aes')
127 127 return 'enc$aes_hmac$%s' % AESCipher(
128 128 ENCRYPTION_KEY, hmac=True).encrypt(value)
129 129
130 130 def process_result_value(self, value, dialect):
131 131 import rhodecode
132 132
133 133 if not value:
134 134 return value
135 135
136 136 parts = value.split('$', 3)
137 137 if not len(parts) == 3:
138 138 # probably not encrypted values
139 139 return value
140 140 else:
141 141 if parts[0] != 'enc':
142 142 # parts ok but without our header ?
143 143 return value
144 144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
145 145 'rhodecode.encrypted_values.strict') or True)
146 146 # at that stage we know it's our encryption
147 147 if parts[1] == 'aes':
148 148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
149 149 elif parts[1] == 'aes_hmac':
150 150 decrypted_data = AESCipher(
151 151 ENCRYPTION_KEY, hmac=True,
152 152 strict_verification=enc_strict_mode).decrypt(parts[2])
153 153 else:
154 154 raise ValueError(
155 155 'Encryption type part is wrong, must be `aes` '
156 156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
157 157 return decrypted_data
158 158
159 159
160 160 class BaseModel(object):
161 161 """
162 162 Base Model for all classes
163 163 """
164 164
165 165 @classmethod
166 166 def _get_keys(cls):
167 167 """return column names for this model """
168 168 return class_mapper(cls).c.keys()
169 169
170 170 def get_dict(self):
171 171 """
172 172 return dict with keys and values corresponding
173 173 to this model data """
174 174
175 175 d = {}
176 176 for k in self._get_keys():
177 177 d[k] = getattr(self, k)
178 178
179 179 # also use __json__() if present to get additional fields
180 180 _json_attr = getattr(self, '__json__', None)
181 181 if _json_attr:
182 182 # update with attributes from __json__
183 183 if callable(_json_attr):
184 184 _json_attr = _json_attr()
185 185 for k, val in _json_attr.iteritems():
186 186 d[k] = val
187 187 return d
188 188
189 189 def get_appstruct(self):
190 190 """return list with keys and values tuples corresponding
191 191 to this model data """
192 192
193 193 l = []
194 194 for k in self._get_keys():
195 195 l.append((k, getattr(self, k),))
196 196 return l
197 197
198 198 def populate_obj(self, populate_dict):
199 199 """populate model with data from given populate_dict"""
200 200
201 201 for k in self._get_keys():
202 202 if k in populate_dict:
203 203 setattr(self, k, populate_dict[k])
204 204
205 205 @classmethod
206 206 def query(cls):
207 207 return Session().query(cls)
208 208
209 209 @classmethod
210 210 def get(cls, id_):
211 211 if id_:
212 212 return cls.query().get(id_)
213 213
214 214 @classmethod
215 215 def get_or_404(cls, id_):
216 216 try:
217 217 id_ = int(id_)
218 218 except (TypeError, ValueError):
219 219 raise HTTPNotFound
220 220
221 221 res = cls.query().get(id_)
222 222 if not res:
223 223 raise HTTPNotFound
224 224 return res
225 225
226 226 @classmethod
227 227 def getAll(cls):
228 228 # deprecated and left for backward compatibility
229 229 return cls.get_all()
230 230
231 231 @classmethod
232 232 def get_all(cls):
233 233 return cls.query().all()
234 234
235 235 @classmethod
236 236 def delete(cls, id_):
237 237 obj = cls.query().get(id_)
238 238 Session().delete(obj)
239 239
240 240 @classmethod
241 241 def identity_cache(cls, session, attr_name, value):
242 242 exist_in_session = []
243 243 for (item_cls, pkey), instance in session.identity_map.items():
244 244 if cls == item_cls and getattr(instance, attr_name) == value:
245 245 exist_in_session.append(instance)
246 246 if exist_in_session:
247 247 if len(exist_in_session) == 1:
248 248 return exist_in_session[0]
249 249 log.exception(
250 250 'multiple objects with attr %s and '
251 251 'value %s found with same name: %r',
252 252 attr_name, value, exist_in_session)
253 253
254 254 def __repr__(self):
255 255 if hasattr(self, '__unicode__'):
256 256 # python repr needs to return str
257 257 try:
258 258 return safe_str(self.__unicode__())
259 259 except UnicodeDecodeError:
260 260 pass
261 261 return '<DB:%s>' % (self.__class__.__name__)
262 262
263 263
264 264 class RhodeCodeSetting(Base, BaseModel):
265 265 __tablename__ = 'rhodecode_settings'
266 266 __table_args__ = (
267 267 UniqueConstraint('app_settings_name'),
268 268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 270 )
271 271
272 272 SETTINGS_TYPES = {
273 273 'str': safe_str,
274 274 'int': safe_int,
275 275 'unicode': safe_unicode,
276 276 'bool': str2bool,
277 277 'list': functools.partial(aslist, sep=',')
278 278 }
279 279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 280 GLOBAL_CONF_KEY = 'app_settings'
281 281
282 282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286 286
287 287 def __init__(self, key='', val='', type='unicode'):
288 288 self.app_settings_name = key
289 289 self.app_settings_type = type
290 290 self.app_settings_value = val
291 291
292 292 @validates('_app_settings_value')
293 293 def validate_settings_value(self, key, val):
294 294 assert type(val) == unicode
295 295 return val
296 296
297 297 @hybrid_property
298 298 def app_settings_value(self):
299 299 v = self._app_settings_value
300 300 _type = self.app_settings_type
301 301 if _type:
302 302 _type = self.app_settings_type.split('.')[0]
303 303 # decode the encrypted value
304 304 if 'encrypted' in self.app_settings_type:
305 305 cipher = EncryptedTextValue()
306 306 v = safe_unicode(cipher.process_result_value(v, None))
307 307
308 308 converter = self.SETTINGS_TYPES.get(_type) or \
309 309 self.SETTINGS_TYPES['unicode']
310 310 return converter(v)
311 311
312 312 @app_settings_value.setter
313 313 def app_settings_value(self, val):
314 314 """
315 315 Setter that will always make sure we use unicode in app_settings_value
316 316
317 317 :param val:
318 318 """
319 319 val = safe_unicode(val)
320 320 # encode the encrypted value
321 321 if 'encrypted' in self.app_settings_type:
322 322 cipher = EncryptedTextValue()
323 323 val = safe_unicode(cipher.process_bind_param(val, None))
324 324 self._app_settings_value = val
325 325
326 326 @hybrid_property
327 327 def app_settings_type(self):
328 328 return self._app_settings_type
329 329
330 330 @app_settings_type.setter
331 331 def app_settings_type(self, val):
332 332 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 333 raise Exception('type must be one of %s got %s'
334 334 % (self.SETTINGS_TYPES.keys(), val))
335 335 self._app_settings_type = val
336 336
337 337 def __unicode__(self):
338 338 return u"<%s('%s:%s[%s]')>" % (
339 339 self.__class__.__name__,
340 340 self.app_settings_name, self.app_settings_value,
341 341 self.app_settings_type
342 342 )
343 343
344 344
345 345 class RhodeCodeUi(Base, BaseModel):
346 346 __tablename__ = 'rhodecode_ui'
347 347 __table_args__ = (
348 348 UniqueConstraint('ui_key'),
349 349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 351 )
352 352
353 353 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 354 # HG
355 355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 356 HOOK_PULL = 'outgoing.pull_logger'
357 357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 358 HOOK_PUSH = 'changegroup.push_logger'
359 359
360 360 # TODO: johbo: Unify way how hooks are configured for git and hg,
361 361 # git part is currently hardcoded.
362 362
363 363 # SVN PATTERNS
364 364 SVN_BRANCH_ID = 'vcs_svn_branch'
365 365 SVN_TAG_ID = 'vcs_svn_tag'
366 366
367 367 ui_id = Column(
368 368 "ui_id", Integer(), nullable=False, unique=True, default=None,
369 369 primary_key=True)
370 370 ui_section = Column(
371 371 "ui_section", String(255), nullable=True, unique=None, default=None)
372 372 ui_key = Column(
373 373 "ui_key", String(255), nullable=True, unique=None, default=None)
374 374 ui_value = Column(
375 375 "ui_value", String(255), nullable=True, unique=None, default=None)
376 376 ui_active = Column(
377 377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
378 378
379 379 def __repr__(self):
380 380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
381 381 self.ui_key, self.ui_value)
382 382
383 383
384 384 class RepoRhodeCodeSetting(Base, BaseModel):
385 385 __tablename__ = 'repo_rhodecode_settings'
386 386 __table_args__ = (
387 387 UniqueConstraint(
388 388 'app_settings_name', 'repository_id',
389 389 name='uq_repo_rhodecode_setting_name_repo_id'),
390 390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
391 391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
392 392 )
393 393
394 394 repository_id = Column(
395 395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
396 396 nullable=False)
397 397 app_settings_id = Column(
398 398 "app_settings_id", Integer(), nullable=False, unique=True,
399 399 default=None, primary_key=True)
400 400 app_settings_name = Column(
401 401 "app_settings_name", String(255), nullable=True, unique=None,
402 402 default=None)
403 403 _app_settings_value = Column(
404 404 "app_settings_value", String(4096), nullable=True, unique=None,
405 405 default=None)
406 406 _app_settings_type = Column(
407 407 "app_settings_type", String(255), nullable=True, unique=None,
408 408 default=None)
409 409
410 410 repository = relationship('Repository')
411 411
412 412 def __init__(self, repository_id, key='', val='', type='unicode'):
413 413 self.repository_id = repository_id
414 414 self.app_settings_name = key
415 415 self.app_settings_type = type
416 416 self.app_settings_value = val
417 417
418 418 @validates('_app_settings_value')
419 419 def validate_settings_value(self, key, val):
420 420 assert type(val) == unicode
421 421 return val
422 422
423 423 @hybrid_property
424 424 def app_settings_value(self):
425 425 v = self._app_settings_value
426 426 type_ = self.app_settings_type
427 427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
428 428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
429 429 return converter(v)
430 430
431 431 @app_settings_value.setter
432 432 def app_settings_value(self, val):
433 433 """
434 434 Setter that will always make sure we use unicode in app_settings_value
435 435
436 436 :param val:
437 437 """
438 438 self._app_settings_value = safe_unicode(val)
439 439
440 440 @hybrid_property
441 441 def app_settings_type(self):
442 442 return self._app_settings_type
443 443
444 444 @app_settings_type.setter
445 445 def app_settings_type(self, val):
446 446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
447 447 if val not in SETTINGS_TYPES:
448 448 raise Exception('type must be one of %s got %s'
449 449 % (SETTINGS_TYPES.keys(), val))
450 450 self._app_settings_type = val
451 451
452 452 def __unicode__(self):
453 453 return u"<%s('%s:%s:%s[%s]')>" % (
454 454 self.__class__.__name__, self.repository.repo_name,
455 455 self.app_settings_name, self.app_settings_value,
456 456 self.app_settings_type
457 457 )
458 458
459 459
460 460 class RepoRhodeCodeUi(Base, BaseModel):
461 461 __tablename__ = 'repo_rhodecode_ui'
462 462 __table_args__ = (
463 463 UniqueConstraint(
464 464 'repository_id', 'ui_section', 'ui_key',
465 465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
466 466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
467 467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
468 468 )
469 469
470 470 repository_id = Column(
471 471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
472 472 nullable=False)
473 473 ui_id = Column(
474 474 "ui_id", Integer(), nullable=False, unique=True, default=None,
475 475 primary_key=True)
476 476 ui_section = Column(
477 477 "ui_section", String(255), nullable=True, unique=None, default=None)
478 478 ui_key = Column(
479 479 "ui_key", String(255), nullable=True, unique=None, default=None)
480 480 ui_value = Column(
481 481 "ui_value", String(255), nullable=True, unique=None, default=None)
482 482 ui_active = Column(
483 483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
484 484
485 485 repository = relationship('Repository')
486 486
487 487 def __repr__(self):
488 488 return '<%s[%s:%s]%s=>%s]>' % (
489 489 self.__class__.__name__, self.repository.repo_name,
490 490 self.ui_section, self.ui_key, self.ui_value)
491 491
492 492
493 493 class User(Base, BaseModel):
494 494 __tablename__ = 'users'
495 495 __table_args__ = (
496 496 UniqueConstraint('username'), UniqueConstraint('email'),
497 497 Index('u_username_idx', 'username'),
498 498 Index('u_email_idx', 'email'),
499 499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
500 500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
501 501 )
502 502 DEFAULT_USER = 'default'
503 503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
504 504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
505 505
506 506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
507 507 username = Column("username", String(255), nullable=True, unique=None, default=None)
508 508 password = Column("password", String(255), nullable=True, unique=None, default=None)
509 509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
510 510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
511 511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
512 512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
513 513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
514 514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
515 515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521 521
522 522 user_log = relationship('UserLog')
523 523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524 524
525 525 repositories = relationship('Repository')
526 526 repository_groups = relationship('RepoGroup')
527 527 user_groups = relationship('UserGroup')
528 528
529 529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531 531
532 532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535 535
536 536 group_member = relationship('UserGroupMember', cascade='all')
537 537
538 538 notifications = relationship('UserNotification', cascade='all')
539 539 # notifications assigned to this user
540 540 user_created_notifications = relationship('Notification', cascade='all')
541 541 # comments created by this user
542 542 user_comments = relationship('ChangesetComment', cascade='all')
543 543 # user profile extra info
544 544 user_emails = relationship('UserEmailMap', cascade='all')
545 545 user_ip_map = relationship('UserIpMap', cascade='all')
546 546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 547 # gists
548 548 user_gists = relationship('Gist', cascade='all')
549 549 # user pull requests
550 550 user_pull_requests = relationship('PullRequest', cascade='all')
551 551 # external identities
552 552 extenal_identities = relationship(
553 553 'ExternalIdentity',
554 554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
555 555 cascade='all')
556 556
557 557 def __unicode__(self):
558 558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
559 559 self.user_id, self.username)
560 560
561 561 @hybrid_property
562 562 def email(self):
563 563 return self._email
564 564
565 565 @email.setter
566 566 def email(self, val):
567 567 self._email = val.lower() if val else None
568 568
569 569 @property
570 570 def firstname(self):
571 571 # alias for future
572 572 return self.name
573 573
574 574 @property
575 575 def emails(self):
576 576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
577 577 return [self.email] + [x.email for x in other]
578 578
579 579 @property
580 580 def auth_tokens(self):
581 581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
582 582
583 583 @property
584 584 def extra_auth_tokens(self):
585 585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
586 586
587 587 @property
588 588 def feed_token(self):
589 589 feed_tokens = UserApiKeys.query()\
590 590 .filter(UserApiKeys.user == self)\
591 591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
592 592 .all()
593 593 if feed_tokens:
594 594 return feed_tokens[0].api_key
595 595 else:
596 596 # use the main token so we don't end up with nothing...
597 597 return self.api_key
598 598
599 599 @classmethod
600 600 def extra_valid_auth_tokens(cls, user, role=None):
601 601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
602 602 .filter(or_(UserApiKeys.expires == -1,
603 603 UserApiKeys.expires >= time.time()))
604 604 if role:
605 605 tokens = tokens.filter(or_(UserApiKeys.role == role,
606 606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
607 607 return tokens.all()
608 608
609 609 @property
610 610 def ip_addresses(self):
611 611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 612 return [x.ip_addr for x in ret]
613 613
614 614 @property
615 615 def username_and_name(self):
616 616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617 617
618 618 @property
619 619 def username_or_name_or_email(self):
620 620 full_name = self.full_name if self.full_name is not ' ' else None
621 621 return self.username or full_name or self.email
622 622
623 623 @property
624 624 def full_name(self):
625 625 return '%s %s' % (self.firstname, self.lastname)
626 626
627 627 @property
628 628 def full_name_or_username(self):
629 629 return ('%s %s' % (self.firstname, self.lastname)
630 630 if (self.firstname and self.lastname) else self.username)
631 631
632 632 @property
633 633 def full_contact(self):
634 634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635 635
636 636 @property
637 637 def short_contact(self):
638 638 return '%s %s' % (self.firstname, self.lastname)
639 639
640 640 @property
641 641 def is_admin(self):
642 642 return self.admin
643 643
644 644 @property
645 645 def AuthUser(self):
646 646 """
647 647 Returns instance of AuthUser for this user
648 648 """
649 649 from rhodecode.lib.auth import AuthUser
650 650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 651 username=self.username)
652 652
653 653 @hybrid_property
654 654 def user_data(self):
655 655 if not self._user_data:
656 656 return {}
657 657
658 658 try:
659 659 return json.loads(self._user_data)
660 660 except TypeError:
661 661 return {}
662 662
663 663 @user_data.setter
664 664 def user_data(self, val):
665 665 if not isinstance(val, dict):
666 666 raise Exception('user_data must be dict, got %s' % type(val))
667 667 try:
668 668 self._user_data = json.dumps(val)
669 669 except Exception:
670 670 log.error(traceback.format_exc())
671 671
672 672 @classmethod
673 673 def get_by_username(cls, username, case_insensitive=False,
674 674 cache=False, identity_cache=False):
675 675 session = Session()
676 676
677 677 if case_insensitive:
678 678 q = cls.query().filter(
679 679 func.lower(cls.username) == func.lower(username))
680 680 else:
681 681 q = cls.query().filter(cls.username == username)
682 682
683 683 if cache:
684 684 if identity_cache:
685 685 val = cls.identity_cache(session, 'username', username)
686 686 if val:
687 687 return val
688 688 else:
689 689 q = q.options(
690 690 FromCache("sql_cache_short",
691 691 "get_user_by_name_%s" % _hash_key(username)))
692 692
693 693 return q.scalar()
694 694
695 695 @classmethod
696 696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 697 q = cls.query().filter(cls.api_key == auth_token)
698 698
699 699 if cache:
700 700 q = q.options(FromCache("sql_cache_short",
701 701 "get_auth_token_%s" % auth_token))
702 702 res = q.scalar()
703 703
704 704 if fallback and not res:
705 705 #fallback to additional keys
706 706 _res = UserApiKeys.query()\
707 707 .filter(UserApiKeys.api_key == auth_token)\
708 708 .filter(or_(UserApiKeys.expires == -1,
709 709 UserApiKeys.expires >= time.time()))\
710 710 .first()
711 711 if _res:
712 712 res = _res.user
713 713 return res
714 714
715 715 @classmethod
716 716 def get_by_email(cls, email, case_insensitive=False, cache=False):
717 717
718 718 if case_insensitive:
719 719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720 720
721 721 else:
722 722 q = cls.query().filter(cls.email == email)
723 723
724 724 if cache:
725 725 q = q.options(FromCache("sql_cache_short",
726 726 "get_email_key_%s" % _hash_key(email)))
727 727
728 728 ret = q.scalar()
729 729 if ret is None:
730 730 q = UserEmailMap.query()
731 731 # try fetching in alternate email map
732 732 if case_insensitive:
733 733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 734 else:
735 735 q = q.filter(UserEmailMap.email == email)
736 736 q = q.options(joinedload(UserEmailMap.user))
737 737 if cache:
738 738 q = q.options(FromCache("sql_cache_short",
739 739 "get_email_map_key_%s" % email))
740 740 ret = getattr(q.scalar(), 'user', None)
741 741
742 742 return ret
743 743
744 744 @classmethod
745 745 def get_from_cs_author(cls, author):
746 746 """
747 747 Tries to get User objects out of commit author string
748 748
749 749 :param author:
750 750 """
751 751 from rhodecode.lib.helpers import email, author_name
752 752 # Valid email in the attribute passed, see if they're in the system
753 753 _email = email(author)
754 754 if _email:
755 755 user = cls.get_by_email(_email, case_insensitive=True)
756 756 if user:
757 757 return user
758 758 # Maybe we can match by username?
759 759 _author = author_name(author)
760 760 user = cls.get_by_username(_author, case_insensitive=True)
761 761 if user:
762 762 return user
763 763
764 764 def update_userdata(self, **kwargs):
765 765 usr = self
766 766 old = usr.user_data
767 767 old.update(**kwargs)
768 768 usr.user_data = old
769 769 Session().add(usr)
770 770 log.debug('updated userdata with ', kwargs)
771 771
772 772 def update_lastlogin(self):
773 773 """Update user lastlogin"""
774 774 self.last_login = datetime.datetime.now()
775 775 Session().add(self)
776 776 log.debug('updated user %s lastlogin', self.username)
777 777
778 778 def update_lastactivity(self):
779 779 """Update user lastactivity"""
780 780 usr = self
781 781 old = usr.user_data
782 782 old.update({'last_activity': time.time()})
783 783 usr.user_data = old
784 784 Session().add(usr)
785 785 log.debug('updated user %s lastactivity', usr.username)
786 786
787 787 def update_password(self, new_password, change_api_key=False):
788 788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789 789
790 790 self.password = get_crypt_password(new_password)
791 791 if change_api_key:
792 792 self.api_key = generate_auth_token(self.username)
793 793 Session().add(self)
794 794
795 795 @classmethod
796 796 def get_first_super_admin(cls):
797 797 user = User.query().filter(User.admin == true()).first()
798 798 if user is None:
799 799 raise Exception('FATAL: Missing administrative account!')
800 800 return user
801 801
802 802 @classmethod
803 803 def get_all_super_admins(cls):
804 804 """
805 805 Returns all admin accounts sorted by username
806 806 """
807 807 return User.query().filter(User.admin == true())\
808 808 .order_by(User.username.asc()).all()
809 809
810 810 @classmethod
811 811 def get_default_user(cls, cache=False):
812 812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 813 if user is None:
814 814 raise Exception('FATAL: Missing default account!')
815 815 return user
816 816
817 817 def _get_default_perms(self, user, suffix=''):
818 818 from rhodecode.model.permission import PermissionModel
819 819 return PermissionModel().get_default_perms(user.user_perms, suffix)
820 820
821 821 def get_default_perms(self, suffix=''):
822 822 return self._get_default_perms(self, suffix)
823 823
824 824 def get_api_data(self, include_secrets=False, details='full'):
825 825 """
826 826 Common function for generating user related data for API
827 827
828 828 :param include_secrets: By default secrets in the API data will be replaced
829 829 by a placeholder value to prevent exposing this data by accident. In case
830 830 this data shall be exposed, set this flag to ``True``.
831 831
832 832 :param details: details can be 'basic|full' basic gives only a subset of
833 833 the available user information that includes user_id, name and emails.
834 834 """
835 835 user = self
836 836 user_data = self.user_data
837 837 data = {
838 838 'user_id': user.user_id,
839 839 'username': user.username,
840 840 'firstname': user.name,
841 841 'lastname': user.lastname,
842 842 'email': user.email,
843 843 'emails': user.emails,
844 844 }
845 845 if details == 'basic':
846 846 return data
847 847
848 848 api_key_length = 40
849 849 api_key_replacement = '*' * api_key_length
850 850
851 851 extras = {
852 852 'api_key': api_key_replacement,
853 853 'api_keys': [api_key_replacement],
854 854 'active': user.active,
855 855 'admin': user.admin,
856 856 'extern_type': user.extern_type,
857 857 'extern_name': user.extern_name,
858 858 'last_login': user.last_login,
859 859 'ip_addresses': user.ip_addresses,
860 860 'language': user_data.get('language')
861 861 }
862 862 data.update(extras)
863 863
864 864 if include_secrets:
865 865 data['api_key'] = user.api_key
866 866 data['api_keys'] = user.auth_tokens
867 867 return data
868 868
869 869 def __json__(self):
870 870 data = {
871 871 'full_name': self.full_name,
872 872 'full_name_or_username': self.full_name_or_username,
873 873 'short_contact': self.short_contact,
874 874 'full_contact': self.full_contact,
875 875 }
876 876 data.update(self.get_api_data())
877 877 return data
878 878
879 879
880 880 class UserApiKeys(Base, BaseModel):
881 881 __tablename__ = 'user_api_keys'
882 882 __table_args__ = (
883 883 Index('uak_api_key_idx', 'api_key'),
884 884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 885 UniqueConstraint('api_key'),
886 886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 888 )
889 889 __mapper_args__ = {}
890 890
891 891 # ApiKey role
892 892 ROLE_ALL = 'token_role_all'
893 893 ROLE_HTTP = 'token_role_http'
894 894 ROLE_VCS = 'token_role_vcs'
895 895 ROLE_API = 'token_role_api'
896 896 ROLE_FEED = 'token_role_feed'
897 897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898 898
899 899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 901 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 903 expires = Column('expires', Float(53), nullable=False)
904 904 role = Column('role', String(255), nullable=True)
905 905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906 906
907 907 user = relationship('User', lazy='joined')
908 908
909 909 @classmethod
910 910 def _get_role_name(cls, role):
911 911 return {
912 912 cls.ROLE_ALL: _('all'),
913 913 cls.ROLE_HTTP: _('http/web interface'),
914 914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 915 cls.ROLE_API: _('api calls'),
916 916 cls.ROLE_FEED: _('feed access'),
917 917 }.get(role, role)
918 918
919 919 @property
920 920 def expired(self):
921 921 if self.expires == -1:
922 922 return False
923 923 return time.time() > self.expires
924 924
925 925 @property
926 926 def role_humanized(self):
927 927 return self._get_role_name(self.role)
928 928
929 929
930 930 class UserEmailMap(Base, BaseModel):
931 931 __tablename__ = 'user_email_map'
932 932 __table_args__ = (
933 933 Index('uem_email_idx', 'email'),
934 934 UniqueConstraint('email'),
935 935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 937 )
938 938 __mapper_args__ = {}
939 939
940 940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 943 user = relationship('User', lazy='joined')
944 944
945 945 @validates('_email')
946 946 def validate_email(self, key, email):
947 947 # check if this email is not main one
948 948 main_email = Session().query(User).filter(User.email == email).scalar()
949 949 if main_email is not None:
950 950 raise AttributeError('email %s is present is user table' % email)
951 951 return email
952 952
953 953 @hybrid_property
954 954 def email(self):
955 955 return self._email
956 956
957 957 @email.setter
958 958 def email(self, val):
959 959 self._email = val.lower() if val else None
960 960
961 961
962 962 class UserIpMap(Base, BaseModel):
963 963 __tablename__ = 'user_ip_map'
964 964 __table_args__ = (
965 965 UniqueConstraint('user_id', 'ip_addr'),
966 966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 968 )
969 969 __mapper_args__ = {}
970 970
971 971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 976 user = relationship('User', lazy='joined')
977 977
978 978 @classmethod
979 979 def _get_ip_range(cls, ip_addr):
980 980 net = ipaddress.ip_network(ip_addr, strict=False)
981 981 return [str(net.network_address), str(net.broadcast_address)]
982 982
983 983 def __json__(self):
984 984 return {
985 985 'ip_addr': self.ip_addr,
986 986 'ip_range': self._get_ip_range(self.ip_addr),
987 987 }
988 988
989 989 def __unicode__(self):
990 990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 991 self.user_id, self.ip_addr)
992 992
993 993 class UserLog(Base, BaseModel):
994 994 __tablename__ = 'user_logs'
995 995 __table_args__ = (
996 996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 998 )
999 999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007 1007
1008 1008 def __unicode__(self):
1009 1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 1010 self.repository_name,
1011 1011 self.action)
1012 1012
1013 1013 @property
1014 1014 def action_as_day(self):
1015 1015 return datetime.date(*self.action_date.timetuple()[:3])
1016 1016
1017 1017 user = relationship('User')
1018 1018 repository = relationship('Repository', cascade='')
1019 1019
1020 1020
1021 1021 class UserGroup(Base, BaseModel):
1022 1022 __tablename__ = 'users_groups'
1023 1023 __table_args__ = (
1024 1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 1026 )
1027 1027
1028 1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036 1036
1037 1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043 1043
1044 1044 user = relationship('User')
1045 1045
1046 1046 @hybrid_property
1047 1047 def group_data(self):
1048 1048 if not self._group_data:
1049 1049 return {}
1050 1050
1051 1051 try:
1052 1052 return json.loads(self._group_data)
1053 1053 except TypeError:
1054 1054 return {}
1055 1055
1056 1056 @group_data.setter
1057 1057 def group_data(self, val):
1058 1058 try:
1059 1059 self._group_data = json.dumps(val)
1060 1060 except Exception:
1061 1061 log.error(traceback.format_exc())
1062 1062
1063 1063 def __unicode__(self):
1064 1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 1065 self.users_group_id,
1066 1066 self.users_group_name)
1067 1067
1068 1068 @classmethod
1069 1069 def get_by_group_name(cls, group_name, cache=False,
1070 1070 case_insensitive=False):
1071 1071 if case_insensitive:
1072 1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 1073 func.lower(group_name))
1074 1074
1075 1075 else:
1076 1076 q = cls.query().filter(cls.users_group_name == group_name)
1077 1077 if cache:
1078 1078 q = q.options(FromCache(
1079 1079 "sql_cache_short",
1080 1080 "get_group_%s" % _hash_key(group_name)))
1081 1081 return q.scalar()
1082 1082
1083 1083 @classmethod
1084 1084 def get(cls, user_group_id, cache=False):
1085 1085 user_group = cls.query()
1086 1086 if cache:
1087 1087 user_group = user_group.options(FromCache("sql_cache_short",
1088 1088 "get_users_group_%s" % user_group_id))
1089 1089 return user_group.get(user_group_id)
1090 1090
1091 1091 def permissions(self, with_admins=True, with_owner=True):
1092 1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 1094 joinedload(UserUserGroupToPerm.user),
1095 1095 joinedload(UserUserGroupToPerm.permission),)
1096 1096
1097 1097 # get owners and admins and permissions. We do a trick of re-writing
1098 1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 1099 # has a global reference and changing one object propagates to all
1100 1100 # others. This means if admin is also an owner admin_row that change
1101 1101 # would propagate to both objects
1102 1102 perm_rows = []
1103 1103 for _usr in q.all():
1104 1104 usr = AttributeDict(_usr.user.get_dict())
1105 1105 usr.permission = _usr.permission.permission_name
1106 1106 perm_rows.append(usr)
1107 1107
1108 1108 # filter the perm rows by 'default' first and then sort them by
1109 1109 # admin,write,read,none permissions sorted again alphabetically in
1110 1110 # each group
1111 1111 perm_rows = sorted(perm_rows, key=display_sort)
1112 1112
1113 1113 _admin_perm = 'usergroup.admin'
1114 1114 owner_row = []
1115 1115 if with_owner:
1116 1116 usr = AttributeDict(self.user.get_dict())
1117 1117 usr.owner_row = True
1118 1118 usr.permission = _admin_perm
1119 1119 owner_row.append(usr)
1120 1120
1121 1121 super_admin_rows = []
1122 1122 if with_admins:
1123 1123 for usr in User.get_all_super_admins():
1124 1124 # if this admin is also owner, don't double the record
1125 1125 if usr.user_id == owner_row[0].user_id:
1126 1126 owner_row[0].admin_row = True
1127 1127 else:
1128 1128 usr = AttributeDict(usr.get_dict())
1129 1129 usr.admin_row = True
1130 1130 usr.permission = _admin_perm
1131 1131 super_admin_rows.append(usr)
1132 1132
1133 1133 return super_admin_rows + owner_row + perm_rows
1134 1134
1135 1135 def permission_user_groups(self):
1136 1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 1139 joinedload(UserGroupUserGroupToPerm.permission),)
1140 1140
1141 1141 perm_rows = []
1142 1142 for _user_group in q.all():
1143 1143 usr = AttributeDict(_user_group.user_group.get_dict())
1144 1144 usr.permission = _user_group.permission.permission_name
1145 1145 perm_rows.append(usr)
1146 1146
1147 1147 return perm_rows
1148 1148
1149 1149 def _get_default_perms(self, user_group, suffix=''):
1150 1150 from rhodecode.model.permission import PermissionModel
1151 1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152 1152
1153 1153 def get_default_perms(self, suffix=''):
1154 1154 return self._get_default_perms(self, suffix)
1155 1155
1156 1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 1157 """
1158 1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 1159 basically forwarded.
1160 1160
1161 1161 """
1162 1162 user_group = self
1163 1163
1164 1164 data = {
1165 1165 'users_group_id': user_group.users_group_id,
1166 1166 'group_name': user_group.users_group_name,
1167 1167 'group_description': user_group.user_group_description,
1168 1168 'active': user_group.users_group_active,
1169 1169 'owner': user_group.user.username,
1170 1170 }
1171 1171 if with_group_members:
1172 1172 users = []
1173 1173 for user in user_group.members:
1174 1174 user = user.user
1175 1175 users.append(user.get_api_data(include_secrets=include_secrets))
1176 1176 data['users'] = users
1177 1177
1178 1178 return data
1179 1179
1180 1180
1181 1181 class UserGroupMember(Base, BaseModel):
1182 1182 __tablename__ = 'users_groups_members'
1183 1183 __table_args__ = (
1184 1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 1186 )
1187 1187
1188 1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191 1191
1192 1192 user = relationship('User', lazy='joined')
1193 1193 users_group = relationship('UserGroup')
1194 1194
1195 1195 def __init__(self, gr_id='', u_id=''):
1196 1196 self.users_group_id = gr_id
1197 1197 self.user_id = u_id
1198 1198
1199 1199
1200 1200 class RepositoryField(Base, BaseModel):
1201 1201 __tablename__ = 'repositories_fields'
1202 1202 __table_args__ = (
1203 1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 1206 )
1207 1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208 1208
1209 1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 1211 field_key = Column("field_key", String(250))
1212 1212 field_label = Column("field_label", String(1024), nullable=False)
1213 1213 field_value = Column("field_value", String(10000), nullable=False)
1214 1214 field_desc = Column("field_desc", String(1024), nullable=False)
1215 1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217 1217
1218 1218 repository = relationship('Repository')
1219 1219
1220 1220 @property
1221 1221 def field_key_prefixed(self):
1222 1222 return 'ex_%s' % self.field_key
1223 1223
1224 1224 @classmethod
1225 1225 def un_prefix_key(cls, key):
1226 1226 if key.startswith(cls.PREFIX):
1227 1227 return key[len(cls.PREFIX):]
1228 1228 return key
1229 1229
1230 1230 @classmethod
1231 1231 def get_by_key_name(cls, key, repo):
1232 1232 row = cls.query()\
1233 1233 .filter(cls.repository == repo)\
1234 1234 .filter(cls.field_key == key).scalar()
1235 1235 return row
1236 1236
1237 1237
1238 1238 class Repository(Base, BaseModel):
1239 1239 __tablename__ = 'repositories'
1240 1240 __table_args__ = (
1241 1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 1244 )
1245 1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247 1247
1248 1248 STATE_CREATED = 'repo_state_created'
1249 1249 STATE_PENDING = 'repo_state_pending'
1250 1250 STATE_ERROR = 'repo_state_error'
1251 1251
1252 1252 LOCK_AUTOMATIC = 'lock_auto'
1253 1253 LOCK_API = 'lock_api'
1254 1254 LOCK_WEB = 'lock_web'
1255 1255 LOCK_PULL = 'lock_pull'
1256 1256
1257 1257 NAME_SEP = URL_SEP
1258 1258
1259 1259 repo_id = Column(
1260 1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 1261 primary_key=True)
1262 1262 _repo_name = Column(
1263 1263 "repo_name", Text(), nullable=False, default=None)
1264 1264 _repo_name_hash = Column(
1265 1265 "repo_name_hash", String(255), nullable=False, unique=True)
1266 1266 repo_state = Column("repo_state", String(255), nullable=True)
1267 1267
1268 1268 clone_uri = Column(
1269 1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 1270 default=None)
1271 1271 repo_type = Column(
1272 1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 1273 user_id = Column(
1274 1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 1275 unique=False, default=None)
1276 1276 private = Column(
1277 1277 "private", Boolean(), nullable=True, unique=None, default=None)
1278 1278 enable_statistics = Column(
1279 1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 1280 enable_downloads = Column(
1281 1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 1282 description = Column(
1283 1283 "description", String(10000), nullable=True, unique=None, default=None)
1284 1284 created_on = Column(
1285 1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 1286 default=datetime.datetime.now)
1287 1287 updated_on = Column(
1288 1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 1289 default=datetime.datetime.now)
1290 1290 _landing_revision = Column(
1291 1291 "landing_revision", String(255), nullable=False, unique=False,
1292 1292 default=None)
1293 1293 enable_locking = Column(
1294 1294 "enable_locking", Boolean(), nullable=False, unique=None,
1295 1295 default=False)
1296 1296 _locked = Column(
1297 1297 "locked", String(255), nullable=True, unique=False, default=None)
1298 1298 _changeset_cache = Column(
1299 1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300 1300
1301 1301 fork_id = Column(
1302 1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 1303 nullable=True, unique=False, default=None)
1304 1304 group_id = Column(
1305 1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 1306 unique=False, default=None)
1307 1307
1308 1308 user = relationship('User', lazy='joined')
1309 1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 1310 group = relationship('RepoGroup', lazy='joined')
1311 1311 repo_to_perm = relationship(
1312 1312 'UserRepoToPerm', cascade='all',
1313 1313 order_by='UserRepoToPerm.repo_to_perm_id')
1314 1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 1315 stats = relationship('Statistics', cascade='all', uselist=False)
1316 1316
1317 1317 followers = relationship(
1318 1318 'UserFollowing',
1319 1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 1320 cascade='all')
1321 1321 extra_fields = relationship(
1322 1322 'RepositoryField', cascade="all, delete, delete-orphan")
1323 1323 logs = relationship('UserLog')
1324 1324 comments = relationship(
1325 1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 1326 pull_requests_source = relationship(
1327 1327 'PullRequest',
1328 1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 1329 cascade="all, delete, delete-orphan")
1330 1330 pull_requests_target = relationship(
1331 1331 'PullRequest',
1332 1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 1333 cascade="all, delete, delete-orphan")
1334 1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 1336 integrations = relationship('Integration',
1337 1337 cascade="all, delete, delete-orphan")
1338 1338
1339 1339 def __unicode__(self):
1340 1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 1341 safe_unicode(self.repo_name))
1342 1342
1343 1343 @hybrid_property
1344 1344 def landing_rev(self):
1345 1345 # always should return [rev_type, rev]
1346 1346 if self._landing_revision:
1347 1347 _rev_info = self._landing_revision.split(':')
1348 1348 if len(_rev_info) < 2:
1349 1349 _rev_info.insert(0, 'rev')
1350 1350 return [_rev_info[0], _rev_info[1]]
1351 1351 return [None, None]
1352 1352
1353 1353 @landing_rev.setter
1354 1354 def landing_rev(self, val):
1355 1355 if ':' not in val:
1356 1356 raise ValueError('value must be delimited with `:` and consist '
1357 1357 'of <rev_type>:<rev>, got %s instead' % val)
1358 1358 self._landing_revision = val
1359 1359
1360 1360 @hybrid_property
1361 1361 def locked(self):
1362 1362 if self._locked:
1363 1363 user_id, timelocked, reason = self._locked.split(':')
1364 1364 lock_values = int(user_id), timelocked, reason
1365 1365 else:
1366 1366 lock_values = [None, None, None]
1367 1367 return lock_values
1368 1368
1369 1369 @locked.setter
1370 1370 def locked(self, val):
1371 1371 if val and isinstance(val, (list, tuple)):
1372 1372 self._locked = ':'.join(map(str, val))
1373 1373 else:
1374 1374 self._locked = None
1375 1375
1376 1376 @hybrid_property
1377 1377 def changeset_cache(self):
1378 1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 1379 dummy = EmptyCommit().__json__()
1380 1380 if not self._changeset_cache:
1381 1381 return dummy
1382 1382 try:
1383 1383 return json.loads(self._changeset_cache)
1384 1384 except TypeError:
1385 1385 return dummy
1386 1386 except Exception:
1387 1387 log.error(traceback.format_exc())
1388 1388 return dummy
1389 1389
1390 1390 @changeset_cache.setter
1391 1391 def changeset_cache(self, val):
1392 1392 try:
1393 1393 self._changeset_cache = json.dumps(val)
1394 1394 except Exception:
1395 1395 log.error(traceback.format_exc())
1396 1396
1397 1397 @hybrid_property
1398 1398 def repo_name(self):
1399 1399 return self._repo_name
1400 1400
1401 1401 @repo_name.setter
1402 1402 def repo_name(self, value):
1403 1403 self._repo_name = value
1404 1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405 1405
1406 1406 @classmethod
1407 1407 def normalize_repo_name(cls, repo_name):
1408 1408 """
1409 1409 Normalizes os specific repo_name to the format internally stored inside
1410 1410 database using URL_SEP
1411 1411
1412 1412 :param cls:
1413 1413 :param repo_name:
1414 1414 """
1415 1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416 1416
1417 1417 @classmethod
1418 1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 1419 session = Session()
1420 1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1421 1421
1422 1422 if cache:
1423 1423 if identity_cache:
1424 1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 1425 if val:
1426 1426 return val
1427 1427 else:
1428 1428 q = q.options(
1429 1429 FromCache("sql_cache_short",
1430 1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431 1431
1432 1432 return q.scalar()
1433 1433
1434 1434 @classmethod
1435 1435 def get_by_full_path(cls, repo_full_path):
1436 1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 1437 repo_name = cls.normalize_repo_name(repo_name)
1438 1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439 1439
1440 1440 @classmethod
1441 1441 def get_repo_forks(cls, repo_id):
1442 1442 return cls.query().filter(Repository.fork_id == repo_id)
1443 1443
1444 1444 @classmethod
1445 1445 def base_path(cls):
1446 1446 """
1447 1447 Returns base path when all repos are stored
1448 1448
1449 1449 :param cls:
1450 1450 """
1451 1451 q = Session().query(RhodeCodeUi)\
1452 1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 1454 return q.one().ui_value
1455 1455
1456 1456 @classmethod
1457 1457 def is_valid(cls, repo_name):
1458 1458 """
1459 1459 returns True if given repo name is a valid filesystem repository
1460 1460
1461 1461 :param cls:
1462 1462 :param repo_name:
1463 1463 """
1464 1464 from rhodecode.lib.utils import is_valid_repo
1465 1465
1466 1466 return is_valid_repo(repo_name, cls.base_path())
1467 1467
1468 1468 @classmethod
1469 1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 1470 case_insensitive=True):
1471 1471 q = Repository.query()
1472 1472
1473 1473 if not isinstance(user_id, Optional):
1474 1474 q = q.filter(Repository.user_id == user_id)
1475 1475
1476 1476 if not isinstance(group_id, Optional):
1477 1477 q = q.filter(Repository.group_id == group_id)
1478 1478
1479 1479 if case_insensitive:
1480 1480 q = q.order_by(func.lower(Repository.repo_name))
1481 1481 else:
1482 1482 q = q.order_by(Repository.repo_name)
1483 1483 return q.all()
1484 1484
1485 1485 @property
1486 1486 def forks(self):
1487 1487 """
1488 1488 Return forks of this repo
1489 1489 """
1490 1490 return Repository.get_repo_forks(self.repo_id)
1491 1491
1492 1492 @property
1493 1493 def parent(self):
1494 1494 """
1495 1495 Returns fork parent
1496 1496 """
1497 1497 return self.fork
1498 1498
1499 1499 @property
1500 1500 def just_name(self):
1501 1501 return self.repo_name.split(self.NAME_SEP)[-1]
1502 1502
1503 1503 @property
1504 1504 def groups_with_parents(self):
1505 1505 groups = []
1506 1506 if self.group is None:
1507 1507 return groups
1508 1508
1509 1509 cur_gr = self.group
1510 1510 groups.insert(0, cur_gr)
1511 1511 while 1:
1512 1512 gr = getattr(cur_gr, 'parent_group', None)
1513 1513 cur_gr = cur_gr.parent_group
1514 1514 if gr is None:
1515 1515 break
1516 1516 groups.insert(0, gr)
1517 1517
1518 1518 return groups
1519 1519
1520 1520 @property
1521 1521 def groups_and_repo(self):
1522 1522 return self.groups_with_parents, self
1523 1523
1524 1524 @LazyProperty
1525 1525 def repo_path(self):
1526 1526 """
1527 1527 Returns base full path for that repository means where it actually
1528 1528 exists on a filesystem
1529 1529 """
1530 1530 q = Session().query(RhodeCodeUi).filter(
1531 1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 1533 return q.one().ui_value
1534 1534
1535 1535 @property
1536 1536 def repo_full_path(self):
1537 1537 p = [self.repo_path]
1538 1538 # we need to split the name by / since this is how we store the
1539 1539 # names in the database, but that eventually needs to be converted
1540 1540 # into a valid system path
1541 1541 p += self.repo_name.split(self.NAME_SEP)
1542 1542 return os.path.join(*map(safe_unicode, p))
1543 1543
1544 1544 @property
1545 1545 def cache_keys(self):
1546 1546 """
1547 1547 Returns associated cache keys for that repo
1548 1548 """
1549 1549 return CacheKey.query()\
1550 1550 .filter(CacheKey.cache_args == self.repo_name)\
1551 1551 .order_by(CacheKey.cache_key)\
1552 1552 .all()
1553 1553
1554 1554 def get_new_name(self, repo_name):
1555 1555 """
1556 1556 returns new full repository name based on assigned group and new new
1557 1557
1558 1558 :param group_name:
1559 1559 """
1560 1560 path_prefix = self.group.full_path_splitted if self.group else []
1561 1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1562 1562
1563 1563 @property
1564 1564 def _config(self):
1565 1565 """
1566 1566 Returns db based config object.
1567 1567 """
1568 1568 from rhodecode.lib.utils import make_db_config
1569 1569 return make_db_config(clear_session=False, repo=self)
1570 1570
1571 1571 def permissions(self, with_admins=True, with_owner=True):
1572 1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 1573 q = q.options(joinedload(UserRepoToPerm.repository),
1574 1574 joinedload(UserRepoToPerm.user),
1575 1575 joinedload(UserRepoToPerm.permission),)
1576 1576
1577 1577 # get owners and admins and permissions. We do a trick of re-writing
1578 1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 1579 # has a global reference and changing one object propagates to all
1580 1580 # others. This means if admin is also an owner admin_row that change
1581 1581 # would propagate to both objects
1582 1582 perm_rows = []
1583 1583 for _usr in q.all():
1584 1584 usr = AttributeDict(_usr.user.get_dict())
1585 1585 usr.permission = _usr.permission.permission_name
1586 1586 perm_rows.append(usr)
1587 1587
1588 1588 # filter the perm rows by 'default' first and then sort them by
1589 1589 # admin,write,read,none permissions sorted again alphabetically in
1590 1590 # each group
1591 1591 perm_rows = sorted(perm_rows, key=display_sort)
1592 1592
1593 1593 _admin_perm = 'repository.admin'
1594 1594 owner_row = []
1595 1595 if with_owner:
1596 1596 usr = AttributeDict(self.user.get_dict())
1597 1597 usr.owner_row = True
1598 1598 usr.permission = _admin_perm
1599 1599 owner_row.append(usr)
1600 1600
1601 1601 super_admin_rows = []
1602 1602 if with_admins:
1603 1603 for usr in User.get_all_super_admins():
1604 1604 # if this admin is also owner, don't double the record
1605 1605 if usr.user_id == owner_row[0].user_id:
1606 1606 owner_row[0].admin_row = True
1607 1607 else:
1608 1608 usr = AttributeDict(usr.get_dict())
1609 1609 usr.admin_row = True
1610 1610 usr.permission = _admin_perm
1611 1611 super_admin_rows.append(usr)
1612 1612
1613 1613 return super_admin_rows + owner_row + perm_rows
1614 1614
1615 1615 def permission_user_groups(self):
1616 1616 q = UserGroupRepoToPerm.query().filter(
1617 1617 UserGroupRepoToPerm.repository == self)
1618 1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 1619 joinedload(UserGroupRepoToPerm.users_group),
1620 1620 joinedload(UserGroupRepoToPerm.permission),)
1621 1621
1622 1622 perm_rows = []
1623 1623 for _user_group in q.all():
1624 1624 usr = AttributeDict(_user_group.users_group.get_dict())
1625 1625 usr.permission = _user_group.permission.permission_name
1626 1626 perm_rows.append(usr)
1627 1627
1628 1628 return perm_rows
1629 1629
1630 1630 def get_api_data(self, include_secrets=False):
1631 1631 """
1632 1632 Common function for generating repo api data
1633 1633
1634 1634 :param include_secrets: See :meth:`User.get_api_data`.
1635 1635
1636 1636 """
1637 1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 1638 # move this methods on models level.
1639 1639 from rhodecode.model.settings import SettingsModel
1640 1640
1641 1641 repo = self
1642 1642 _user_id, _time, _reason = self.locked
1643 1643
1644 1644 data = {
1645 1645 'repo_id': repo.repo_id,
1646 1646 'repo_name': repo.repo_name,
1647 1647 'repo_type': repo.repo_type,
1648 1648 'clone_uri': repo.clone_uri or '',
1649 1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 1650 'private': repo.private,
1651 1651 'created_on': repo.created_on,
1652 1652 'description': repo.description,
1653 1653 'landing_rev': repo.landing_rev,
1654 1654 'owner': repo.user.username,
1655 1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 1656 'enable_statistics': repo.enable_statistics,
1657 1657 'enable_locking': repo.enable_locking,
1658 1658 'enable_downloads': repo.enable_downloads,
1659 1659 'last_changeset': repo.changeset_cache,
1660 1660 'locked_by': User.get(_user_id).get_api_data(
1661 1661 include_secrets=include_secrets) if _user_id else None,
1662 1662 'locked_date': time_to_datetime(_time) if _time else None,
1663 1663 'lock_reason': _reason if _reason else None,
1664 1664 }
1665 1665
1666 1666 # TODO: mikhail: should be per-repo settings here
1667 1667 rc_config = SettingsModel().get_all_settings()
1668 1668 repository_fields = str2bool(
1669 1669 rc_config.get('rhodecode_repository_fields'))
1670 1670 if repository_fields:
1671 1671 for f in self.extra_fields:
1672 1672 data[f.field_key_prefixed] = f.field_value
1673 1673
1674 1674 return data
1675 1675
1676 1676 @classmethod
1677 1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 1678 if not lock_time:
1679 1679 lock_time = time.time()
1680 1680 if not lock_reason:
1681 1681 lock_reason = cls.LOCK_AUTOMATIC
1682 1682 repo.locked = [user_id, lock_time, lock_reason]
1683 1683 Session().add(repo)
1684 1684 Session().commit()
1685 1685
1686 1686 @classmethod
1687 1687 def unlock(cls, repo):
1688 1688 repo.locked = None
1689 1689 Session().add(repo)
1690 1690 Session().commit()
1691 1691
1692 1692 @classmethod
1693 1693 def getlock(cls, repo):
1694 1694 return repo.locked
1695 1695
1696 1696 def is_user_lock(self, user_id):
1697 1697 if self.lock[0]:
1698 1698 lock_user_id = safe_int(self.lock[0])
1699 1699 user_id = safe_int(user_id)
1700 1700 # both are ints, and they are equal
1701 1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702 1702
1703 1703 return False
1704 1704
1705 1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 1706 """
1707 1707 Checks locking on this repository, if locking is enabled and lock is
1708 1708 present returns a tuple of make_lock, locked, locked_by.
1709 1709 make_lock can have 3 states None (do nothing) True, make lock
1710 1710 False release lock, This value is later propagated to hooks, which
1711 1711 do the locking. Think about this as signals passed to hooks what to do.
1712 1712
1713 1713 """
1714 1714 # TODO: johbo: This is part of the business logic and should be moved
1715 1715 # into the RepositoryModel.
1716 1716
1717 1717 if action not in ('push', 'pull'):
1718 1718 raise ValueError("Invalid action value: %s" % repr(action))
1719 1719
1720 1720 # defines if locked error should be thrown to user
1721 1721 currently_locked = False
1722 1722 # defines if new lock should be made, tri-state
1723 1723 make_lock = None
1724 1724 repo = self
1725 1725 user = User.get(user_id)
1726 1726
1727 1727 lock_info = repo.locked
1728 1728
1729 1729 if repo and (repo.enable_locking or not only_when_enabled):
1730 1730 if action == 'push':
1731 1731 # check if it's already locked !, if it is compare users
1732 1732 locked_by_user_id = lock_info[0]
1733 1733 if user.user_id == locked_by_user_id:
1734 1734 log.debug(
1735 1735 'Got `push` action from user %s, now unlocking', user)
1736 1736 # unlock if we have push from user who locked
1737 1737 make_lock = False
1738 1738 else:
1739 1739 # we're not the same user who locked, ban with
1740 1740 # code defined in settings (default is 423 HTTP Locked) !
1741 1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 1742 currently_locked = True
1743 1743 elif action == 'pull':
1744 1744 # [0] user [1] date
1745 1745 if lock_info[0] and lock_info[1]:
1746 1746 log.debug('Repo %s is currently locked by %s', repo, user)
1747 1747 currently_locked = True
1748 1748 else:
1749 1749 log.debug('Setting lock on repo %s by %s', repo, user)
1750 1750 make_lock = True
1751 1751
1752 1752 else:
1753 1753 log.debug('Repository %s do not have locking enabled', repo)
1754 1754
1755 1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 1756 make_lock, currently_locked, lock_info)
1757 1757
1758 1758 from rhodecode.lib.auth import HasRepoPermissionAny
1759 1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 1761 # if we don't have at least write permission we cannot make a lock
1762 1762 log.debug('lock state reset back to FALSE due to lack '
1763 1763 'of at least read permission')
1764 1764 make_lock = False
1765 1765
1766 1766 return make_lock, currently_locked, lock_info
1767 1767
1768 1768 @property
1769 1769 def last_db_change(self):
1770 1770 return self.updated_on
1771 1771
1772 1772 @property
1773 1773 def clone_uri_hidden(self):
1774 1774 clone_uri = self.clone_uri
1775 1775 if clone_uri:
1776 1776 import urlobject
1777 1777 url_obj = urlobject.URLObject(clone_uri)
1778 1778 if url_obj.password:
1779 1779 clone_uri = url_obj.with_password('*****')
1780 1780 return clone_uri
1781 1781
1782 1782 def clone_url(self, **override):
1783 1783 qualified_home_url = url('home', qualified=True)
1784 1784
1785 1785 uri_tmpl = None
1786 1786 if 'with_id' in override:
1787 1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 1788 del override['with_id']
1789 1789
1790 1790 if 'uri_tmpl' in override:
1791 1791 uri_tmpl = override['uri_tmpl']
1792 1792 del override['uri_tmpl']
1793 1793
1794 1794 # we didn't override our tmpl from **overrides
1795 1795 if not uri_tmpl:
1796 1796 uri_tmpl = self.DEFAULT_CLONE_URI
1797 1797 try:
1798 1798 from pylons import tmpl_context as c
1799 1799 uri_tmpl = c.clone_uri_tmpl
1800 1800 except Exception:
1801 1801 # in any case if we call this outside of request context,
1802 1802 # ie, not having tmpl_context set up
1803 1803 pass
1804 1804
1805 1805 return get_clone_url(uri_tmpl=uri_tmpl,
1806 1806 qualifed_home_url=qualified_home_url,
1807 1807 repo_name=self.repo_name,
1808 1808 repo_id=self.repo_id, **override)
1809 1809
1810 1810 def set_state(self, state):
1811 1811 self.repo_state = state
1812 1812 Session().add(self)
1813 1813 #==========================================================================
1814 1814 # SCM PROPERTIES
1815 1815 #==========================================================================
1816 1816
1817 1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 1818 return get_commit_safe(
1819 1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820 1820
1821 1821 def get_changeset(self, rev=None, pre_load=None):
1822 1822 warnings.warn("Use get_commit", DeprecationWarning)
1823 1823 commit_id = None
1824 1824 commit_idx = None
1825 1825 if isinstance(rev, basestring):
1826 1826 commit_id = rev
1827 1827 else:
1828 1828 commit_idx = rev
1829 1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 1830 pre_load=pre_load)
1831 1831
1832 1832 def get_landing_commit(self):
1833 1833 """
1834 1834 Returns landing commit, or if that doesn't exist returns the tip
1835 1835 """
1836 1836 _rev_type, _rev = self.landing_rev
1837 1837 commit = self.get_commit(_rev)
1838 1838 if isinstance(commit, EmptyCommit):
1839 1839 return self.get_commit()
1840 1840 return commit
1841 1841
1842 1842 def update_commit_cache(self, cs_cache=None, config=None):
1843 1843 """
1844 1844 Update cache of last changeset for repository, keys should be::
1845 1845
1846 1846 short_id
1847 1847 raw_id
1848 1848 revision
1849 1849 parents
1850 1850 message
1851 1851 date
1852 1852 author
1853 1853
1854 1854 :param cs_cache:
1855 1855 """
1856 1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 1857 if cs_cache is None:
1858 1858 # use no-cache version here
1859 1859 scm_repo = self.scm_instance(cache=False, config=config)
1860 1860 if scm_repo:
1861 1861 cs_cache = scm_repo.get_commit(
1862 1862 pre_load=["author", "date", "message", "parents"])
1863 1863 else:
1864 1864 cs_cache = EmptyCommit()
1865 1865
1866 1866 if isinstance(cs_cache, BaseChangeset):
1867 1867 cs_cache = cs_cache.__json__()
1868 1868
1869 1869 def is_outdated(new_cs_cache):
1870 1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 1872 return True
1873 1873 return False
1874 1874
1875 1875 # check if we have maybe already latest cached revision
1876 1876 if is_outdated(cs_cache) or not self.changeset_cache:
1877 1877 _default = datetime.datetime.fromtimestamp(0)
1878 1878 last_change = cs_cache.get('date') or _default
1879 1879 log.debug('updated repo %s with new cs cache %s',
1880 1880 self.repo_name, cs_cache)
1881 1881 self.updated_on = last_change
1882 1882 self.changeset_cache = cs_cache
1883 1883 Session().add(self)
1884 1884 Session().commit()
1885 1885 else:
1886 1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 1887 'commit already with latest changes', self.repo_name)
1888 1888
1889 1889 @property
1890 1890 def tip(self):
1891 1891 return self.get_commit('tip')
1892 1892
1893 1893 @property
1894 1894 def author(self):
1895 1895 return self.tip.author
1896 1896
1897 1897 @property
1898 1898 def last_change(self):
1899 1899 return self.scm_instance().last_change
1900 1900
1901 1901 def get_comments(self, revisions=None):
1902 1902 """
1903 1903 Returns comments for this repository grouped by revisions
1904 1904
1905 1905 :param revisions: filter query by revisions only
1906 1906 """
1907 1907 cmts = ChangesetComment.query()\
1908 1908 .filter(ChangesetComment.repo == self)
1909 1909 if revisions:
1910 1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 1911 grouped = collections.defaultdict(list)
1912 1912 for cmt in cmts.all():
1913 1913 grouped[cmt.revision].append(cmt)
1914 1914 return grouped
1915 1915
1916 1916 def statuses(self, revisions=None):
1917 1917 """
1918 1918 Returns statuses for this repository
1919 1919
1920 1920 :param revisions: list of revisions to get statuses for
1921 1921 """
1922 1922 statuses = ChangesetStatus.query()\
1923 1923 .filter(ChangesetStatus.repo == self)\
1924 1924 .filter(ChangesetStatus.version == 0)
1925 1925
1926 1926 if revisions:
1927 1927 # Try doing the filtering in chunks to avoid hitting limits
1928 1928 size = 500
1929 1929 status_results = []
1930 1930 for chunk in xrange(0, len(revisions), size):
1931 1931 status_results += statuses.filter(
1932 1932 ChangesetStatus.revision.in_(
1933 1933 revisions[chunk: chunk+size])
1934 1934 ).all()
1935 1935 else:
1936 1936 status_results = statuses.all()
1937 1937
1938 1938 grouped = {}
1939 1939
1940 1940 # maybe we have open new pullrequest without a status?
1941 1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 1944 for rev in pr.revisions:
1945 1945 pr_id = pr.pull_request_id
1946 1946 pr_repo = pr.target_repo.repo_name
1947 1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948 1948
1949 1949 for stat in status_results:
1950 1950 pr_id = pr_repo = None
1951 1951 if stat.pull_request:
1952 1952 pr_id = stat.pull_request.pull_request_id
1953 1953 pr_repo = stat.pull_request.target_repo.repo_name
1954 1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 1955 pr_id, pr_repo]
1956 1956 return grouped
1957 1957
1958 1958 # ==========================================================================
1959 1959 # SCM CACHE INSTANCE
1960 1960 # ==========================================================================
1961 1961
1962 1962 def scm_instance(self, **kwargs):
1963 1963 import rhodecode
1964 1964
1965 1965 # Passing a config will not hit the cache currently only used
1966 1966 # for repo2dbmapper
1967 1967 config = kwargs.pop('config', None)
1968 1968 cache = kwargs.pop('cache', None)
1969 1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 1970 # if cache is NOT defined use default global, else we have a full
1971 1971 # control over cache behaviour
1972 1972 if cache is None and full_cache and not config:
1973 1973 return self._get_instance_cached()
1974 1974 return self._get_instance(cache=bool(cache), config=config)
1975 1975
1976 1976 def _get_instance_cached(self):
1977 1977 @cache_region('long_term')
1978 1978 def _get_repo(cache_key):
1979 1979 return self._get_instance()
1980 1980
1981 1981 invalidator_context = CacheKey.repo_context_cache(
1982 1982 _get_repo, self.repo_name, None, thread_scoped=True)
1983 1983
1984 1984 with invalidator_context as context:
1985 1985 context.invalidate()
1986 1986 repo = context.compute()
1987 1987
1988 1988 return repo
1989 1989
1990 1990 def _get_instance(self, cache=True, config=None):
1991 1991 config = config or self._config
1992 1992 custom_wire = {
1993 1993 'cache': cache # controls the vcs.remote cache
1994 1994 }
1995 1995
1996 1996 repo = get_vcs_instance(
1997 1997 repo_path=safe_str(self.repo_full_path),
1998 1998 config=config,
1999 1999 with_wire=custom_wire,
2000 2000 create=False)
2001 2001
2002 2002 return repo
2003 2003
2004 2004 def __json__(self):
2005 2005 return {'landing_rev': self.landing_rev}
2006 2006
2007 2007 def get_dict(self):
2008 2008
2009 2009 # Since we transformed `repo_name` to a hybrid property, we need to
2010 2010 # keep compatibility with the code which uses `repo_name` field.
2011 2011
2012 2012 result = super(Repository, self).get_dict()
2013 2013 result['repo_name'] = result.pop('_repo_name', None)
2014 2014 return result
2015 2015
2016 2016
2017 2017 class RepoGroup(Base, BaseModel):
2018 2018 __tablename__ = 'groups'
2019 2019 __table_args__ = (
2020 2020 UniqueConstraint('group_name', 'group_parent_id'),
2021 2021 CheckConstraint('group_id != group_parent_id'),
2022 2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 2024 )
2025 2025 __mapper_args__ = {'order_by': 'group_name'}
2026 2026
2027 2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028 2028
2029 2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036 2036
2037 2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2038 2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2039 2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2040 2040 user = relationship('User')
2041 2041 integrations = relationship('Integration',
2042 2042 cascade="all, delete, delete-orphan")
2043 2043
2044 2044 def __init__(self, group_name='', parent_group=None):
2045 2045 self.group_name = group_name
2046 2046 self.parent_group = parent_group
2047 2047
2048 2048 def __unicode__(self):
2049 2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2050 2050 self.group_name)
2051 2051
2052 2052 @classmethod
2053 2053 def _generate_choice(cls, repo_group):
2054 2054 from webhelpers.html import literal as _literal
2055 2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2056 2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2057 2057
2058 2058 @classmethod
2059 2059 def groups_choices(cls, groups=None, show_empty_group=True):
2060 2060 if not groups:
2061 2061 groups = cls.query().all()
2062 2062
2063 2063 repo_groups = []
2064 2064 if show_empty_group:
2065 2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2066 2066
2067 2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2068 2068
2069 2069 repo_groups = sorted(
2070 2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2071 2071 return repo_groups
2072 2072
2073 2073 @classmethod
2074 2074 def url_sep(cls):
2075 2075 return URL_SEP
2076 2076
2077 2077 @classmethod
2078 2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2079 2079 if case_insensitive:
2080 2080 gr = cls.query().filter(func.lower(cls.group_name)
2081 2081 == func.lower(group_name))
2082 2082 else:
2083 2083 gr = cls.query().filter(cls.group_name == group_name)
2084 2084 if cache:
2085 2085 gr = gr.options(FromCache(
2086 2086 "sql_cache_short",
2087 2087 "get_group_%s" % _hash_key(group_name)))
2088 2088 return gr.scalar()
2089 2089
2090 2090 @classmethod
2091 2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2092 2092 case_insensitive=True):
2093 2093 q = RepoGroup.query()
2094 2094
2095 2095 if not isinstance(user_id, Optional):
2096 2096 q = q.filter(RepoGroup.user_id == user_id)
2097 2097
2098 2098 if not isinstance(group_id, Optional):
2099 2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2100 2100
2101 2101 if case_insensitive:
2102 2102 q = q.order_by(func.lower(RepoGroup.group_name))
2103 2103 else:
2104 2104 q = q.order_by(RepoGroup.group_name)
2105 2105 return q.all()
2106 2106
2107 2107 @property
2108 2108 def parents(self):
2109 2109 parents_recursion_limit = 10
2110 2110 groups = []
2111 2111 if self.parent_group is None:
2112 2112 return groups
2113 2113 cur_gr = self.parent_group
2114 2114 groups.insert(0, cur_gr)
2115 2115 cnt = 0
2116 2116 while 1:
2117 2117 cnt += 1
2118 2118 gr = getattr(cur_gr, 'parent_group', None)
2119 2119 cur_gr = cur_gr.parent_group
2120 2120 if gr is None:
2121 2121 break
2122 2122 if cnt == parents_recursion_limit:
2123 2123 # this will prevent accidental infinit loops
2124 2124 log.error(('more than %s parents found for group %s, stopping '
2125 2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2126 2126 break
2127 2127
2128 2128 groups.insert(0, gr)
2129 2129 return groups
2130 2130
2131 2131 @property
2132 2132 def children(self):
2133 2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2134 2134
2135 2135 @property
2136 2136 def name(self):
2137 2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2138 2138
2139 2139 @property
2140 2140 def full_path(self):
2141 2141 return self.group_name
2142 2142
2143 2143 @property
2144 2144 def full_path_splitted(self):
2145 2145 return self.group_name.split(RepoGroup.url_sep())
2146 2146
2147 2147 @property
2148 2148 def repositories(self):
2149 2149 return Repository.query()\
2150 2150 .filter(Repository.group == self)\
2151 2151 .order_by(Repository.repo_name)
2152 2152
2153 2153 @property
2154 2154 def repositories_recursive_count(self):
2155 2155 cnt = self.repositories.count()
2156 2156
2157 2157 def children_count(group):
2158 2158 cnt = 0
2159 2159 for child in group.children:
2160 2160 cnt += child.repositories.count()
2161 2161 cnt += children_count(child)
2162 2162 return cnt
2163 2163
2164 2164 return cnt + children_count(self)
2165 2165
2166 2166 def _recursive_objects(self, include_repos=True):
2167 2167 all_ = []
2168 2168
2169 2169 def _get_members(root_gr):
2170 2170 if include_repos:
2171 2171 for r in root_gr.repositories:
2172 2172 all_.append(r)
2173 2173 childs = root_gr.children.all()
2174 2174 if childs:
2175 2175 for gr in childs:
2176 2176 all_.append(gr)
2177 2177 _get_members(gr)
2178 2178
2179 2179 _get_members(self)
2180 2180 return [self] + all_
2181 2181
2182 2182 def recursive_groups_and_repos(self):
2183 2183 """
2184 2184 Recursive return all groups, with repositories in those groups
2185 2185 """
2186 2186 return self._recursive_objects()
2187 2187
2188 2188 def recursive_groups(self):
2189 2189 """
2190 2190 Returns all children groups for this group including children of children
2191 2191 """
2192 2192 return self._recursive_objects(include_repos=False)
2193 2193
2194 2194 def get_new_name(self, group_name):
2195 2195 """
2196 2196 returns new full group name based on parent and new name
2197 2197
2198 2198 :param group_name:
2199 2199 """
2200 2200 path_prefix = (self.parent_group.full_path_splitted if
2201 2201 self.parent_group else [])
2202 2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2203 2203
2204 2204 def permissions(self, with_admins=True, with_owner=True):
2205 2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2206 2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2207 2207 joinedload(UserRepoGroupToPerm.user),
2208 2208 joinedload(UserRepoGroupToPerm.permission),)
2209 2209
2210 2210 # get owners and admins and permissions. We do a trick of re-writing
2211 2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2212 2212 # has a global reference and changing one object propagates to all
2213 2213 # others. This means if admin is also an owner admin_row that change
2214 2214 # would propagate to both objects
2215 2215 perm_rows = []
2216 2216 for _usr in q.all():
2217 2217 usr = AttributeDict(_usr.user.get_dict())
2218 2218 usr.permission = _usr.permission.permission_name
2219 2219 perm_rows.append(usr)
2220 2220
2221 2221 # filter the perm rows by 'default' first and then sort them by
2222 2222 # admin,write,read,none permissions sorted again alphabetically in
2223 2223 # each group
2224 2224 perm_rows = sorted(perm_rows, key=display_sort)
2225 2225
2226 2226 _admin_perm = 'group.admin'
2227 2227 owner_row = []
2228 2228 if with_owner:
2229 2229 usr = AttributeDict(self.user.get_dict())
2230 2230 usr.owner_row = True
2231 2231 usr.permission = _admin_perm
2232 2232 owner_row.append(usr)
2233 2233
2234 2234 super_admin_rows = []
2235 2235 if with_admins:
2236 2236 for usr in User.get_all_super_admins():
2237 2237 # if this admin is also owner, don't double the record
2238 2238 if usr.user_id == owner_row[0].user_id:
2239 2239 owner_row[0].admin_row = True
2240 2240 else:
2241 2241 usr = AttributeDict(usr.get_dict())
2242 2242 usr.admin_row = True
2243 2243 usr.permission = _admin_perm
2244 2244 super_admin_rows.append(usr)
2245 2245
2246 2246 return super_admin_rows + owner_row + perm_rows
2247 2247
2248 2248 def permission_user_groups(self):
2249 2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2250 2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2251 2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2252 2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2253 2253
2254 2254 perm_rows = []
2255 2255 for _user_group in q.all():
2256 2256 usr = AttributeDict(_user_group.users_group.get_dict())
2257 2257 usr.permission = _user_group.permission.permission_name
2258 2258 perm_rows.append(usr)
2259 2259
2260 2260 return perm_rows
2261 2261
2262 2262 def get_api_data(self):
2263 2263 """
2264 2264 Common function for generating api data
2265 2265
2266 2266 """
2267 2267 group = self
2268 2268 data = {
2269 2269 'group_id': group.group_id,
2270 2270 'group_name': group.group_name,
2271 2271 'group_description': group.group_description,
2272 2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2273 2273 'repositories': [x.repo_name for x in group.repositories],
2274 2274 'owner': group.user.username,
2275 2275 }
2276 2276 return data
2277 2277
2278 2278
2279 2279 class Permission(Base, BaseModel):
2280 2280 __tablename__ = 'permissions'
2281 2281 __table_args__ = (
2282 2282 Index('p_perm_name_idx', 'permission_name'),
2283 2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2284 2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2285 2285 )
2286 2286 PERMS = [
2287 2287 ('hg.admin', _('RhodeCode Super Administrator')),
2288 2288
2289 2289 ('repository.none', _('Repository no access')),
2290 2290 ('repository.read', _('Repository read access')),
2291 2291 ('repository.write', _('Repository write access')),
2292 2292 ('repository.admin', _('Repository admin access')),
2293 2293
2294 2294 ('group.none', _('Repository group no access')),
2295 2295 ('group.read', _('Repository group read access')),
2296 2296 ('group.write', _('Repository group write access')),
2297 2297 ('group.admin', _('Repository group admin access')),
2298 2298
2299 2299 ('usergroup.none', _('User group no access')),
2300 2300 ('usergroup.read', _('User group read access')),
2301 2301 ('usergroup.write', _('User group write access')),
2302 2302 ('usergroup.admin', _('User group admin access')),
2303 2303
2304 2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2305 2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2306 2306
2307 2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2308 2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2309 2309
2310 2310 ('hg.create.none', _('Repository creation disabled')),
2311 2311 ('hg.create.repository', _('Repository creation enabled')),
2312 2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2313 2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2314 2314
2315 2315 ('hg.fork.none', _('Repository forking disabled')),
2316 2316 ('hg.fork.repository', _('Repository forking enabled')),
2317 2317
2318 2318 ('hg.register.none', _('Registration disabled')),
2319 2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2320 2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2321 2321
2322 2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2323 2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2324 2324
2325 2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2326 2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2327 2327 ]
2328 2328
2329 2329 # definition of system default permissions for DEFAULT user
2330 2330 DEFAULT_USER_PERMISSIONS = [
2331 2331 'repository.read',
2332 2332 'group.read',
2333 2333 'usergroup.read',
2334 2334 'hg.create.repository',
2335 2335 'hg.repogroup.create.false',
2336 2336 'hg.usergroup.create.false',
2337 2337 'hg.create.write_on_repogroup.true',
2338 2338 'hg.fork.repository',
2339 2339 'hg.register.manual_activate',
2340 2340 'hg.extern_activate.auto',
2341 2341 'hg.inherit_default_perms.true',
2342 2342 ]
2343 2343
2344 2344 # defines which permissions are more important higher the more important
2345 2345 # Weight defines which permissions are more important.
2346 2346 # The higher number the more important.
2347 2347 PERM_WEIGHTS = {
2348 2348 'repository.none': 0,
2349 2349 'repository.read': 1,
2350 2350 'repository.write': 3,
2351 2351 'repository.admin': 4,
2352 2352
2353 2353 'group.none': 0,
2354 2354 'group.read': 1,
2355 2355 'group.write': 3,
2356 2356 'group.admin': 4,
2357 2357
2358 2358 'usergroup.none': 0,
2359 2359 'usergroup.read': 1,
2360 2360 'usergroup.write': 3,
2361 2361 'usergroup.admin': 4,
2362 2362
2363 2363 'hg.repogroup.create.false': 0,
2364 2364 'hg.repogroup.create.true': 1,
2365 2365
2366 2366 'hg.usergroup.create.false': 0,
2367 2367 'hg.usergroup.create.true': 1,
2368 2368
2369 2369 'hg.fork.none': 0,
2370 2370 'hg.fork.repository': 1,
2371 2371 'hg.create.none': 0,
2372 2372 'hg.create.repository': 1
2373 2373 }
2374 2374
2375 2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378 2378
2379 2379 def __unicode__(self):
2380 2380 return u"<%s('%s:%s')>" % (
2381 2381 self.__class__.__name__, self.permission_id, self.permission_name
2382 2382 )
2383 2383
2384 2384 @classmethod
2385 2385 def get_by_key(cls, key):
2386 2386 return cls.query().filter(cls.permission_name == key).scalar()
2387 2387
2388 2388 @classmethod
2389 2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 2393 .filter(UserRepoToPerm.user_id == user_id)
2394 2394 if repo_id:
2395 2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 2396 return q.all()
2397 2397
2398 2398 @classmethod
2399 2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 2401 .join(
2402 2402 Permission,
2403 2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 2404 .join(
2405 2405 Repository,
2406 2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 2407 .join(
2408 2408 UserGroup,
2409 2409 UserGroupRepoToPerm.users_group_id ==
2410 2410 UserGroup.users_group_id)\
2411 2411 .join(
2412 2412 UserGroupMember,
2413 2413 UserGroupRepoToPerm.users_group_id ==
2414 2414 UserGroupMember.users_group_id)\
2415 2415 .filter(
2416 2416 UserGroupMember.user_id == user_id,
2417 2417 UserGroup.users_group_active == true())
2418 2418 if repo_id:
2419 2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 2420 return q.all()
2421 2421
2422 2422 @classmethod
2423 2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 2428 if repo_group_id:
2429 2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 2430 return q.all()
2431 2431
2432 2432 @classmethod
2433 2433 def get_default_group_perms_from_user_group(
2434 2434 cls, user_id, repo_group_id=None):
2435 2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 2436 .join(
2437 2437 Permission,
2438 2438 UserGroupRepoGroupToPerm.permission_id ==
2439 2439 Permission.permission_id)\
2440 2440 .join(
2441 2441 RepoGroup,
2442 2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 2443 .join(
2444 2444 UserGroup,
2445 2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 2446 UserGroup.users_group_id)\
2447 2447 .join(
2448 2448 UserGroupMember,
2449 2449 UserGroupRepoGroupToPerm.users_group_id ==
2450 2450 UserGroupMember.users_group_id)\
2451 2451 .filter(
2452 2452 UserGroupMember.user_id == user_id,
2453 2453 UserGroup.users_group_active == true())
2454 2454 if repo_group_id:
2455 2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 2456 return q.all()
2457 2457
2458 2458 @classmethod
2459 2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 2463 .filter(UserUserGroupToPerm.user_id == user_id)
2464 2464 if user_group_id:
2465 2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 2466 return q.all()
2467 2467
2468 2468 @classmethod
2469 2469 def get_default_user_group_perms_from_user_group(
2470 2470 cls, user_id, user_group_id=None):
2471 2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 2473 .join(
2474 2474 Permission,
2475 2475 UserGroupUserGroupToPerm.permission_id ==
2476 2476 Permission.permission_id)\
2477 2477 .join(
2478 2478 TargetUserGroup,
2479 2479 UserGroupUserGroupToPerm.target_user_group_id ==
2480 2480 TargetUserGroup.users_group_id)\
2481 2481 .join(
2482 2482 UserGroup,
2483 2483 UserGroupUserGroupToPerm.user_group_id ==
2484 2484 UserGroup.users_group_id)\
2485 2485 .join(
2486 2486 UserGroupMember,
2487 2487 UserGroupUserGroupToPerm.user_group_id ==
2488 2488 UserGroupMember.users_group_id)\
2489 2489 .filter(
2490 2490 UserGroupMember.user_id == user_id,
2491 2491 UserGroup.users_group_active == true())
2492 2492 if user_group_id:
2493 2493 q = q.filter(
2494 2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495 2495
2496 2496 return q.all()
2497 2497
2498 2498
2499 2499 class UserRepoToPerm(Base, BaseModel):
2500 2500 __tablename__ = 'repo_to_perm'
2501 2501 __table_args__ = (
2502 2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 2505 )
2506 2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510 2510
2511 2511 user = relationship('User')
2512 2512 repository = relationship('Repository')
2513 2513 permission = relationship('Permission')
2514 2514
2515 2515 @classmethod
2516 2516 def create(cls, user, repository, permission):
2517 2517 n = cls()
2518 2518 n.user = user
2519 2519 n.repository = repository
2520 2520 n.permission = permission
2521 2521 Session().add(n)
2522 2522 return n
2523 2523
2524 2524 def __unicode__(self):
2525 2525 return u'<%s => %s >' % (self.user, self.repository)
2526 2526
2527 2527
2528 2528 class UserUserGroupToPerm(Base, BaseModel):
2529 2529 __tablename__ = 'user_user_group_to_perm'
2530 2530 __table_args__ = (
2531 2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 2534 )
2535 2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2536 2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539 2539
2540 2540 user = relationship('User')
2541 2541 user_group = relationship('UserGroup')
2542 2542 permission = relationship('Permission')
2543 2543
2544 2544 @classmethod
2545 2545 def create(cls, user, user_group, permission):
2546 2546 n = cls()
2547 2547 n.user = user
2548 2548 n.user_group = user_group
2549 2549 n.permission = permission
2550 2550 Session().add(n)
2551 2551 return n
2552 2552
2553 2553 def __unicode__(self):
2554 2554 return u'<%s => %s >' % (self.user, self.user_group)
2555 2555
2556 2556
2557 2557 class UserToPerm(Base, BaseModel):
2558 2558 __tablename__ = 'user_to_perm'
2559 2559 __table_args__ = (
2560 2560 UniqueConstraint('user_id', 'permission_id'),
2561 2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 2563 )
2564 2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567 2567
2568 2568 user = relationship('User')
2569 2569 permission = relationship('Permission', lazy='joined')
2570 2570
2571 2571 def __unicode__(self):
2572 2572 return u'<%s => %s >' % (self.user, self.permission)
2573 2573
2574 2574
2575 2575 class UserGroupRepoToPerm(Base, BaseModel):
2576 2576 __tablename__ = 'users_group_repo_to_perm'
2577 2577 __table_args__ = (
2578 2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 2581 )
2582 2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586 2586
2587 2587 users_group = relationship('UserGroup')
2588 2588 permission = relationship('Permission')
2589 2589 repository = relationship('Repository')
2590 2590
2591 2591 @classmethod
2592 2592 def create(cls, users_group, repository, permission):
2593 2593 n = cls()
2594 2594 n.users_group = users_group
2595 2595 n.repository = repository
2596 2596 n.permission = permission
2597 2597 Session().add(n)
2598 2598 return n
2599 2599
2600 2600 def __unicode__(self):
2601 2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602 2602
2603 2603
2604 2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 2605 __tablename__ = 'user_group_user_group_to_perm'
2606 2606 __table_args__ = (
2607 2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 2608 CheckConstraint('target_user_group_id != user_group_id'),
2609 2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 2611 )
2612 2612 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2613 2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616 2616
2617 2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 2619 permission = relationship('Permission')
2620 2620
2621 2621 @classmethod
2622 2622 def create(cls, target_user_group, user_group, permission):
2623 2623 n = cls()
2624 2624 n.target_user_group = target_user_group
2625 2625 n.user_group = user_group
2626 2626 n.permission = permission
2627 2627 Session().add(n)
2628 2628 return n
2629 2629
2630 2630 def __unicode__(self):
2631 2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632 2632
2633 2633
2634 2634 class UserGroupToPerm(Base, BaseModel):
2635 2635 __tablename__ = 'users_group_to_perm'
2636 2636 __table_args__ = (
2637 2637 UniqueConstraint('users_group_id', 'permission_id',),
2638 2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 2640 )
2641 2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644 2644
2645 2645 users_group = relationship('UserGroup')
2646 2646 permission = relationship('Permission')
2647 2647
2648 2648
2649 2649 class UserRepoGroupToPerm(Base, BaseModel):
2650 2650 __tablename__ = 'user_repo_group_to_perm'
2651 2651 __table_args__ = (
2652 2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 2655 )
2656 2656
2657 2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661 2661
2662 2662 user = relationship('User')
2663 2663 group = relationship('RepoGroup')
2664 2664 permission = relationship('Permission')
2665 2665
2666 2666 @classmethod
2667 2667 def create(cls, user, repository_group, permission):
2668 2668 n = cls()
2669 2669 n.user = user
2670 2670 n.group = repository_group
2671 2671 n.permission = permission
2672 2672 Session().add(n)
2673 2673 return n
2674 2674
2675 2675
2676 2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 2677 __tablename__ = 'users_group_repo_group_to_perm'
2678 2678 __table_args__ = (
2679 2679 UniqueConstraint('users_group_id', 'group_id'),
2680 2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 2682 )
2683 2683
2684 2684 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)
2685 2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688 2688
2689 2689 users_group = relationship('UserGroup')
2690 2690 permission = relationship('Permission')
2691 2691 group = relationship('RepoGroup')
2692 2692
2693 2693 @classmethod
2694 2694 def create(cls, user_group, repository_group, permission):
2695 2695 n = cls()
2696 2696 n.users_group = user_group
2697 2697 n.group = repository_group
2698 2698 n.permission = permission
2699 2699 Session().add(n)
2700 2700 return n
2701 2701
2702 2702 def __unicode__(self):
2703 2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704 2704
2705 2705
2706 2706 class Statistics(Base, BaseModel):
2707 2707 __tablename__ = 'statistics'
2708 2708 __table_args__ = (
2709 2709 UniqueConstraint('repository_id'),
2710 2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 2712 )
2713 2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719 2719
2720 2720 repository = relationship('Repository', single_parent=True)
2721 2721
2722 2722
2723 2723 class UserFollowing(Base, BaseModel):
2724 2724 __tablename__ = 'user_followings'
2725 2725 __table_args__ = (
2726 2726 UniqueConstraint('user_id', 'follows_repository_id'),
2727 2727 UniqueConstraint('user_id', 'follows_user_id'),
2728 2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 2730 )
2731 2731
2732 2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737 2737
2738 2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739 2739
2740 2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742 2742
2743 2743 @classmethod
2744 2744 def get_repo_followers(cls, repo_id):
2745 2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2746 2746
2747 2747
2748 2748 class CacheKey(Base, BaseModel):
2749 2749 __tablename__ = 'cache_invalidation'
2750 2750 __table_args__ = (
2751 2751 UniqueConstraint('cache_key'),
2752 2752 Index('key_idx', 'cache_key'),
2753 2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 2755 )
2756 2756 CACHE_TYPE_ATOM = 'ATOM'
2757 2757 CACHE_TYPE_RSS = 'RSS'
2758 2758 CACHE_TYPE_README = 'README'
2759 2759
2760 2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764 2764
2765 2765 def __init__(self, cache_key, cache_args=''):
2766 2766 self.cache_key = cache_key
2767 2767 self.cache_args = cache_args
2768 2768 self.cache_active = False
2769 2769
2770 2770 def __unicode__(self):
2771 2771 return u"<%s('%s:%s[%s]')>" % (
2772 2772 self.__class__.__name__,
2773 2773 self.cache_id, self.cache_key, self.cache_active)
2774 2774
2775 2775 def _cache_key_partition(self):
2776 2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 2777 return prefix, repo_name, suffix
2778 2778
2779 2779 def get_prefix(self):
2780 2780 """
2781 2781 Try to extract prefix from existing cache key. The key could consist
2782 2782 of prefix, repo_name, suffix
2783 2783 """
2784 2784 # this returns prefix, repo_name, suffix
2785 2785 return self._cache_key_partition()[0]
2786 2786
2787 2787 def get_suffix(self):
2788 2788 """
2789 2789 get suffix that might have been used in _get_cache_key to
2790 2790 generate self.cache_key. Only used for informational purposes
2791 2791 in repo_edit.html.
2792 2792 """
2793 2793 # prefix, repo_name, suffix
2794 2794 return self._cache_key_partition()[2]
2795 2795
2796 2796 @classmethod
2797 2797 def delete_all_cache(cls):
2798 2798 """
2799 2799 Delete all cache keys from database.
2800 2800 Should only be run when all instances are down and all entries
2801 2801 thus stale.
2802 2802 """
2803 2803 cls.query().delete()
2804 2804 Session().commit()
2805 2805
2806 2806 @classmethod
2807 2807 def get_cache_key(cls, repo_name, cache_type):
2808 2808 """
2809 2809
2810 2810 Generate a cache key for this process of RhodeCode instance.
2811 2811 Prefix most likely will be process id or maybe explicitly set
2812 2812 instance_id from .ini file.
2813 2813 """
2814 2814 import rhodecode
2815 2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816 2816
2817 2817 repo_as_unicode = safe_unicode(repo_name)
2818 2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 2819 if cache_type else repo_as_unicode
2820 2820
2821 2821 return u'{}{}'.format(prefix, key)
2822 2822
2823 2823 @classmethod
2824 2824 def set_invalidate(cls, repo_name, delete=False):
2825 2825 """
2826 2826 Mark all caches of a repo as invalid in the database.
2827 2827 """
2828 2828
2829 2829 try:
2830 2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 2831 if delete:
2832 2832 log.debug('cache objects deleted for repo %s',
2833 2833 safe_str(repo_name))
2834 2834 qry.delete()
2835 2835 else:
2836 2836 log.debug('cache objects marked as invalid for repo %s',
2837 2837 safe_str(repo_name))
2838 2838 qry.update({"cache_active": False})
2839 2839
2840 2840 Session().commit()
2841 2841 except Exception:
2842 2842 log.exception(
2843 2843 'Cache key invalidation failed for repository %s',
2844 2844 safe_str(repo_name))
2845 2845 Session().rollback()
2846 2846
2847 2847 @classmethod
2848 2848 def get_active_cache(cls, cache_key):
2849 2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 2850 if inv_obj:
2851 2851 return inv_obj
2852 2852 return None
2853 2853
2854 2854 @classmethod
2855 2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 2856 thread_scoped=False):
2857 2857 """
2858 2858 @cache_region('long_term')
2859 2859 def _heavy_calculation(cache_key):
2860 2860 return 'result'
2861 2861
2862 2862 cache_context = CacheKey.repo_context_cache(
2863 2863 _heavy_calculation, repo_name, cache_type)
2864 2864
2865 2865 with cache_context as context:
2866 2866 context.invalidate()
2867 2867 computed = context.compute()
2868 2868
2869 2869 assert computed == 'result'
2870 2870 """
2871 2871 from rhodecode.lib import caches
2872 2872 return caches.InvalidationContext(
2873 2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874 2874
2875 2875
2876 2876 class ChangesetComment(Base, BaseModel):
2877 2877 __tablename__ = 'changeset_comments'
2878 2878 __table_args__ = (
2879 2879 Index('cc_revision_idx', 'revision'),
2880 2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 2882 )
2883 2883
2884 2884 COMMENT_OUTDATED = u'comment_outdated'
2885 2885
2886 2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 2888 revision = Column('revision', String(40), nullable=True)
2889 2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 2891 line_no = Column('line_no', Unicode(10), nullable=True)
2892 2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 2898 renderer = Column('renderer', Unicode(64), nullable=True)
2899 2899 display_state = Column('display_state', Unicode(128), nullable=True)
2900 2900
2901 2901 author = relationship('User', lazy='joined')
2902 2902 repo = relationship('Repository')
2903 2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 2904 pull_request = relationship('PullRequest', lazy='joined')
2905 2905 pull_request_version = relationship('PullRequestVersion')
2906 2906
2907 2907 @classmethod
2908 2908 def get_users(cls, revision=None, pull_request_id=None):
2909 2909 """
2910 2910 Returns user associated with this ChangesetComment. ie those
2911 2911 who actually commented
2912 2912
2913 2913 :param cls:
2914 2914 :param revision:
2915 2915 """
2916 2916 q = Session().query(User)\
2917 2917 .join(ChangesetComment.author)
2918 2918 if revision:
2919 2919 q = q.filter(cls.revision == revision)
2920 2920 elif pull_request_id:
2921 2921 q = q.filter(cls.pull_request_id == pull_request_id)
2922 2922 return q.all()
2923 2923
2924 2924 def render(self, mentions=False):
2925 2925 from rhodecode.lib import helpers as h
2926 2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927 2927
2928 2928 def __repr__(self):
2929 2929 if self.comment_id:
2930 2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 2931 else:
2932 2932 return '<DB:ChangesetComment at %#x>' % id(self)
2933 2933
2934 2934
2935 2935 class ChangesetStatus(Base, BaseModel):
2936 2936 __tablename__ = 'changeset_statuses'
2937 2937 __table_args__ = (
2938 2938 Index('cs_revision_idx', 'revision'),
2939 2939 Index('cs_version_idx', 'version'),
2940 2940 UniqueConstraint('repo_id', 'revision', 'version'),
2941 2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 2943 )
2944 2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 2945 STATUS_APPROVED = 'approved'
2946 2946 STATUS_REJECTED = 'rejected'
2947 2947 STATUS_UNDER_REVIEW = 'under_review'
2948 2948
2949 2949 STATUSES = [
2950 2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 2951 (STATUS_APPROVED, _("Approved")),
2952 2952 (STATUS_REJECTED, _("Rejected")),
2953 2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 2954 ]
2955 2955
2956 2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 2959 revision = Column('revision', String(40), nullable=False)
2960 2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 2963 version = Column('version', Integer(), nullable=False, default=0)
2964 2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965 2965
2966 2966 author = relationship('User', lazy='joined')
2967 2967 repo = relationship('Repository')
2968 2968 comment = relationship('ChangesetComment', lazy='joined')
2969 2969 pull_request = relationship('PullRequest', lazy='joined')
2970 2970
2971 2971 def __unicode__(self):
2972 2972 return u"<%s('%s[%s]:%s')>" % (
2973 2973 self.__class__.__name__,
2974 2974 self.status, self.version, self.author
2975 2975 )
2976 2976
2977 2977 @classmethod
2978 2978 def get_status_lbl(cls, value):
2979 2979 return dict(cls.STATUSES).get(value)
2980 2980
2981 2981 @property
2982 2982 def status_lbl(self):
2983 2983 return ChangesetStatus.get_status_lbl(self.status)
2984 2984
2985 2985
2986 2986 class _PullRequestBase(BaseModel):
2987 2987 """
2988 2988 Common attributes of pull request and version entries.
2989 2989 """
2990 2990
2991 2991 # .status values
2992 2992 STATUS_NEW = u'new'
2993 2993 STATUS_OPEN = u'open'
2994 2994 STATUS_CLOSED = u'closed'
2995 2995
2996 2996 title = Column('title', Unicode(255), nullable=True)
2997 2997 description = Column(
2998 2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 2999 nullable=True)
3000 3000 # new/open/closed status of pull request (not approve/reject/etc)
3001 3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 3002 created_on = Column(
3003 3003 'created_on', DateTime(timezone=False), nullable=False,
3004 3004 default=datetime.datetime.now)
3005 3005 updated_on = Column(
3006 3006 'updated_on', DateTime(timezone=False), nullable=False,
3007 3007 default=datetime.datetime.now)
3008 3008
3009 3009 @declared_attr
3010 3010 def user_id(cls):
3011 3011 return Column(
3012 3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 3013 unique=None)
3014 3014
3015 3015 # 500 revisions max
3016 3016 _revisions = Column(
3017 3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018 3018
3019 3019 @declared_attr
3020 3020 def source_repo_id(cls):
3021 3021 # TODO: dan: rename column to source_repo_id
3022 3022 return Column(
3023 3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 3024 nullable=False)
3025 3025
3026 3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027 3027
3028 3028 @declared_attr
3029 3029 def target_repo_id(cls):
3030 3030 # TODO: dan: rename column to target_repo_id
3031 3031 return Column(
3032 3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 3033 nullable=False)
3034 3034
3035 3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036 3036
3037 3037 # TODO: dan: rename column to last_merge_source_rev
3038 3038 _last_merge_source_rev = Column(
3039 3039 'last_merge_org_rev', String(40), nullable=True)
3040 3040 # TODO: dan: rename column to last_merge_target_rev
3041 3041 _last_merge_target_rev = Column(
3042 3042 'last_merge_other_rev', String(40), nullable=True)
3043 3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3044 3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3045 3045
3046 3046 @hybrid_property
3047 3047 def revisions(self):
3048 3048 return self._revisions.split(':') if self._revisions else []
3049 3049
3050 3050 @revisions.setter
3051 3051 def revisions(self, val):
3052 3052 self._revisions = ':'.join(val)
3053 3053
3054 3054 @declared_attr
3055 3055 def author(cls):
3056 3056 return relationship('User', lazy='joined')
3057 3057
3058 3058 @declared_attr
3059 3059 def source_repo(cls):
3060 3060 return relationship(
3061 3061 'Repository',
3062 3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3063 3063
3064 3064 @property
3065 3065 def source_ref_parts(self):
3066 3066 refs = self.source_ref.split(':')
3067 3067 return Reference(refs[0], refs[1], refs[2])
3068 3068
3069 3069 @declared_attr
3070 3070 def target_repo(cls):
3071 3071 return relationship(
3072 3072 'Repository',
3073 3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074 3074
3075 3075 @property
3076 3076 def target_ref_parts(self):
3077 3077 refs = self.target_ref.split(':')
3078 3078 return Reference(refs[0], refs[1], refs[2])
3079 3079
3080 3080
3081 3081 class PullRequest(Base, _PullRequestBase):
3082 3082 __tablename__ = 'pull_requests'
3083 3083 __table_args__ = (
3084 3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 3086 )
3087 3087
3088 3088 pull_request_id = Column(
3089 3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3090 3090
3091 3091 def __repr__(self):
3092 3092 if self.pull_request_id:
3093 3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3094 3094 else:
3095 3095 return '<DB:PullRequest at %#x>' % id(self)
3096 3096
3097 3097 reviewers = relationship('PullRequestReviewers',
3098 3098 cascade="all, delete, delete-orphan")
3099 3099 statuses = relationship('ChangesetStatus')
3100 3100 comments = relationship('ChangesetComment',
3101 3101 cascade="all, delete, delete-orphan")
3102 3102 versions = relationship('PullRequestVersion',
3103 3103 cascade="all, delete, delete-orphan")
3104 3104
3105 3105 def is_closed(self):
3106 3106 return self.status == self.STATUS_CLOSED
3107 3107
3108 3108 def get_api_data(self):
3109 3109 from rhodecode.model.pull_request import PullRequestModel
3110 3110 pull_request = self
3111 3111 merge_status = PullRequestModel().merge_status(pull_request)
3112 pull_request_url = url(
3113 'pullrequest_show', repo_name=self.target_repo.repo_name,
3114 pull_request_id=self.pull_request_id, qualified=True)
3112 3115 data = {
3113 3116 'pull_request_id': pull_request.pull_request_id,
3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3115 pull_request_id=self.pull_request_id,
3116 qualified=True),
3117 'url': pull_request_url,
3117 3118 'title': pull_request.title,
3118 3119 'description': pull_request.description,
3119 3120 'status': pull_request.status,
3120 3121 'created_on': pull_request.created_on,
3121 3122 'updated_on': pull_request.updated_on,
3122 3123 'commit_ids': pull_request.revisions,
3123 3124 'review_status': pull_request.calculated_review_status(),
3124 3125 'mergeable': {
3125 3126 'status': merge_status[0],
3126 3127 'message': unicode(merge_status[1]),
3127 3128 },
3128 3129 'source': {
3129 3130 'clone_url': pull_request.source_repo.clone_url(),
3130 3131 'repository': pull_request.source_repo.repo_name,
3131 3132 'reference': {
3132 3133 'name': pull_request.source_ref_parts.name,
3133 3134 'type': pull_request.source_ref_parts.type,
3134 3135 'commit_id': pull_request.source_ref_parts.commit_id,
3135 3136 },
3136 3137 },
3137 3138 'target': {
3138 3139 'clone_url': pull_request.target_repo.clone_url(),
3139 3140 'repository': pull_request.target_repo.repo_name,
3140 3141 'reference': {
3141 3142 'name': pull_request.target_ref_parts.name,
3142 3143 'type': pull_request.target_ref_parts.type,
3143 3144 'commit_id': pull_request.target_ref_parts.commit_id,
3144 3145 },
3145 3146 },
3147 'shadow': {
3148 # TODO: martinb: Unify generation/suffix of clone url.
3149 'clone_url': '{}/repository'.format(pull_request_url),
3150 },
3146 3151 'author': pull_request.author.get_api_data(include_secrets=False,
3147 3152 details='basic'),
3148 3153 'reviewers': [
3149 3154 {
3150 3155 'user': reviewer.get_api_data(include_secrets=False,
3151 3156 details='basic'),
3152 3157 'reasons': reasons,
3153 3158 'review_status': st[0][1].status if st else 'not_reviewed',
3154 3159 }
3155 3160 for reviewer, reasons, st in pull_request.reviewers_statuses()
3156 3161 ]
3157 3162 }
3158 3163
3159 3164 return data
3160 3165
3161 3166 def __json__(self):
3162 3167 return {
3163 3168 'revisions': self.revisions,
3164 3169 }
3165 3170
3166 3171 def calculated_review_status(self):
3167 3172 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3168 3173 # because it's tricky on how to use ChangesetStatusModel from there
3169 3174 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3170 3175 from rhodecode.model.changeset_status import ChangesetStatusModel
3171 3176 return ChangesetStatusModel().calculated_review_status(self)
3172 3177
3173 3178 def reviewers_statuses(self):
3174 3179 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3175 3180 from rhodecode.model.changeset_status import ChangesetStatusModel
3176 3181 return ChangesetStatusModel().reviewers_statuses(self)
3177 3182
3178 3183
3179 3184 class PullRequestVersion(Base, _PullRequestBase):
3180 3185 __tablename__ = 'pull_request_versions'
3181 3186 __table_args__ = (
3182 3187 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3183 3188 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3184 3189 )
3185 3190
3186 3191 pull_request_version_id = Column(
3187 3192 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3188 3193 pull_request_id = Column(
3189 3194 'pull_request_id', Integer(),
3190 3195 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3191 3196 pull_request = relationship('PullRequest')
3192 3197
3193 3198 def __repr__(self):
3194 3199 if self.pull_request_version_id:
3195 3200 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3196 3201 else:
3197 3202 return '<DB:PullRequestVersion at %#x>' % id(self)
3198 3203
3199 3204
3200 3205 class PullRequestReviewers(Base, BaseModel):
3201 3206 __tablename__ = 'pull_request_reviewers'
3202 3207 __table_args__ = (
3203 3208 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3204 3209 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3205 3210 )
3206 3211
3207 3212 def __init__(self, user=None, pull_request=None, reasons=None):
3208 3213 self.user = user
3209 3214 self.pull_request = pull_request
3210 3215 self.reasons = reasons or []
3211 3216
3212 3217 @hybrid_property
3213 3218 def reasons(self):
3214 3219 if not self._reasons:
3215 3220 return []
3216 3221 return self._reasons
3217 3222
3218 3223 @reasons.setter
3219 3224 def reasons(self, val):
3220 3225 val = val or []
3221 3226 if any(not isinstance(x, basestring) for x in val):
3222 3227 raise Exception('invalid reasons type, must be list of strings')
3223 3228 self._reasons = val
3224 3229
3225 3230 pull_requests_reviewers_id = Column(
3226 3231 'pull_requests_reviewers_id', Integer(), nullable=False,
3227 3232 primary_key=True)
3228 3233 pull_request_id = Column(
3229 3234 "pull_request_id", Integer(),
3230 3235 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3231 3236 user_id = Column(
3232 3237 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3233 3238 _reasons = Column(
3234 3239 'reason', MutationList.as_mutable(
3235 3240 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3236 3241
3237 3242 user = relationship('User')
3238 3243 pull_request = relationship('PullRequest')
3239 3244
3240 3245
3241 3246 class Notification(Base, BaseModel):
3242 3247 __tablename__ = 'notifications'
3243 3248 __table_args__ = (
3244 3249 Index('notification_type_idx', 'type'),
3245 3250 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3246 3251 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3247 3252 )
3248 3253
3249 3254 TYPE_CHANGESET_COMMENT = u'cs_comment'
3250 3255 TYPE_MESSAGE = u'message'
3251 3256 TYPE_MENTION = u'mention'
3252 3257 TYPE_REGISTRATION = u'registration'
3253 3258 TYPE_PULL_REQUEST = u'pull_request'
3254 3259 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3255 3260
3256 3261 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3257 3262 subject = Column('subject', Unicode(512), nullable=True)
3258 3263 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3259 3264 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3260 3265 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3261 3266 type_ = Column('type', Unicode(255))
3262 3267
3263 3268 created_by_user = relationship('User')
3264 3269 notifications_to_users = relationship('UserNotification', lazy='joined',
3265 3270 cascade="all, delete, delete-orphan")
3266 3271
3267 3272 @property
3268 3273 def recipients(self):
3269 3274 return [x.user for x in UserNotification.query()\
3270 3275 .filter(UserNotification.notification == self)\
3271 3276 .order_by(UserNotification.user_id.asc()).all()]
3272 3277
3273 3278 @classmethod
3274 3279 def create(cls, created_by, subject, body, recipients, type_=None):
3275 3280 if type_ is None:
3276 3281 type_ = Notification.TYPE_MESSAGE
3277 3282
3278 3283 notification = cls()
3279 3284 notification.created_by_user = created_by
3280 3285 notification.subject = subject
3281 3286 notification.body = body
3282 3287 notification.type_ = type_
3283 3288 notification.created_on = datetime.datetime.now()
3284 3289
3285 3290 for u in recipients:
3286 3291 assoc = UserNotification()
3287 3292 assoc.notification = notification
3288 3293
3289 3294 # if created_by is inside recipients mark his notification
3290 3295 # as read
3291 3296 if u.user_id == created_by.user_id:
3292 3297 assoc.read = True
3293 3298
3294 3299 u.notifications.append(assoc)
3295 3300 Session().add(notification)
3296 3301
3297 3302 return notification
3298 3303
3299 3304 @property
3300 3305 def description(self):
3301 3306 from rhodecode.model.notification import NotificationModel
3302 3307 return NotificationModel().make_description(self)
3303 3308
3304 3309
3305 3310 class UserNotification(Base, BaseModel):
3306 3311 __tablename__ = 'user_to_notification'
3307 3312 __table_args__ = (
3308 3313 UniqueConstraint('user_id', 'notification_id'),
3309 3314 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3310 3315 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3311 3316 )
3312 3317 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3313 3318 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3314 3319 read = Column('read', Boolean, default=False)
3315 3320 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3316 3321
3317 3322 user = relationship('User', lazy="joined")
3318 3323 notification = relationship('Notification', lazy="joined",
3319 3324 order_by=lambda: Notification.created_on.desc(),)
3320 3325
3321 3326 def mark_as_read(self):
3322 3327 self.read = True
3323 3328 Session().add(self)
3324 3329
3325 3330
3326 3331 class Gist(Base, BaseModel):
3327 3332 __tablename__ = 'gists'
3328 3333 __table_args__ = (
3329 3334 Index('g_gist_access_id_idx', 'gist_access_id'),
3330 3335 Index('g_created_on_idx', 'created_on'),
3331 3336 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3332 3337 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3333 3338 )
3334 3339 GIST_PUBLIC = u'public'
3335 3340 GIST_PRIVATE = u'private'
3336 3341 DEFAULT_FILENAME = u'gistfile1.txt'
3337 3342
3338 3343 ACL_LEVEL_PUBLIC = u'acl_public'
3339 3344 ACL_LEVEL_PRIVATE = u'acl_private'
3340 3345
3341 3346 gist_id = Column('gist_id', Integer(), primary_key=True)
3342 3347 gist_access_id = Column('gist_access_id', Unicode(250))
3343 3348 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3344 3349 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3345 3350 gist_expires = Column('gist_expires', Float(53), nullable=False)
3346 3351 gist_type = Column('gist_type', Unicode(128), nullable=False)
3347 3352 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3348 3353 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3349 3354 acl_level = Column('acl_level', Unicode(128), nullable=True)
3350 3355
3351 3356 owner = relationship('User')
3352 3357
3353 3358 def __repr__(self):
3354 3359 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3355 3360
3356 3361 @classmethod
3357 3362 def get_or_404(cls, id_):
3358 3363 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3359 3364 if not res:
3360 3365 raise HTTPNotFound
3361 3366 return res
3362 3367
3363 3368 @classmethod
3364 3369 def get_by_access_id(cls, gist_access_id):
3365 3370 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3366 3371
3367 3372 def gist_url(self):
3368 3373 import rhodecode
3369 3374 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3370 3375 if alias_url:
3371 3376 return alias_url.replace('{gistid}', self.gist_access_id)
3372 3377
3373 3378 return url('gist', gist_id=self.gist_access_id, qualified=True)
3374 3379
3375 3380 @classmethod
3376 3381 def base_path(cls):
3377 3382 """
3378 3383 Returns base path when all gists are stored
3379 3384
3380 3385 :param cls:
3381 3386 """
3382 3387 from rhodecode.model.gist import GIST_STORE_LOC
3383 3388 q = Session().query(RhodeCodeUi)\
3384 3389 .filter(RhodeCodeUi.ui_key == URL_SEP)
3385 3390 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3386 3391 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3387 3392
3388 3393 def get_api_data(self):
3389 3394 """
3390 3395 Common function for generating gist related data for API
3391 3396 """
3392 3397 gist = self
3393 3398 data = {
3394 3399 'gist_id': gist.gist_id,
3395 3400 'type': gist.gist_type,
3396 3401 'access_id': gist.gist_access_id,
3397 3402 'description': gist.gist_description,
3398 3403 'url': gist.gist_url(),
3399 3404 'expires': gist.gist_expires,
3400 3405 'created_on': gist.created_on,
3401 3406 'modified_at': gist.modified_at,
3402 3407 'content': None,
3403 3408 'acl_level': gist.acl_level,
3404 3409 }
3405 3410 return data
3406 3411
3407 3412 def __json__(self):
3408 3413 data = dict(
3409 3414 )
3410 3415 data.update(self.get_api_data())
3411 3416 return data
3412 3417 # SCM functions
3413 3418
3414 3419 def scm_instance(self, **kwargs):
3415 3420 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3416 3421 return get_vcs_instance(
3417 3422 repo_path=safe_str(full_repo_path), create=False)
3418 3423
3419 3424
3420 3425 class DbMigrateVersion(Base, BaseModel):
3421 3426 __tablename__ = 'db_migrate_version'
3422 3427 __table_args__ = (
3423 3428 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3424 3429 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3425 3430 )
3426 3431 repository_id = Column('repository_id', String(250), primary_key=True)
3427 3432 repository_path = Column('repository_path', Text)
3428 3433 version = Column('version', Integer)
3429 3434
3430 3435
3431 3436 class ExternalIdentity(Base, BaseModel):
3432 3437 __tablename__ = 'external_identities'
3433 3438 __table_args__ = (
3434 3439 Index('local_user_id_idx', 'local_user_id'),
3435 3440 Index('external_id_idx', 'external_id'),
3436 3441 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3437 3442 'mysql_charset': 'utf8'})
3438 3443
3439 3444 external_id = Column('external_id', Unicode(255), default=u'',
3440 3445 primary_key=True)
3441 3446 external_username = Column('external_username', Unicode(1024), default=u'')
3442 3447 local_user_id = Column('local_user_id', Integer(),
3443 3448 ForeignKey('users.user_id'), primary_key=True)
3444 3449 provider_name = Column('provider_name', Unicode(255), default=u'',
3445 3450 primary_key=True)
3446 3451 access_token = Column('access_token', String(1024), default=u'')
3447 3452 alt_token = Column('alt_token', String(1024), default=u'')
3448 3453 token_secret = Column('token_secret', String(1024), default=u'')
3449 3454
3450 3455 @classmethod
3451 3456 def by_external_id_and_provider(cls, external_id, provider_name,
3452 3457 local_user_id=None):
3453 3458 """
3454 3459 Returns ExternalIdentity instance based on search params
3455 3460
3456 3461 :param external_id:
3457 3462 :param provider_name:
3458 3463 :return: ExternalIdentity
3459 3464 """
3460 3465 query = cls.query()
3461 3466 query = query.filter(cls.external_id == external_id)
3462 3467 query = query.filter(cls.provider_name == provider_name)
3463 3468 if local_user_id:
3464 3469 query = query.filter(cls.local_user_id == local_user_id)
3465 3470 return query.first()
3466 3471
3467 3472 @classmethod
3468 3473 def user_by_external_id_and_provider(cls, external_id, provider_name):
3469 3474 """
3470 3475 Returns User instance based on search params
3471 3476
3472 3477 :param external_id:
3473 3478 :param provider_name:
3474 3479 :return: User
3475 3480 """
3476 3481 query = User.query()
3477 3482 query = query.filter(cls.external_id == external_id)
3478 3483 query = query.filter(cls.provider_name == provider_name)
3479 3484 query = query.filter(User.user_id == cls.local_user_id)
3480 3485 return query.first()
3481 3486
3482 3487 @classmethod
3483 3488 def by_local_user_id(cls, local_user_id):
3484 3489 """
3485 3490 Returns all tokens for user
3486 3491
3487 3492 :param local_user_id:
3488 3493 :return: ExternalIdentity
3489 3494 """
3490 3495 query = cls.query()
3491 3496 query = query.filter(cls.local_user_id == local_user_id)
3492 3497 return query
3493 3498
3494 3499
3495 3500 class Integration(Base, BaseModel):
3496 3501 __tablename__ = 'integrations'
3497 3502 __table_args__ = (
3498 3503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3499 3504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3500 3505 )
3501 3506
3502 3507 integration_id = Column('integration_id', Integer(), primary_key=True)
3503 3508 integration_type = Column('integration_type', String(255))
3504 3509 enabled = Column('enabled', Boolean(), nullable=False)
3505 3510 name = Column('name', String(255), nullable=False)
3506 3511 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3507 3512 default=False)
3508 3513
3509 3514 settings = Column(
3510 3515 'settings_json', MutationObj.as_mutable(
3511 3516 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3512 3517 repo_id = Column(
3513 3518 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3514 3519 nullable=True, unique=None, default=None)
3515 3520 repo = relationship('Repository', lazy='joined')
3516 3521
3517 3522 repo_group_id = Column(
3518 3523 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3519 3524 nullable=True, unique=None, default=None)
3520 3525 repo_group = relationship('RepoGroup', lazy='joined')
3521 3526
3522 3527 @property
3523 3528 def scope(self):
3524 3529 if self.repo:
3525 3530 return repr(self.repo)
3526 3531 if self.repo_group:
3527 3532 if self.child_repos_only:
3528 3533 return repr(self.repo_group) + ' (child repos only)'
3529 3534 else:
3530 3535 return repr(self.repo_group) + ' (recursive)'
3531 3536 if self.child_repos_only:
3532 3537 return 'root_repos'
3533 3538 return 'global'
3534 3539
3535 3540 def __repr__(self):
3536 3541 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3537 3542
3538 3543
3539 3544 class RepoReviewRuleUser(Base, BaseModel):
3540 3545 __tablename__ = 'repo_review_rules_users'
3541 3546 __table_args__ = (
3542 3547 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3543 3548 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3544 3549 )
3545 3550 repo_review_rule_user_id = Column(
3546 3551 'repo_review_rule_user_id', Integer(), primary_key=True)
3547 3552 repo_review_rule_id = Column("repo_review_rule_id",
3548 3553 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3549 3554 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3550 3555 nullable=False)
3551 3556 user = relationship('User')
3552 3557
3553 3558
3554 3559 class RepoReviewRuleUserGroup(Base, BaseModel):
3555 3560 __tablename__ = 'repo_review_rules_users_groups'
3556 3561 __table_args__ = (
3557 3562 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3558 3563 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3559 3564 )
3560 3565 repo_review_rule_users_group_id = Column(
3561 3566 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3562 3567 repo_review_rule_id = Column("repo_review_rule_id",
3563 3568 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3564 3569 users_group_id = Column("users_group_id", Integer(),
3565 3570 ForeignKey('users_groups.users_group_id'), nullable=False)
3566 3571 users_group = relationship('UserGroup')
3567 3572
3568 3573
3569 3574 class RepoReviewRule(Base, BaseModel):
3570 3575 __tablename__ = 'repo_review_rules'
3571 3576 __table_args__ = (
3572 3577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3573 3578 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3574 3579 )
3575 3580
3576 3581 repo_review_rule_id = Column(
3577 3582 'repo_review_rule_id', Integer(), primary_key=True)
3578 3583 repo_id = Column(
3579 3584 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3580 3585 repo = relationship('Repository', backref='review_rules')
3581 3586
3582 3587 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3583 3588 default=u'*') # glob
3584 3589 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3585 3590 default=u'*') # glob
3586 3591
3587 3592 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3588 3593 nullable=False, default=False)
3589 3594 rule_users = relationship('RepoReviewRuleUser')
3590 3595 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3591 3596
3592 3597 @hybrid_property
3593 3598 def branch_pattern(self):
3594 3599 return self._branch_pattern or '*'
3595 3600
3596 3601 def _validate_glob(self, value):
3597 3602 re.compile('^' + glob2re(value) + '$')
3598 3603
3599 3604 @branch_pattern.setter
3600 3605 def branch_pattern(self, value):
3601 3606 self._validate_glob(value)
3602 3607 self._branch_pattern = value or '*'
3603 3608
3604 3609 @hybrid_property
3605 3610 def file_pattern(self):
3606 3611 return self._file_pattern or '*'
3607 3612
3608 3613 @file_pattern.setter
3609 3614 def file_pattern(self, value):
3610 3615 self._validate_glob(value)
3611 3616 self._file_pattern = value or '*'
3612 3617
3613 3618 def matches(self, branch, files_changed):
3614 3619 """
3615 3620 Check if this review rule matches a branch/files in a pull request
3616 3621
3617 3622 :param branch: branch name for the commit
3618 3623 :param files_changed: list of file paths changed in the pull request
3619 3624 """
3620 3625
3621 3626 branch = branch or ''
3622 3627 files_changed = files_changed or []
3623 3628
3624 3629 branch_matches = True
3625 3630 if branch:
3626 3631 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3627 3632 branch_matches = bool(branch_regex.search(branch))
3628 3633
3629 3634 files_matches = True
3630 3635 if self.file_pattern != '*':
3631 3636 files_matches = False
3632 3637 file_regex = re.compile(glob2re(self.file_pattern))
3633 3638 for filename in files_changed:
3634 3639 if file_regex.search(filename):
3635 3640 files_matches = True
3636 3641 break
3637 3642
3638 3643 return branch_matches and files_matches
3639 3644
3640 3645 @property
3641 3646 def review_users(self):
3642 3647 """ Returns the users which this rule applies to """
3643 3648
3644 3649 users = set()
3645 3650 users |= set([
3646 3651 rule_user.user for rule_user in self.rule_users
3647 3652 if rule_user.user.active])
3648 3653 users |= set(
3649 3654 member.user
3650 3655 for rule_user_group in self.rule_user_groups
3651 3656 for member in rule_user_group.users_group.members
3652 3657 if member.user.active
3653 3658 )
3654 3659 return users
3655 3660
3656 3661 def __repr__(self):
3657 3662 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3658 3663 self.repo_review_rule_id, self.repo)
General Comments 0
You need to be logged in to leave comments. Login now