##// END OF EJS Templates
api: added get_comment method, and return versions for comments to allow simple edits via API.
marcink -
r4440:c1776819 default
parent child Browse files
Show More
@@ -1,133 +1,134 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 import pytest
22 22
23 23 from rhodecode.api.utils import Optional, OAttr
24 24 from rhodecode.api.tests.utils import (
25 25 build_data, api_call, assert_error, assert_ok)
26 26
27 27
28 28 @pytest.mark.usefixtures("testuser_api", "app")
29 29 class TestApi(object):
30 30 maxDiff = None
31 31
32 32 def test_Optional_object(self):
33 33
34 34 option1 = Optional(None)
35 35 assert '<Optional:%s>' % (None,) == repr(option1)
36 36 assert option1() is None
37 37
38 38 assert 1 == Optional.extract(Optional(1))
39 39 assert 'example' == Optional.extract('example')
40 40
41 41 def test_Optional_OAttr(self):
42 42 option1 = Optional(OAttr('apiuser'))
43 43 assert 'apiuser' == Optional.extract(option1)
44 44
45 45 def test_OAttr_object(self):
46 46 oattr1 = OAttr('apiuser')
47 47 assert '<OptionalAttr:apiuser>' == repr(oattr1)
48 48 assert oattr1() == oattr1
49 49
50 50 def test_api_wrong_key(self):
51 51 id_, params = build_data('trololo', 'get_user')
52 52 response = api_call(self.app, params)
53 53
54 54 expected = 'Invalid API KEY'
55 55 assert_error(id_, expected, given=response.body)
56 56
57 57 def test_api_missing_non_optional_param(self):
58 58 id_, params = build_data(self.apikey, 'get_repo')
59 59 response = api_call(self.app, params)
60 60
61 61 expected = 'Missing non optional `repoid` arg in JSON DATA'
62 62 assert_error(id_, expected, given=response.body)
63 63
64 64 def test_api_missing_non_optional_param_args_null(self):
65 65 id_, params = build_data(self.apikey, 'get_repo')
66 66 params = params.replace('"args": {}', '"args": null')
67 67 response = api_call(self.app, params)
68 68
69 69 expected = 'Missing non optional `repoid` arg in JSON DATA'
70 70 assert_error(id_, expected, given=response.body)
71 71
72 72 def test_api_missing_non_optional_param_args_bad(self):
73 73 id_, params = build_data(self.apikey, 'get_repo')
74 74 params = params.replace('"args": {}', '"args": 1')
75 75 response = api_call(self.app, params)
76 76
77 77 expected = 'Missing non optional `repoid` arg in JSON DATA'
78 78 assert_error(id_, expected, given=response.body)
79 79
80 80 def test_api_non_existing_method(self, request):
81 81 id_, params = build_data(self.apikey, 'not_existing', args='xx')
82 82 response = api_call(self.app, params)
83 83 expected = 'No such method: not_existing. Similar methods: none'
84 84 assert_error(id_, expected, given=response.body)
85 85
86 86 def test_api_non_existing_method_have_similar(self, request):
87 87 id_, params = build_data(self.apikey, 'comment', args='xx')
88 88 response = api_call(self.app, params)
89 89 expected = 'No such method: comment. ' \
90 'Similar methods: changeset_comment, comment_pull_request, edit_comment, ' \
91 'get_pull_request_comments, comment_commit, get_repo_comments'
90 'Similar methods: changeset_comment, comment_pull_request, ' \
91 'get_pull_request_comments, comment_commit, edit_comment, ' \
92 'get_comment, get_repo_comments'
92 93 assert_error(id_, expected, given=response.body)
93 94
94 95 def test_api_disabled_user(self, request):
95 96
96 97 def set_active(active):
97 98 from rhodecode.model.db import Session, User
98 99 user = User.get_by_auth_token(self.apikey)
99 100 user.active = active
100 101 Session().add(user)
101 102 Session().commit()
102 103
103 104 request.addfinalizer(lambda: set_active(True))
104 105
105 106 set_active(False)
106 107 id_, params = build_data(self.apikey, 'test', args='xx')
107 108 response = api_call(self.app, params)
108 109 expected = 'Request from this user not allowed'
109 110 assert_error(id_, expected, given=response.body)
110 111
111 112 def test_api_args_is_null(self):
112 113 __, params = build_data(self.apikey, 'get_users', )
113 114 params = params.replace('"args": {}', '"args": null')
114 115 response = api_call(self.app, params)
115 116 assert response.status == '200 OK'
116 117
117 118 def test_api_args_is_bad(self):
118 119 __, params = build_data(self.apikey, 'get_users', )
119 120 params = params.replace('"args": {}', '"args": 1')
120 121 response = api_call(self.app, params)
121 122 assert response.status == '200 OK'
122 123
123 124 def test_api_args_different_args(self):
124 125 import string
125 126 expected = {
126 127 'ascii_letters': string.ascii_letters,
127 128 'ws': string.whitespace,
128 129 'printables': string.printable
129 130 }
130 131 id_, params = build_data(self.apikey, 'test', args=expected)
131 132 response = api_call(self.app, params)
132 133 assert response.status == '200 OK'
133 134 assert_ok(id_, expected, response.body)
@@ -1,61 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 pytest
23 23
24 24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
25 25
26 26
27 27 @pytest.mark.usefixtures("testuser_api", "app")
28 28 class TestGetMethod(object):
29 29 def test_get_methods_no_matches(self):
30 30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
31 31 response = api_call(self.app, params)
32 32
33 33 expected = []
34 34 assert_ok(id_, expected, given=response.body)
35 35
36 36 def test_get_methods(self):
37 37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
38 38 response = api_call(self.app, params)
39 39
40 expected = ['changeset_comment', 'comment_pull_request', 'edit_comment',
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
40 expected = [
41 'changeset_comment', 'comment_pull_request', 'get_pull_request_comments',
42 'comment_commit', 'edit_comment', 'get_comment', 'get_repo_comments'
43 ]
42 44 assert_ok(id_, expected, given=response.body)
43 45
44 46 def test_get_methods_on_single_match(self):
45 47 id_, params = build_data(self.apikey, 'get_method',
46 48 pattern='*comment_commit*')
47 49 response = api_call(self.app, params)
48 50
49 51 expected = ['comment_commit',
50 52 {'apiuser': '<RequiredType>',
51 53 'comment_type': "<Optional:u'note'>",
52 54 'commit_id': '<RequiredType>',
53 55 'extra_recipients': '<Optional:[]>',
54 56 'message': '<RequiredType>',
55 57 'repoid': '<RequiredType>',
56 58 'request': '<RequiredType>',
57 59 'resolves_comment_id': '<Optional:None>',
58 60 'status': '<Optional:None>',
59 61 'userid': '<Optional:<OptionalAttr:apiuser>>',
60 62 'send_email': '<Optional:True>'}]
61 63 assert_ok(id_, expected, given=response.body)
@@ -1,86 +1,87 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 pytest
23 23 import urlobject
24 24
25 25 from rhodecode.api.tests.utils import (
26 26 build_data, api_call, assert_error, assert_ok)
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.utils2 import safe_unicode
29 29
30 30 pytestmark = pytest.mark.backends("git", "hg")
31 31
32 32
33 33 @pytest.mark.usefixtures("testuser_api", "app")
34 34 class TestGetPullRequestComments(object):
35 35
36 36 def test_api_get_pull_request_comments(self, pr_util, http_host_only_stub):
37 37 from rhodecode.model.pull_request import PullRequestModel
38 38
39 39 pull_request = pr_util.create_pull_request(mergeable=True)
40 40 id_, params = build_data(
41 41 self.apikey, 'get_pull_request_comments',
42 42 pullrequestid=pull_request.pull_request_id)
43 43
44 44 response = api_call(self.app, params)
45 45
46 46 assert response.status == '200 OK'
47 47 resp_date = response.json['result'][0]['comment_created_on']
48 48 resp_comment_id = response.json['result'][0]['comment_id']
49 49
50 50 expected = [
51 51 {'comment_author': {'active': True,
52 52 'full_name_or_username': 'RhodeCode Admin',
53 53 'username': 'test_admin'},
54 54 'comment_created_on': resp_date,
55 55 'comment_f_path': None,
56 56 'comment_id': resp_comment_id,
57 57 'comment_lineno': None,
58 58 'comment_status': {'status': 'under_review',
59 59 'status_lbl': 'Under Review'},
60 60 'comment_text': 'Auto status change to |new_status|\n\n.. |new_status| replace:: *"Under Review"*',
61 61 'comment_type': 'note',
62 62 'comment_resolved_by': None,
63 63 'pull_request_version': None,
64 'comment_last_version': 0,
64 65 'comment_commit_id': None,
65 66 'comment_pull_request_id': pull_request.pull_request_id
66 67 }
67 68 ]
68 69 assert_ok(id_, expected, response.body)
69 70
70 71 def test_api_get_pull_request_comments_repo_error(self, pr_util):
71 72 pull_request = pr_util.create_pull_request()
72 73 id_, params = build_data(
73 74 self.apikey, 'get_pull_request_comments',
74 75 repoid=666, pullrequestid=pull_request.pull_request_id)
75 76 response = api_call(self.app, params)
76 77
77 78 expected = 'repository `666` does not exist'
78 79 assert_error(id_, expected, given=response.body)
79 80
80 81 def test_api_get_pull_request_comments_pull_request_error(self):
81 82 id_, params = build_data(
82 83 self.apikey, 'get_pull_request_comments', pullrequestid=666)
83 84 response = api_call(self.app, params)
84 85
85 86 expected = 'pull request `666` does not exist'
86 87 assert_error(id_, expected, given=response.body)
@@ -1,110 +1,142 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 pytest
23 23
24 24 from rhodecode.model.db import User, ChangesetComment
25 25 from rhodecode.model.meta import Session
26 26 from rhodecode.model.comment import CommentsModel
27 27 from rhodecode.api.tests.utils import (
28 28 build_data, api_call, assert_error, assert_call_ok)
29 29
30 30
31 31 @pytest.fixture()
32 32 def make_repo_comments_factory(request):
33 33
34 34 class Make(object):
35 35
36 36 def make_comments(self, repo):
37 37 user = User.get_first_super_admin()
38 38 commit = repo.scm_instance()[0]
39 39
40 40 commit_id = commit.raw_id
41 41 file_0 = commit.affected_files[0]
42 42 comments = []
43 43
44 44 # general
45 CommentsModel().create(
45 comment = CommentsModel().create(
46 46 text='General Comment', repo=repo, user=user, commit_id=commit_id,
47 47 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
48 comments.append(comment)
48 49
49 50 # inline
50 CommentsModel().create(
51 comment = CommentsModel().create(
51 52 text='Inline Comment', repo=repo, user=user, commit_id=commit_id,
52 53 f_path=file_0, line_no='n1',
53 54 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
55 comments.append(comment)
54 56
55 57 # todo
56 CommentsModel().create(
58 comment = CommentsModel().create(
57 59 text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id,
58 60 f_path=file_0, line_no='n1',
59 61 comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False)
62 comments.append(comment)
60 63
61 @request.addfinalizer
62 def cleanup():
63 for comment in comments:
64 Session().delete(comment)
64 return comments
65
65 66 return Make()
66 67
67 68
68 69 @pytest.mark.usefixtures("testuser_api", "app")
69 70 class TestGetRepo(object):
70 71
71 72 @pytest.mark.parametrize('filters, expected_count', [
72 73 ({}, 3),
73 74 ({'comment_type': ChangesetComment.COMMENT_TYPE_NOTE}, 2),
74 75 ({'comment_type': ChangesetComment.COMMENT_TYPE_TODO}, 1),
75 76 ({'commit_id': 'FILLED DYNAMIC'}, 3),
76 77 ])
77 78 def test_api_get_repo_comments(self, backend, user_util,
78 79 make_repo_comments_factory, filters, expected_count):
79 80 commits = [{'message': 'A'}, {'message': 'B'}]
80 81 repo = backend.create_repo(commits=commits)
81 82 make_repo_comments_factory.make_comments(repo)
82 83
83 84 api_call_params = {'repoid': repo.repo_name,}
84 85 api_call_params.update(filters)
85 86
86 87 if 'commit_id' in api_call_params:
87 88 commit = repo.scm_instance()[0]
88 89 commit_id = commit.raw_id
89 90 api_call_params['commit_id'] = commit_id
90 91
91 92 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
92 93 response = api_call(self.app, params)
93 94 result = assert_call_ok(id_, given=response.body)
94 95
95 96 assert len(result) == expected_count
96 97
97 98 def test_api_get_repo_comments_wrong_comment_type(
98 99 self, make_repo_comments_factory, backend_hg):
99 100 commits = [{'message': 'A'}, {'message': 'B'}]
100 101 repo = backend_hg.create_repo(commits=commits)
101 102 make_repo_comments_factory.make_comments(repo)
102 103
103 104 api_call_params = {'repoid': repo.repo_name}
104 105 api_call_params.update({'comment_type': 'bogus'})
105 106
106 107 expected = 'comment_type must be one of `{}` got {}'.format(
107 108 ChangesetComment.COMMENT_TYPES, 'bogus')
108 109 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
109 110 response = api_call(self.app, params)
110 111 assert_error(id_, expected, given=response.body)
112
113 def test_api_get_comment(self, make_repo_comments_factory, backend_hg):
114 commits = [{'message': 'A'}, {'message': 'B'}]
115 repo = backend_hg.create_repo(commits=commits)
116
117 comments = make_repo_comments_factory.make_comments(repo)
118 comment_ids = [x.comment_id for x in comments]
119 Session().commit()
120
121 for comment_id in comment_ids:
122 id_, params = build_data(self.apikey, 'get_comment',
123 **{'comment_id': comment_id})
124 response = api_call(self.app, params)
125 result = assert_call_ok(id_, given=response.body)
126 assert result['comment_id'] == comment_id
127
128 def test_api_get_comment_no_access(self, make_repo_comments_factory, backend_hg, user_util):
129 commits = [{'message': 'A'}, {'message': 'B'}]
130 repo = backend_hg.create_repo(commits=commits)
131 comments = make_repo_comments_factory.make_comments(repo)
132 comment_id = comments[0].comment_id
133
134 test_user = user_util.create_user()
135 user_util.grant_user_permission_to_repo(repo, test_user, 'repository.none')
136
137 id_, params = build_data(test_user.api_key, 'get_comment',
138 **{'comment_id': comment_id})
139 response = api_call(self.app, params)
140 assert_error(id_,
141 expected='comment `{}` does not exist'.format(comment_id),
142 given=response.body)
@@ -1,1092 +1,1018 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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, JSONRPCValidationError
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 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.exceptions import CommentVersionMismatch
31 30 from rhodecode.lib.base import vcs_operation_context
32 31 from rhodecode.lib.utils2 import str2bool
33 32 from rhodecode.model.changeset_status import ChangesetStatusModel
34 33 from rhodecode.model.comment import CommentsModel
35 34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 36 from rhodecode.model.settings import SettingsModel
38 37 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
41 39
42 40 log = logging.getLogger(__name__)
43 41
44 42
45 43 @jsonrpc_method()
46 44 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
47 45 merge_state=Optional(False)):
48 46 """
49 47 Get a pull request based on the given ID.
50 48
51 49 :param apiuser: This is filled automatically from the |authtoken|.
52 50 :type apiuser: AuthUser
53 51 :param repoid: Optional, repository name or repository ID from where
54 52 the pull request was opened.
55 53 :type repoid: str or int
56 54 :param pullrequestid: ID of the requested pull request.
57 55 :type pullrequestid: int
58 56 :param merge_state: Optional calculate merge state for each repository.
59 57 This could result in longer time to fetch the data
60 58 :type merge_state: bool
61 59
62 60 Example output:
63 61
64 62 .. code-block:: bash
65 63
66 64 "id": <id_given_in_input>,
67 65 "result":
68 66 {
69 67 "pull_request_id": "<pull_request_id>",
70 68 "url": "<url>",
71 69 "title": "<title>",
72 70 "description": "<description>",
73 71 "status" : "<status>",
74 72 "created_on": "<date_time_created>",
75 73 "updated_on": "<date_time_updated>",
76 74 "versions": "<number_or_versions_of_pr>",
77 75 "commit_ids": [
78 76 ...
79 77 "<commit_id>",
80 78 "<commit_id>",
81 79 ...
82 80 ],
83 81 "review_status": "<review_status>",
84 82 "mergeable": {
85 83 "status": "<bool>",
86 84 "message": "<message>",
87 85 },
88 86 "source": {
89 87 "clone_url": "<clone_url>",
90 88 "repository": "<repository_name>",
91 89 "reference":
92 90 {
93 91 "name": "<name>",
94 92 "type": "<type>",
95 93 "commit_id": "<commit_id>",
96 94 }
97 95 },
98 96 "target": {
99 97 "clone_url": "<clone_url>",
100 98 "repository": "<repository_name>",
101 99 "reference":
102 100 {
103 101 "name": "<name>",
104 102 "type": "<type>",
105 103 "commit_id": "<commit_id>",
106 104 }
107 105 },
108 106 "merge": {
109 107 "clone_url": "<clone_url>",
110 108 "reference":
111 109 {
112 110 "name": "<name>",
113 111 "type": "<type>",
114 112 "commit_id": "<commit_id>",
115 113 }
116 114 },
117 115 "author": <user_obj>,
118 116 "reviewers": [
119 117 ...
120 118 {
121 119 "user": "<user_obj>",
122 120 "review_status": "<review_status>",
123 121 }
124 122 ...
125 123 ]
126 124 },
127 125 "error": null
128 126 """
129 127
130 128 pull_request = get_pull_request_or_error(pullrequestid)
131 129 if Optional.extract(repoid):
132 130 repo = get_repo_or_error(repoid)
133 131 else:
134 132 repo = pull_request.target_repo
135 133
136 134 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
137 135 raise JSONRPCError('repository `%s` or pull request `%s` '
138 136 'does not exist' % (repoid, pullrequestid))
139 137
140 138 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
141 139 # otherwise we can lock the repo on calculation of merge state while update/merge
142 140 # is happening.
143 141 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
144 142 merge_state = Optional.extract(merge_state, binary=True) and pr_created
145 143 data = pull_request.get_api_data(with_merge_state=merge_state)
146 144 return data
147 145
148 146
149 147 @jsonrpc_method()
150 148 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
151 149 merge_state=Optional(False)):
152 150 """
153 151 Get all pull requests from the repository specified in `repoid`.
154 152
155 153 :param apiuser: This is filled automatically from the |authtoken|.
156 154 :type apiuser: AuthUser
157 155 :param repoid: Optional repository name or repository ID.
158 156 :type repoid: str or int
159 157 :param status: Only return pull requests with the specified status.
160 158 Valid options are.
161 159 * ``new`` (default)
162 160 * ``open``
163 161 * ``closed``
164 162 :type status: str
165 163 :param merge_state: Optional calculate merge state for each repository.
166 164 This could result in longer time to fetch the data
167 165 :type merge_state: bool
168 166
169 167 Example output:
170 168
171 169 .. code-block:: bash
172 170
173 171 "id": <id_given_in_input>,
174 172 "result":
175 173 [
176 174 ...
177 175 {
178 176 "pull_request_id": "<pull_request_id>",
179 177 "url": "<url>",
180 178 "title" : "<title>",
181 179 "description": "<description>",
182 180 "status": "<status>",
183 181 "created_on": "<date_time_created>",
184 182 "updated_on": "<date_time_updated>",
185 183 "commit_ids": [
186 184 ...
187 185 "<commit_id>",
188 186 "<commit_id>",
189 187 ...
190 188 ],
191 189 "review_status": "<review_status>",
192 190 "mergeable": {
193 191 "status": "<bool>",
194 192 "message: "<message>",
195 193 },
196 194 "source": {
197 195 "clone_url": "<clone_url>",
198 196 "reference":
199 197 {
200 198 "name": "<name>",
201 199 "type": "<type>",
202 200 "commit_id": "<commit_id>",
203 201 }
204 202 },
205 203 "target": {
206 204 "clone_url": "<clone_url>",
207 205 "reference":
208 206 {
209 207 "name": "<name>",
210 208 "type": "<type>",
211 209 "commit_id": "<commit_id>",
212 210 }
213 211 },
214 212 "merge": {
215 213 "clone_url": "<clone_url>",
216 214 "reference":
217 215 {
218 216 "name": "<name>",
219 217 "type": "<type>",
220 218 "commit_id": "<commit_id>",
221 219 }
222 220 },
223 221 "author": <user_obj>,
224 222 "reviewers": [
225 223 ...
226 224 {
227 225 "user": "<user_obj>",
228 226 "review_status": "<review_status>",
229 227 }
230 228 ...
231 229 ]
232 230 }
233 231 ...
234 232 ],
235 233 "error": null
236 234
237 235 """
238 236 repo = get_repo_or_error(repoid)
239 237 if not has_superadmin_permission(apiuser):
240 238 _perms = (
241 239 'repository.admin', 'repository.write', 'repository.read',)
242 240 validate_repo_permissions(apiuser, repoid, repo, _perms)
243 241
244 242 status = Optional.extract(status)
245 243 merge_state = Optional.extract(merge_state, binary=True)
246 244 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
247 245 order_by='id', order_dir='desc')
248 246 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
249 247 return data
250 248
251 249
252 250 @jsonrpc_method()
253 251 def merge_pull_request(
254 252 request, apiuser, pullrequestid, repoid=Optional(None),
255 253 userid=Optional(OAttr('apiuser'))):
256 254 """
257 255 Merge the pull request specified by `pullrequestid` into its target
258 256 repository.
259 257
260 258 :param apiuser: This is filled automatically from the |authtoken|.
261 259 :type apiuser: AuthUser
262 260 :param repoid: Optional, repository name or repository ID of the
263 261 target repository to which the |pr| is to be merged.
264 262 :type repoid: str or int
265 263 :param pullrequestid: ID of the pull request which shall be merged.
266 264 :type pullrequestid: int
267 265 :param userid: Merge the pull request as this user.
268 266 :type userid: Optional(str or int)
269 267
270 268 Example output:
271 269
272 270 .. code-block:: bash
273 271
274 272 "id": <id_given_in_input>,
275 273 "result": {
276 274 "executed": "<bool>",
277 275 "failure_reason": "<int>",
278 276 "merge_status_message": "<str>",
279 277 "merge_commit_id": "<merge_commit_id>",
280 278 "possible": "<bool>",
281 279 "merge_ref": {
282 280 "commit_id": "<commit_id>",
283 281 "type": "<type>",
284 282 "name": "<name>"
285 283 }
286 284 },
287 285 "error": null
288 286 """
289 287 pull_request = get_pull_request_or_error(pullrequestid)
290 288 if Optional.extract(repoid):
291 289 repo = get_repo_or_error(repoid)
292 290 else:
293 291 repo = pull_request.target_repo
294 292 auth_user = apiuser
295 293
296 294 if not isinstance(userid, Optional):
297 295 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
298 296 user=apiuser, repo_name=repo.repo_name)
299 297 if has_superadmin_permission(apiuser) or is_repo_admin:
300 298 apiuser = get_user_or_error(userid)
301 299 auth_user = apiuser.AuthUser()
302 300 else:
303 301 raise JSONRPCError('userid is not the same as your user')
304 302
305 303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
306 304 raise JSONRPCError(
307 305 'Operation forbidden because pull request is in state {}, '
308 306 'only state {} is allowed.'.format(
309 307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
310 308
311 309 with pull_request.set_state(PullRequest.STATE_UPDATING):
312 310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
313 311 translator=request.translate)
314 312 merge_possible = not check.failed
315 313
316 314 if not merge_possible:
317 315 error_messages = []
318 316 for err_type, error_msg in check.errors:
319 317 error_msg = request.translate(error_msg)
320 318 error_messages.append(error_msg)
321 319
322 320 reasons = ','.join(error_messages)
323 321 raise JSONRPCError(
324 322 'merge not possible for following reasons: {}'.format(reasons))
325 323
326 324 target_repo = pull_request.target_repo
327 325 extras = vcs_operation_context(
328 326 request.environ, repo_name=target_repo.repo_name,
329 327 username=auth_user.username, action='push',
330 328 scm=target_repo.repo_type)
331 329 with pull_request.set_state(PullRequest.STATE_UPDATING):
332 330 merge_response = PullRequestModel().merge_repo(
333 331 pull_request, apiuser, extras=extras)
334 332 if merge_response.executed:
335 333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
336 334
337 335 Session().commit()
338 336
339 337 # In previous versions the merge response directly contained the merge
340 338 # commit id. It is now contained in the merge reference object. To be
341 339 # backwards compatible we have to extract it again.
342 340 merge_response = merge_response.asdict()
343 341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
344 342
345 343 return merge_response
346 344
347 345
348 346 @jsonrpc_method()
349 347 def get_pull_request_comments(
350 348 request, apiuser, pullrequestid, repoid=Optional(None)):
351 349 """
352 350 Get all comments of pull request specified with the `pullrequestid`
353 351
354 352 :param apiuser: This is filled automatically from the |authtoken|.
355 353 :type apiuser: AuthUser
356 354 :param repoid: Optional repository name or repository ID.
357 355 :type repoid: str or int
358 356 :param pullrequestid: The pull request ID.
359 357 :type pullrequestid: int
360 358
361 359 Example output:
362 360
363 361 .. code-block:: bash
364 362
365 363 id : <id_given_in_input>
366 364 result : [
367 365 {
368 366 "comment_author": {
369 367 "active": true,
370 368 "full_name_or_username": "Tom Gore",
371 369 "username": "admin"
372 370 },
373 371 "comment_created_on": "2017-01-02T18:43:45.533",
374 372 "comment_f_path": null,
375 373 "comment_id": 25,
376 374 "comment_lineno": null,
377 375 "comment_status": {
378 376 "status": "under_review",
379 377 "status_lbl": "Under Review"
380 378 },
381 379 "comment_text": "Example text",
382 380 "comment_type": null,
381 "comment_last_version: 0,
383 382 "pull_request_version": null,
384 383 "comment_commit_id": None,
385 384 "comment_pull_request_id": <pull_request_id>
386 385 }
387 386 ],
388 387 error : null
389 388 """
390 389
391 390 pull_request = get_pull_request_or_error(pullrequestid)
392 391 if Optional.extract(repoid):
393 392 repo = get_repo_or_error(repoid)
394 393 else:
395 394 repo = pull_request.target_repo
396 395
397 396 if not PullRequestModel().check_user_read(
398 397 pull_request, apiuser, api=True):
399 398 raise JSONRPCError('repository `%s` or pull request `%s` '
400 399 'does not exist' % (repoid, pullrequestid))
401 400
402 401 (pull_request_latest,
403 402 pull_request_at_ver,
404 403 pull_request_display_obj,
405 404 at_version) = PullRequestModel().get_pr_version(
406 405 pull_request.pull_request_id, version=None)
407 406
408 407 versions = pull_request_display_obj.versions()
409 408 ver_map = {
410 409 ver.pull_request_version_id: cnt
411 410 for cnt, ver in enumerate(versions, 1)
412 411 }
413 412
414 413 # GENERAL COMMENTS with versions #
415 414 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
416 415 q = q.order_by(ChangesetComment.comment_id.asc())
417 416 general_comments = q.all()
418 417
419 418 # INLINE COMMENTS with versions #
420 419 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
421 420 q = q.order_by(ChangesetComment.comment_id.asc())
422 421 inline_comments = q.all()
423 422
424 423 data = []
425 424 for comment in inline_comments + general_comments:
426 425 full_data = comment.get_api_data()
427 426 pr_version_id = None
428 427 if comment.pull_request_version_id:
429 428 pr_version_id = 'v{}'.format(
430 429 ver_map[comment.pull_request_version_id])
431 430
432 431 # sanitize some entries
433 432
434 433 full_data['pull_request_version'] = pr_version_id
435 434 full_data['comment_author'] = {
436 435 'username': full_data['comment_author'].username,
437 436 'full_name_or_username': full_data['comment_author'].full_name_or_username,
438 437 'active': full_data['comment_author'].active,
439 438 }
440 439
441 440 if full_data['comment_status']:
442 441 full_data['comment_status'] = {
443 442 'status': full_data['comment_status'][0].status,
444 443 'status_lbl': full_data['comment_status'][0].status_lbl,
445 444 }
446 445 else:
447 446 full_data['comment_status'] = {}
448 447
449 448 data.append(full_data)
450 449 return data
451 450
452 451
453 452 @jsonrpc_method()
454 453 def comment_pull_request(
455 454 request, apiuser, pullrequestid, repoid=Optional(None),
456 455 message=Optional(None), commit_id=Optional(None), status=Optional(None),
457 456 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
458 457 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
459 458 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
460 459 """
461 460 Comment on the pull request specified with the `pullrequestid`,
462 461 in the |repo| specified by the `repoid`, and optionally change the
463 462 review status.
464 463
465 464 :param apiuser: This is filled automatically from the |authtoken|.
466 465 :type apiuser: AuthUser
467 466 :param repoid: Optional repository name or repository ID.
468 467 :type repoid: str or int
469 468 :param pullrequestid: The pull request ID.
470 469 :type pullrequestid: int
471 470 :param commit_id: Specify the commit_id for which to set a comment. If
472 471 given commit_id is different than latest in the PR status
473 472 change won't be performed.
474 473 :type commit_id: str
475 474 :param message: The text content of the comment.
476 475 :type message: str
477 476 :param status: (**Optional**) Set the approval status of the pull
478 477 request. One of: 'not_reviewed', 'approved', 'rejected',
479 478 'under_review'
480 479 :type status: str
481 480 :param comment_type: Comment type, one of: 'note', 'todo'
482 481 :type comment_type: Optional(str), default: 'note'
483 482 :param resolves_comment_id: id of comment which this one will resolve
484 483 :type resolves_comment_id: Optional(int)
485 484 :param extra_recipients: list of user ids or usernames to add
486 485 notifications for this comment. Acts like a CC for notification
487 486 :type extra_recipients: Optional(list)
488 487 :param userid: Comment on the pull request as this user
489 488 :type userid: Optional(str or int)
490 489 :param send_email: Define if this comment should also send email notification
491 490 :type send_email: Optional(bool)
492 491
493 492 Example output:
494 493
495 494 .. code-block:: bash
496 495
497 496 id : <id_given_in_input>
498 497 result : {
499 498 "pull_request_id": "<Integer>",
500 499 "comment_id": "<Integer>",
501 500 "status": {"given": <given_status>,
502 501 "was_changed": <bool status_was_actually_changed> },
503 502 },
504 503 error : null
505 504 """
506 505 pull_request = get_pull_request_or_error(pullrequestid)
507 506 if Optional.extract(repoid):
508 507 repo = get_repo_or_error(repoid)
509 508 else:
510 509 repo = pull_request.target_repo
511 510
512 511 auth_user = apiuser
513 512 if not isinstance(userid, Optional):
514 513 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
515 514 user=apiuser, repo_name=repo.repo_name)
516 515 if has_superadmin_permission(apiuser) or is_repo_admin:
517 516 apiuser = get_user_or_error(userid)
518 517 auth_user = apiuser.AuthUser()
519 518 else:
520 519 raise JSONRPCError('userid is not the same as your user')
521 520
522 521 if pull_request.is_closed():
523 522 raise JSONRPCError(
524 523 'pull request `%s` comment failed, pull request is closed' % (
525 524 pullrequestid,))
526 525
527 526 if not PullRequestModel().check_user_read(
528 527 pull_request, apiuser, api=True):
529 528 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
530 529 message = Optional.extract(message)
531 530 status = Optional.extract(status)
532 531 commit_id = Optional.extract(commit_id)
533 532 comment_type = Optional.extract(comment_type)
534 533 resolves_comment_id = Optional.extract(resolves_comment_id)
535 534 extra_recipients = Optional.extract(extra_recipients)
536 535 send_email = Optional.extract(send_email, binary=True)
537 536
538 537 if not message and not status:
539 538 raise JSONRPCError(
540 539 'Both message and status parameters are missing. '
541 540 'At least one is required.')
542 541
543 542 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
544 543 status is not None):
545 544 raise JSONRPCError('Unknown comment status: `%s`' % status)
546 545
547 546 if commit_id and commit_id not in pull_request.revisions:
548 547 raise JSONRPCError(
549 548 'Invalid commit_id `%s` for this pull request.' % commit_id)
550 549
551 550 allowed_to_change_status = PullRequestModel().check_user_change_status(
552 551 pull_request, apiuser)
553 552
554 553 # if commit_id is passed re-validated if user is allowed to change status
555 554 # based on latest commit_id from the PR
556 555 if commit_id:
557 556 commit_idx = pull_request.revisions.index(commit_id)
558 557 if commit_idx != 0:
559 558 allowed_to_change_status = False
560 559
561 560 if resolves_comment_id:
562 561 comment = ChangesetComment.get(resolves_comment_id)
563 562 if not comment:
564 563 raise JSONRPCError(
565 564 'Invalid resolves_comment_id `%s` for this pull request.'
566 565 % resolves_comment_id)
567 566 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
568 567 raise JSONRPCError(
569 568 'Comment `%s` is wrong type for setting status to resolved.'
570 569 % resolves_comment_id)
571 570
572 571 text = message
573 572 status_label = ChangesetStatus.get_status_lbl(status)
574 573 if status and allowed_to_change_status:
575 574 st_message = ('Status change %(transition_icon)s %(status)s'
576 575 % {'transition_icon': '>', 'status': status_label})
577 576 text = message or st_message
578 577
579 578 rc_config = SettingsModel().get_all_settings()
580 579 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
581 580
582 581 status_change = status and allowed_to_change_status
583 582 comment = CommentsModel().create(
584 583 text=text,
585 584 repo=pull_request.target_repo.repo_id,
586 585 user=apiuser.user_id,
587 586 pull_request=pull_request.pull_request_id,
588 587 f_path=None,
589 588 line_no=None,
590 589 status_change=(status_label if status_change else None),
591 590 status_change_type=(status if status_change else None),
592 591 closing_pr=False,
593 592 renderer=renderer,
594 593 comment_type=comment_type,
595 594 resolves_comment_id=resolves_comment_id,
596 595 auth_user=auth_user,
597 596 extra_recipients=extra_recipients,
598 597 send_email=send_email
599 598 )
600 599
601 600 if allowed_to_change_status and status:
602 601 old_calculated_status = pull_request.calculated_review_status()
603 602 ChangesetStatusModel().set_status(
604 603 pull_request.target_repo.repo_id,
605 604 status,
606 605 apiuser.user_id,
607 606 comment,
608 607 pull_request=pull_request.pull_request_id
609 608 )
610 609 Session().flush()
611 610
612 611 Session().commit()
613 612
614 613 PullRequestModel().trigger_pull_request_hook(
615 614 pull_request, apiuser, 'comment',
616 615 data={'comment': comment})
617 616
618 617 if allowed_to_change_status and status:
619 618 # we now calculate the status of pull request, and based on that
620 619 # calculation we set the commits status
621 620 calculated_status = pull_request.calculated_review_status()
622 621 if old_calculated_status != calculated_status:
623 622 PullRequestModel().trigger_pull_request_hook(
624 623 pull_request, apiuser, 'review_status_change',
625 624 data={'status': calculated_status})
626 625
627 626 data = {
628 627 'pull_request_id': pull_request.pull_request_id,
629 628 'comment_id': comment.comment_id if comment else None,
630 629 'status': {'given': status, 'was_changed': status_change},
631 630 }
632 631 return data
633 632
634 633
635 634 @jsonrpc_method()
636 def edit_comment(
637 request, apiuser, message, comment_id, version,
638 userid=Optional(OAttr('apiuser')),
639 ):
640 """
641 Edit comment on the pull request or commit,
642 specified by the `comment_id` and version. Initially version should be 0
643
644 :param apiuser: This is filled automatically from the |authtoken|.
645 :type apiuser: AuthUser
646 :param comment_id: Specify the comment_id for editing
647 :type comment_id: int
648 :param version: version of the comment that will be created, starts from 0
649 :type version: int
650 :param message: The text content of the comment.
651 :type message: str
652 :param userid: Comment on the pull request as this user
653 :type userid: Optional(str or int)
654
655 Example output:
656
657 .. code-block:: bash
658
659 id : <id_given_in_input>
660 result : {
661 "comment_history_id": "<Integer>",
662 "version": "<Integer>",
663 },
664 error : null
665 """
666
667 auth_user = apiuser
668 comment = ChangesetComment.get(comment_id)
669
670 is_super_admin = has_superadmin_permission(apiuser)
671 is_repo_admin = HasRepoPermissionAnyApi('repository.admin') \
672 (user=apiuser, repo_name=comment.repo.repo_name)
673
674 if not isinstance(userid, Optional):
675 if is_super_admin or is_repo_admin:
676 apiuser = get_user_or_error(userid)
677 auth_user = apiuser.AuthUser()
678 else:
679 raise JSONRPCError('userid is not the same as your user')
680
681 comment_author = comment.author.user_id == auth_user.user_id
682 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
683 raise JSONRPCError("you don't have access to edit this comment")
684
685 try:
686 comment_history = CommentsModel().edit(
687 comment_id=comment_id,
688 text=message,
689 auth_user=auth_user,
690 version=version,
691 )
692 Session().commit()
693 except CommentVersionMismatch:
694 raise JSONRPCError(
695 'comment ({}) version ({}) mismatch'.format(comment_id, version)
696 )
697 if not comment_history and not message:
698 raise JSONRPCError(
699 "comment ({}) can't be changed with empty string".format(comment_id)
700 )
701 data = {
702 'comment_history_id': comment_history.comment_history_id if comment_history else None,
703 'version': comment_history.version if comment_history else None,
704 }
705 return data
706
707
708 @jsonrpc_method()
709 635 def create_pull_request(
710 636 request, apiuser, source_repo, target_repo, source_ref, target_ref,
711 637 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
712 638 description_renderer=Optional(''), reviewers=Optional(None)):
713 639 """
714 640 Creates a new pull request.
715 641
716 642 Accepts refs in the following formats:
717 643
718 644 * branch:<branch_name>:<sha>
719 645 * branch:<branch_name>
720 646 * bookmark:<bookmark_name>:<sha> (Mercurial only)
721 647 * bookmark:<bookmark_name> (Mercurial only)
722 648
723 649 :param apiuser: This is filled automatically from the |authtoken|.
724 650 :type apiuser: AuthUser
725 651 :param source_repo: Set the source repository name.
726 652 :type source_repo: str
727 653 :param target_repo: Set the target repository name.
728 654 :type target_repo: str
729 655 :param source_ref: Set the source ref name.
730 656 :type source_ref: str
731 657 :param target_ref: Set the target ref name.
732 658 :type target_ref: str
733 659 :param owner: user_id or username
734 660 :type owner: Optional(str)
735 661 :param title: Optionally Set the pull request title, it's generated otherwise
736 662 :type title: str
737 663 :param description: Set the pull request description.
738 664 :type description: Optional(str)
739 665 :type description_renderer: Optional(str)
740 666 :param description_renderer: Set pull request renderer for the description.
741 667 It should be 'rst', 'markdown' or 'plain'. If not give default
742 668 system renderer will be used
743 669 :param reviewers: Set the new pull request reviewers list.
744 670 Reviewer defined by review rules will be added automatically to the
745 671 defined list.
746 672 :type reviewers: Optional(list)
747 673 Accepts username strings or objects of the format:
748 674
749 675 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
750 676 """
751 677
752 678 source_db_repo = get_repo_or_error(source_repo)
753 679 target_db_repo = get_repo_or_error(target_repo)
754 680 if not has_superadmin_permission(apiuser):
755 681 _perms = ('repository.admin', 'repository.write', 'repository.read',)
756 682 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
757 683
758 684 owner = validate_set_owner_permissions(apiuser, owner)
759 685
760 686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
761 687 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
762 688
763 689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
764 690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
765 691
766 692 reviewer_objects = Optional.extract(reviewers) or []
767 693
768 694 # serialize and validate passed in given reviewers
769 695 if reviewer_objects:
770 696 schema = ReviewerListSchema()
771 697 try:
772 698 reviewer_objects = schema.deserialize(reviewer_objects)
773 699 except Invalid as err:
774 700 raise JSONRPCValidationError(colander_exc=err)
775 701
776 702 # validate users
777 703 for reviewer_object in reviewer_objects:
778 704 user = get_user_or_error(reviewer_object['username'])
779 705 reviewer_object['user_id'] = user.user_id
780 706
781 707 get_default_reviewers_data, validate_default_reviewers = \
782 708 PullRequestModel().get_reviewer_functions()
783 709
784 710 # recalculate reviewers logic, to make sure we can validate this
785 711 default_reviewers_data = get_default_reviewers_data(
786 712 owner, source_db_repo,
787 713 source_commit, target_db_repo, target_commit)
788 714
789 715 # now MERGE our given with the calculated
790 716 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
791 717
792 718 try:
793 719 reviewers = validate_default_reviewers(
794 720 reviewer_objects, default_reviewers_data)
795 721 except ValueError as e:
796 722 raise JSONRPCError('Reviewers Validation: {}'.format(e))
797 723
798 724 title = Optional.extract(title)
799 725 if not title:
800 726 title_source_ref = source_ref.split(':', 2)[1]
801 727 title = PullRequestModel().generate_pullrequest_title(
802 728 source=source_repo,
803 729 source_ref=title_source_ref,
804 730 target=target_repo
805 731 )
806 732
807 733 diff_info = default_reviewers_data['diff_info']
808 734 common_ancestor_id = diff_info['ancestor']
809 735 commits = diff_info['commits']
810 736
811 737 if not common_ancestor_id:
812 738 raise JSONRPCError('no common ancestor found')
813 739
814 740 if not commits:
815 741 raise JSONRPCError('no commits found')
816 742
817 743 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
818 744 revisions = [commit.raw_id for commit in reversed(commits)]
819 745
820 746 # recalculate target ref based on ancestor
821 747 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
822 748 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
823 749
824 750 # fetch renderer, if set fallback to plain in case of PR
825 751 rc_config = SettingsModel().get_all_settings()
826 752 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
827 753 description = Optional.extract(description)
828 754 description_renderer = Optional.extract(description_renderer) or default_system_renderer
829 755
830 756 pull_request = PullRequestModel().create(
831 757 created_by=owner.user_id,
832 758 source_repo=source_repo,
833 759 source_ref=full_source_ref,
834 760 target_repo=target_repo,
835 761 target_ref=full_target_ref,
836 762 common_ancestor_id=common_ancestor_id,
837 763 revisions=revisions,
838 764 reviewers=reviewers,
839 765 title=title,
840 766 description=description,
841 767 description_renderer=description_renderer,
842 768 reviewer_data=default_reviewers_data,
843 769 auth_user=apiuser
844 770 )
845 771
846 772 Session().commit()
847 773 data = {
848 774 'msg': 'Created new pull request `{}`'.format(title),
849 775 'pull_request_id': pull_request.pull_request_id,
850 776 }
851 777 return data
852 778
853 779
854 780 @jsonrpc_method()
855 781 def update_pull_request(
856 782 request, apiuser, pullrequestid, repoid=Optional(None),
857 783 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
858 784 reviewers=Optional(None), update_commits=Optional(None)):
859 785 """
860 786 Updates a pull request.
861 787
862 788 :param apiuser: This is filled automatically from the |authtoken|.
863 789 :type apiuser: AuthUser
864 790 :param repoid: Optional repository name or repository ID.
865 791 :type repoid: str or int
866 792 :param pullrequestid: The pull request ID.
867 793 :type pullrequestid: int
868 794 :param title: Set the pull request title.
869 795 :type title: str
870 796 :param description: Update pull request description.
871 797 :type description: Optional(str)
872 798 :type description_renderer: Optional(str)
873 799 :param description_renderer: Update pull request renderer for the description.
874 800 It should be 'rst', 'markdown' or 'plain'
875 801 :param reviewers: Update pull request reviewers list with new value.
876 802 :type reviewers: Optional(list)
877 803 Accepts username strings or objects of the format:
878 804
879 805 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
880 806
881 807 :param update_commits: Trigger update of commits for this pull request
882 808 :type: update_commits: Optional(bool)
883 809
884 810 Example output:
885 811
886 812 .. code-block:: bash
887 813
888 814 id : <id_given_in_input>
889 815 result : {
890 816 "msg": "Updated pull request `63`",
891 817 "pull_request": <pull_request_object>,
892 818 "updated_reviewers": {
893 819 "added": [
894 820 "username"
895 821 ],
896 822 "removed": []
897 823 },
898 824 "updated_commits": {
899 825 "added": [
900 826 "<sha1_hash>"
901 827 ],
902 828 "common": [
903 829 "<sha1_hash>",
904 830 "<sha1_hash>",
905 831 ],
906 832 "removed": []
907 833 }
908 834 }
909 835 error : null
910 836 """
911 837
912 838 pull_request = get_pull_request_or_error(pullrequestid)
913 839 if Optional.extract(repoid):
914 840 repo = get_repo_or_error(repoid)
915 841 else:
916 842 repo = pull_request.target_repo
917 843
918 844 if not PullRequestModel().check_user_update(
919 845 pull_request, apiuser, api=True):
920 846 raise JSONRPCError(
921 847 'pull request `%s` update failed, no permission to update.' % (
922 848 pullrequestid,))
923 849 if pull_request.is_closed():
924 850 raise JSONRPCError(
925 851 'pull request `%s` update failed, pull request is closed' % (
926 852 pullrequestid,))
927 853
928 854 reviewer_objects = Optional.extract(reviewers) or []
929 855
930 856 if reviewer_objects:
931 857 schema = ReviewerListSchema()
932 858 try:
933 859 reviewer_objects = schema.deserialize(reviewer_objects)
934 860 except Invalid as err:
935 861 raise JSONRPCValidationError(colander_exc=err)
936 862
937 863 # validate users
938 864 for reviewer_object in reviewer_objects:
939 865 user = get_user_or_error(reviewer_object['username'])
940 866 reviewer_object['user_id'] = user.user_id
941 867
942 868 get_default_reviewers_data, get_validated_reviewers = \
943 869 PullRequestModel().get_reviewer_functions()
944 870
945 871 # re-use stored rules
946 872 reviewer_rules = pull_request.reviewer_data
947 873 try:
948 874 reviewers = get_validated_reviewers(
949 875 reviewer_objects, reviewer_rules)
950 876 except ValueError as e:
951 877 raise JSONRPCError('Reviewers Validation: {}'.format(e))
952 878 else:
953 879 reviewers = []
954 880
955 881 title = Optional.extract(title)
956 882 description = Optional.extract(description)
957 883 description_renderer = Optional.extract(description_renderer)
958 884
959 885 if title or description:
960 886 PullRequestModel().edit(
961 887 pull_request,
962 888 title or pull_request.title,
963 889 description or pull_request.description,
964 890 description_renderer or pull_request.description_renderer,
965 891 apiuser)
966 892 Session().commit()
967 893
968 894 commit_changes = {"added": [], "common": [], "removed": []}
969 895 if str2bool(Optional.extract(update_commits)):
970 896
971 897 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
972 898 raise JSONRPCError(
973 899 'Operation forbidden because pull request is in state {}, '
974 900 'only state {} is allowed.'.format(
975 901 pull_request.pull_request_state, PullRequest.STATE_CREATED))
976 902
977 903 with pull_request.set_state(PullRequest.STATE_UPDATING):
978 904 if PullRequestModel().has_valid_update_type(pull_request):
979 905 db_user = apiuser.get_instance()
980 906 update_response = PullRequestModel().update_commits(
981 907 pull_request, db_user)
982 908 commit_changes = update_response.changes or commit_changes
983 909 Session().commit()
984 910
985 911 reviewers_changes = {"added": [], "removed": []}
986 912 if reviewers:
987 913 old_calculated_status = pull_request.calculated_review_status()
988 914 added_reviewers, removed_reviewers = \
989 915 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
990 916
991 917 reviewers_changes['added'] = sorted(
992 918 [get_user_or_error(n).username for n in added_reviewers])
993 919 reviewers_changes['removed'] = sorted(
994 920 [get_user_or_error(n).username for n in removed_reviewers])
995 921 Session().commit()
996 922
997 923 # trigger status changed if change in reviewers changes the status
998 924 calculated_status = pull_request.calculated_review_status()
999 925 if old_calculated_status != calculated_status:
1000 926 PullRequestModel().trigger_pull_request_hook(
1001 927 pull_request, apiuser, 'review_status_change',
1002 928 data={'status': calculated_status})
1003 929
1004 930 data = {
1005 931 'msg': 'Updated pull request `{}`'.format(
1006 932 pull_request.pull_request_id),
1007 933 'pull_request': pull_request.get_api_data(),
1008 934 'updated_commits': commit_changes,
1009 935 'updated_reviewers': reviewers_changes
1010 936 }
1011 937
1012 938 return data
1013 939
1014 940
1015 941 @jsonrpc_method()
1016 942 def close_pull_request(
1017 943 request, apiuser, pullrequestid, repoid=Optional(None),
1018 944 userid=Optional(OAttr('apiuser')), message=Optional('')):
1019 945 """
1020 946 Close the pull request specified by `pullrequestid`.
1021 947
1022 948 :param apiuser: This is filled automatically from the |authtoken|.
1023 949 :type apiuser: AuthUser
1024 950 :param repoid: Repository name or repository ID to which the pull
1025 951 request belongs.
1026 952 :type repoid: str or int
1027 953 :param pullrequestid: ID of the pull request to be closed.
1028 954 :type pullrequestid: int
1029 955 :param userid: Close the pull request as this user.
1030 956 :type userid: Optional(str or int)
1031 957 :param message: Optional message to close the Pull Request with. If not
1032 958 specified it will be generated automatically.
1033 959 :type message: Optional(str)
1034 960
1035 961 Example output:
1036 962
1037 963 .. code-block:: bash
1038 964
1039 965 "id": <id_given_in_input>,
1040 966 "result": {
1041 967 "pull_request_id": "<int>",
1042 968 "close_status": "<str:status_lbl>,
1043 969 "closed": "<bool>"
1044 970 },
1045 971 "error": null
1046 972
1047 973 """
1048 974 _ = request.translate
1049 975
1050 976 pull_request = get_pull_request_or_error(pullrequestid)
1051 977 if Optional.extract(repoid):
1052 978 repo = get_repo_or_error(repoid)
1053 979 else:
1054 980 repo = pull_request.target_repo
1055 981
1056 982 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1057 983 user=apiuser, repo_name=repo.repo_name)
1058 984 if not isinstance(userid, Optional):
1059 985 if has_superadmin_permission(apiuser) or is_repo_admin:
1060 986 apiuser = get_user_or_error(userid)
1061 987 else:
1062 988 raise JSONRPCError('userid is not the same as your user')
1063 989
1064 990 if pull_request.is_closed():
1065 991 raise JSONRPCError(
1066 992 'pull request `%s` is already closed' % (pullrequestid,))
1067 993
1068 994 # only owner or admin or person with write permissions
1069 995 allowed_to_close = PullRequestModel().check_user_update(
1070 996 pull_request, apiuser, api=True)
1071 997
1072 998 if not allowed_to_close:
1073 999 raise JSONRPCError(
1074 1000 'pull request `%s` close failed, no permission to close.' % (
1075 1001 pullrequestid,))
1076 1002
1077 1003 # message we're using to close the PR, else it's automatically generated
1078 1004 message = Optional.extract(message)
1079 1005
1080 1006 # finally close the PR, with proper message comment
1081 1007 comment, status = PullRequestModel().close_pull_request_with_comment(
1082 1008 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1083 1009 status_lbl = ChangesetStatus.get_status_lbl(status)
1084 1010
1085 1011 Session().commit()
1086 1012
1087 1013 data = {
1088 1014 'pull_request_id': pull_request.pull_request_id,
1089 1015 'close_status': status_lbl,
1090 1016 'closed': True,
1091 1017 }
1092 1018 return data
@@ -1,2349 +1,2491 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 import logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger, rc_cache
33 33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import (
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
36 HasRepoPermissionAnyApi)
35 37 from rhodecode.lib.celerylib.utils import get_task_id
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int, safe_unicode
38 from rhodecode.lib.utils2 import (
39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
37 40 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 from rhodecode.lib.exceptions import (
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
39 43 from rhodecode.lib.vcs import RepositoryError
40 44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
41 45 from rhodecode.model.changeset_status import ChangesetStatusModel
42 46 from rhodecode.model.comment import CommentsModel
43 47 from rhodecode.model.db import (
44 48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
45 49 ChangesetComment)
46 50 from rhodecode.model.permission import PermissionModel
47 51 from rhodecode.model.repo import RepoModel
48 52 from rhodecode.model.scm import ScmModel, RepoList
49 53 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
50 54 from rhodecode.model import validation_schema
51 55 from rhodecode.model.validation_schema.schemas import repo_schema
52 56
53 57 log = logging.getLogger(__name__)
54 58
55 59
56 60 @jsonrpc_method()
57 61 def get_repo(request, apiuser, repoid, cache=Optional(True)):
58 62 """
59 63 Gets an existing repository by its name or repository_id.
60 64
61 65 The members section so the output returns users groups or users
62 66 associated with that repository.
63 67
64 68 This command can only be run using an |authtoken| with admin rights,
65 69 or users with at least read rights to the |repo|.
66 70
67 71 :param apiuser: This is filled automatically from the |authtoken|.
68 72 :type apiuser: AuthUser
69 73 :param repoid: The repository name or repository id.
70 74 :type repoid: str or int
71 75 :param cache: use the cached value for last changeset
72 76 :type: cache: Optional(bool)
73 77
74 78 Example output:
75 79
76 80 .. code-block:: bash
77 81
78 82 {
79 83 "error": null,
80 84 "id": <repo_id>,
81 85 "result": {
82 86 "clone_uri": null,
83 87 "created_on": "timestamp",
84 88 "description": "repo description",
85 89 "enable_downloads": false,
86 90 "enable_locking": false,
87 91 "enable_statistics": false,
88 92 "followers": [
89 93 {
90 94 "active": true,
91 95 "admin": false,
92 96 "api_key": "****************************************",
93 97 "api_keys": [
94 98 "****************************************"
95 99 ],
96 100 "email": "user@example.com",
97 101 "emails": [
98 102 "user@example.com"
99 103 ],
100 104 "extern_name": "rhodecode",
101 105 "extern_type": "rhodecode",
102 106 "firstname": "username",
103 107 "ip_addresses": [],
104 108 "language": null,
105 109 "last_login": "2015-09-16T17:16:35.854",
106 110 "lastname": "surname",
107 111 "user_id": <user_id>,
108 112 "username": "name"
109 113 }
110 114 ],
111 115 "fork_of": "parent-repo",
112 116 "landing_rev": [
113 117 "rev",
114 118 "tip"
115 119 ],
116 120 "last_changeset": {
117 121 "author": "User <user@example.com>",
118 122 "branch": "default",
119 123 "date": "timestamp",
120 124 "message": "last commit message",
121 125 "parents": [
122 126 {
123 127 "raw_id": "commit-id"
124 128 }
125 129 ],
126 130 "raw_id": "commit-id",
127 131 "revision": <revision number>,
128 132 "short_id": "short id"
129 133 },
130 134 "lock_reason": null,
131 135 "locked_by": null,
132 136 "locked_date": null,
133 137 "owner": "owner-name",
134 138 "permissions": [
135 139 {
136 140 "name": "super-admin-name",
137 141 "origin": "super-admin",
138 142 "permission": "repository.admin",
139 143 "type": "user"
140 144 },
141 145 {
142 146 "name": "owner-name",
143 147 "origin": "owner",
144 148 "permission": "repository.admin",
145 149 "type": "user"
146 150 },
147 151 {
148 152 "name": "user-group-name",
149 153 "origin": "permission",
150 154 "permission": "repository.write",
151 155 "type": "user_group"
152 156 }
153 157 ],
154 158 "private": true,
155 159 "repo_id": 676,
156 160 "repo_name": "user-group/repo-name",
157 161 "repo_type": "hg"
158 162 }
159 163 }
160 164 """
161 165
162 166 repo = get_repo_or_error(repoid)
163 167 cache = Optional.extract(cache)
164 168
165 169 include_secrets = False
166 170 if has_superadmin_permission(apiuser):
167 171 include_secrets = True
168 172 else:
169 173 # check if we have at least read permission for this repo !
170 174 _perms = (
171 175 'repository.admin', 'repository.write', 'repository.read',)
172 176 validate_repo_permissions(apiuser, repoid, repo, _perms)
173 177
174 178 permissions = []
175 179 for _user in repo.permissions():
176 180 user_data = {
177 181 'name': _user.username,
178 182 'permission': _user.permission,
179 183 'origin': get_origin(_user),
180 184 'type': "user",
181 185 }
182 186 permissions.append(user_data)
183 187
184 188 for _user_group in repo.permission_user_groups():
185 189 user_group_data = {
186 190 'name': _user_group.users_group_name,
187 191 'permission': _user_group.permission,
188 192 'origin': get_origin(_user_group),
189 193 'type': "user_group",
190 194 }
191 195 permissions.append(user_group_data)
192 196
193 197 following_users = [
194 198 user.user.get_api_data(include_secrets=include_secrets)
195 199 for user in repo.followers]
196 200
197 201 if not cache:
198 202 repo.update_commit_cache()
199 203 data = repo.get_api_data(include_secrets=include_secrets)
200 204 data['permissions'] = permissions
201 205 data['followers'] = following_users
202 206 return data
203 207
204 208
205 209 @jsonrpc_method()
206 210 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
207 211 """
208 212 Lists all existing repositories.
209 213
210 214 This command can only be run using an |authtoken| with admin rights,
211 215 or users with at least read rights to |repos|.
212 216
213 217 :param apiuser: This is filled automatically from the |authtoken|.
214 218 :type apiuser: AuthUser
215 219 :param root: specify root repository group to fetch repositories.
216 220 filters the returned repositories to be members of given root group.
217 221 :type root: Optional(None)
218 222 :param traverse: traverse given root into subrepositories. With this flag
219 223 set to False, it will only return top-level repositories from `root`.
220 224 if root is empty it will return just top-level repositories.
221 225 :type traverse: Optional(True)
222 226
223 227
224 228 Example output:
225 229
226 230 .. code-block:: bash
227 231
228 232 id : <id_given_in_input>
229 233 result: [
230 234 {
231 235 "repo_id" : "<repo_id>",
232 236 "repo_name" : "<reponame>"
233 237 "repo_type" : "<repo_type>",
234 238 "clone_uri" : "<clone_uri>",
235 239 "private": : "<bool>",
236 240 "created_on" : "<datetimecreated>",
237 241 "description" : "<description>",
238 242 "landing_rev": "<landing_rev>",
239 243 "owner": "<repo_owner>",
240 244 "fork_of": "<name_of_fork_parent>",
241 245 "enable_downloads": "<bool>",
242 246 "enable_locking": "<bool>",
243 247 "enable_statistics": "<bool>",
244 248 },
245 249 ...
246 250 ]
247 251 error: null
248 252 """
249 253
250 254 include_secrets = has_superadmin_permission(apiuser)
251 255 _perms = ('repository.read', 'repository.write', 'repository.admin',)
252 256 extras = {'user': apiuser}
253 257
254 258 root = Optional.extract(root)
255 259 traverse = Optional.extract(traverse, binary=True)
256 260
257 261 if root:
258 262 # verify parent existance, if it's empty return an error
259 263 parent = RepoGroup.get_by_group_name(root)
260 264 if not parent:
261 265 raise JSONRPCError(
262 266 'Root repository group `{}` does not exist'.format(root))
263 267
264 268 if traverse:
265 269 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
266 270 else:
267 271 repos = RepoModel().get_repos_for_root(root=parent)
268 272 else:
269 273 if traverse:
270 274 repos = RepoModel().get_all()
271 275 else:
272 276 # return just top-level
273 277 repos = RepoModel().get_repos_for_root(root=None)
274 278
275 279 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
276 280 return [repo.get_api_data(include_secrets=include_secrets)
277 281 for repo in repo_list]
278 282
279 283
280 284 @jsonrpc_method()
281 285 def get_repo_changeset(request, apiuser, repoid, revision,
282 286 details=Optional('basic')):
283 287 """
284 288 Returns information about a changeset.
285 289
286 290 Additionally parameters define the amount of details returned by
287 291 this function.
288 292
289 293 This command can only be run using an |authtoken| with admin rights,
290 294 or users with at least read rights to the |repo|.
291 295
292 296 :param apiuser: This is filled automatically from the |authtoken|.
293 297 :type apiuser: AuthUser
294 298 :param repoid: The repository name or repository id
295 299 :type repoid: str or int
296 300 :param revision: revision for which listing should be done
297 301 :type revision: str
298 302 :param details: details can be 'basic|extended|full' full gives diff
299 303 info details like the diff itself, and number of changed files etc.
300 304 :type details: Optional(str)
301 305
302 306 """
303 307 repo = get_repo_or_error(repoid)
304 308 if not has_superadmin_permission(apiuser):
305 309 _perms = (
306 310 'repository.admin', 'repository.write', 'repository.read',)
307 311 validate_repo_permissions(apiuser, repoid, repo, _perms)
308 312
309 313 changes_details = Optional.extract(details)
310 314 _changes_details_types = ['basic', 'extended', 'full']
311 315 if changes_details not in _changes_details_types:
312 316 raise JSONRPCError(
313 317 'ret_type must be one of %s' % (
314 318 ','.join(_changes_details_types)))
315 319
316 320 pre_load = ['author', 'branch', 'date', 'message', 'parents',
317 321 'status', '_commit', '_file_paths']
318 322
319 323 try:
320 324 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
321 325 except TypeError as e:
322 326 raise JSONRPCError(safe_str(e))
323 327 _cs_json = cs.__json__()
324 328 _cs_json['diff'] = build_commit_data(cs, changes_details)
325 329 if changes_details == 'full':
326 330 _cs_json['refs'] = cs._get_refs()
327 331 return _cs_json
328 332
329 333
330 334 @jsonrpc_method()
331 335 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
332 336 details=Optional('basic')):
333 337 """
334 338 Returns a set of commits limited by the number starting
335 339 from the `start_rev` option.
336 340
337 341 Additional parameters define the amount of details returned by this
338 342 function.
339 343
340 344 This command can only be run using an |authtoken| with admin rights,
341 345 or users with at least read rights to |repos|.
342 346
343 347 :param apiuser: This is filled automatically from the |authtoken|.
344 348 :type apiuser: AuthUser
345 349 :param repoid: The repository name or repository ID.
346 350 :type repoid: str or int
347 351 :param start_rev: The starting revision from where to get changesets.
348 352 :type start_rev: str
349 353 :param limit: Limit the number of commits to this amount
350 354 :type limit: str or int
351 355 :param details: Set the level of detail returned. Valid option are:
352 356 ``basic``, ``extended`` and ``full``.
353 357 :type details: Optional(str)
354 358
355 359 .. note::
356 360
357 361 Setting the parameter `details` to the value ``full`` is extensive
358 362 and returns details like the diff itself, and the number
359 363 of changed files.
360 364
361 365 """
362 366 repo = get_repo_or_error(repoid)
363 367 if not has_superadmin_permission(apiuser):
364 368 _perms = (
365 369 'repository.admin', 'repository.write', 'repository.read',)
366 370 validate_repo_permissions(apiuser, repoid, repo, _perms)
367 371
368 372 changes_details = Optional.extract(details)
369 373 _changes_details_types = ['basic', 'extended', 'full']
370 374 if changes_details not in _changes_details_types:
371 375 raise JSONRPCError(
372 376 'ret_type must be one of %s' % (
373 377 ','.join(_changes_details_types)))
374 378
375 379 limit = int(limit)
376 380 pre_load = ['author', 'branch', 'date', 'message', 'parents',
377 381 'status', '_commit', '_file_paths']
378 382
379 383 vcs_repo = repo.scm_instance()
380 384 # SVN needs a special case to distinguish its index and commit id
381 385 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
382 386 start_rev = vcs_repo.commit_ids[0]
383 387
384 388 try:
385 389 commits = vcs_repo.get_commits(
386 390 start_id=start_rev, pre_load=pre_load, translate_tags=False)
387 391 except TypeError as e:
388 392 raise JSONRPCError(safe_str(e))
389 393 except Exception:
390 394 log.exception('Fetching of commits failed')
391 395 raise JSONRPCError('Error occurred during commit fetching')
392 396
393 397 ret = []
394 398 for cnt, commit in enumerate(commits):
395 399 if cnt >= limit != -1:
396 400 break
397 401 _cs_json = commit.__json__()
398 402 _cs_json['diff'] = build_commit_data(commit, changes_details)
399 403 if changes_details == 'full':
400 404 _cs_json['refs'] = {
401 405 'branches': [commit.branch],
402 406 'bookmarks': getattr(commit, 'bookmarks', []),
403 407 'tags': commit.tags
404 408 }
405 409 ret.append(_cs_json)
406 410 return ret
407 411
408 412
409 413 @jsonrpc_method()
410 414 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
411 415 ret_type=Optional('all'), details=Optional('basic'),
412 416 max_file_bytes=Optional(None)):
413 417 """
414 418 Returns a list of nodes and children in a flat list for a given
415 419 path at given revision.
416 420
417 421 It's possible to specify ret_type to show only `files` or `dirs`.
418 422
419 423 This command can only be run using an |authtoken| with admin rights,
420 424 or users with at least read rights to |repos|.
421 425
422 426 :param apiuser: This is filled automatically from the |authtoken|.
423 427 :type apiuser: AuthUser
424 428 :param repoid: The repository name or repository ID.
425 429 :type repoid: str or int
426 430 :param revision: The revision for which listing should be done.
427 431 :type revision: str
428 432 :param root_path: The path from which to start displaying.
429 433 :type root_path: str
430 434 :param ret_type: Set the return type. Valid options are
431 435 ``all`` (default), ``files`` and ``dirs``.
432 436 :type ret_type: Optional(str)
433 437 :param details: Returns extended information about nodes, such as
434 438 md5, binary, and or content.
435 439 The valid options are ``basic`` and ``full``.
436 440 :type details: Optional(str)
437 441 :param max_file_bytes: Only return file content under this file size bytes
438 442 :type details: Optional(int)
439 443
440 444 Example output:
441 445
442 446 .. code-block:: bash
443 447
444 448 id : <id_given_in_input>
445 449 result: [
446 450 {
447 451 "binary": false,
448 452 "content": "File line",
449 453 "extension": "md",
450 454 "lines": 2,
451 455 "md5": "059fa5d29b19c0657e384749480f6422",
452 456 "mimetype": "text/x-minidsrc",
453 457 "name": "file.md",
454 458 "size": 580,
455 459 "type": "file"
456 460 },
457 461 ...
458 462 ]
459 463 error: null
460 464 """
461 465
462 466 repo = get_repo_or_error(repoid)
463 467 if not has_superadmin_permission(apiuser):
464 468 _perms = ('repository.admin', 'repository.write', 'repository.read',)
465 469 validate_repo_permissions(apiuser, repoid, repo, _perms)
466 470
467 471 ret_type = Optional.extract(ret_type)
468 472 details = Optional.extract(details)
469 473 _extended_types = ['basic', 'full']
470 474 if details not in _extended_types:
471 475 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
472 476 extended_info = False
473 477 content = False
474 478 if details == 'basic':
475 479 extended_info = True
476 480
477 481 if details == 'full':
478 482 extended_info = content = True
479 483
480 484 _map = {}
481 485 try:
482 486 # check if repo is not empty by any chance, skip quicker if it is.
483 487 _scm = repo.scm_instance()
484 488 if _scm.is_empty():
485 489 return []
486 490
487 491 _d, _f = ScmModel().get_nodes(
488 492 repo, revision, root_path, flat=False,
489 493 extended_info=extended_info, content=content,
490 494 max_file_bytes=max_file_bytes)
491 495 _map = {
492 496 'all': _d + _f,
493 497 'files': _f,
494 498 'dirs': _d,
495 499 }
496 500 return _map[ret_type]
497 501 except KeyError:
498 502 raise JSONRPCError(
499 503 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
500 504 except Exception:
501 505 log.exception("Exception occurred while trying to get repo nodes")
502 506 raise JSONRPCError(
503 507 'failed to get repo: `%s` nodes' % repo.repo_name
504 508 )
505 509
506 510
507 511 @jsonrpc_method()
508 512 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
509 513 max_file_bytes=Optional(None), details=Optional('basic'),
510 514 cache=Optional(True)):
511 515 """
512 516 Returns a single file from repository at given revision.
513 517
514 518 This command can only be run using an |authtoken| with admin rights,
515 519 or users with at least read rights to |repos|.
516 520
517 521 :param apiuser: This is filled automatically from the |authtoken|.
518 522 :type apiuser: AuthUser
519 523 :param repoid: The repository name or repository ID.
520 524 :type repoid: str or int
521 525 :param commit_id: The revision for which listing should be done.
522 526 :type commit_id: str
523 527 :param file_path: The path from which to start displaying.
524 528 :type file_path: str
525 529 :param details: Returns different set of information about nodes.
526 530 The valid options are ``minimal`` ``basic`` and ``full``.
527 531 :type details: Optional(str)
528 532 :param max_file_bytes: Only return file content under this file size bytes
529 533 :type max_file_bytes: Optional(int)
530 534 :param cache: Use internal caches for fetching files. If disabled fetching
531 535 files is slower but more memory efficient
532 536 :type cache: Optional(bool)
533 537
534 538 Example output:
535 539
536 540 .. code-block:: bash
537 541
538 542 id : <id_given_in_input>
539 543 result: {
540 544 "binary": false,
541 545 "extension": "py",
542 546 "lines": 35,
543 547 "content": "....",
544 548 "md5": "76318336366b0f17ee249e11b0c99c41",
545 549 "mimetype": "text/x-python",
546 550 "name": "python.py",
547 551 "size": 817,
548 552 "type": "file",
549 553 }
550 554 error: null
551 555 """
552 556
553 557 repo = get_repo_or_error(repoid)
554 558 if not has_superadmin_permission(apiuser):
555 559 _perms = ('repository.admin', 'repository.write', 'repository.read',)
556 560 validate_repo_permissions(apiuser, repoid, repo, _perms)
557 561
558 562 cache = Optional.extract(cache, binary=True)
559 563 details = Optional.extract(details)
560 564 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
561 565 if details not in _extended_types:
562 566 raise JSONRPCError(
563 567 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
564 568 extended_info = False
565 569 content = False
566 570
567 571 if details == 'minimal':
568 572 extended_info = False
569 573
570 574 elif details == 'basic':
571 575 extended_info = True
572 576
573 577 elif details == 'full':
574 578 extended_info = content = True
575 579
576 580 file_path = safe_unicode(file_path)
577 581 try:
578 582 # check if repo is not empty by any chance, skip quicker if it is.
579 583 _scm = repo.scm_instance()
580 584 if _scm.is_empty():
581 585 return None
582 586
583 587 node = ScmModel().get_node(
584 588 repo, commit_id, file_path, extended_info=extended_info,
585 589 content=content, max_file_bytes=max_file_bytes, cache=cache)
586 590 except NodeDoesNotExistError:
587 591 raise JSONRPCError(u'There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
588 592 repo.repo_name, file_path, commit_id))
589 593 except Exception:
590 594 log.exception(u"Exception occurred while trying to get repo %s file",
591 595 repo.repo_name)
592 596 raise JSONRPCError(u'failed to get repo: `{}` file at path {}'.format(
593 597 repo.repo_name, file_path))
594 598
595 599 return node
596 600
597 601
598 602 @jsonrpc_method()
599 603 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
600 604 """
601 605 Returns a list of tree nodes for path at given revision. This api is built
602 606 strictly for usage in full text search building, and shouldn't be consumed
603 607
604 608 This command can only be run using an |authtoken| with admin rights,
605 609 or users with at least read rights to |repos|.
606 610
607 611 """
608 612
609 613 repo = get_repo_or_error(repoid)
610 614 if not has_superadmin_permission(apiuser):
611 615 _perms = ('repository.admin', 'repository.write', 'repository.read',)
612 616 validate_repo_permissions(apiuser, repoid, repo, _perms)
613 617
614 618 repo_id = repo.repo_id
615 619 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
616 620 cache_on = cache_seconds > 0
617 621
618 622 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
619 623 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
620 624
621 625 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
622 626 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
623 627
624 628 try:
625 629 # check if repo is not empty by any chance, skip quicker if it is.
626 630 _scm = repo.scm_instance()
627 631 if _scm.is_empty():
628 632 return []
629 633 except RepositoryError:
630 634 log.exception("Exception occurred while trying to get repo nodes")
631 635 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
632 636
633 637 try:
634 638 # we need to resolve commit_id to a FULL sha for cache to work correctly.
635 639 # sending 'master' is a pointer that needs to be translated to current commit.
636 640 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
637 641 log.debug(
638 642 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
639 643 'with caching: %s[TTL: %ss]' % (
640 644 repo_id, commit_id, cache_on, cache_seconds or 0))
641 645
642 646 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
643 647 return tree_files
644 648
645 649 except Exception:
646 650 log.exception("Exception occurred while trying to get repo nodes")
647 651 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
648 652
649 653
650 654 @jsonrpc_method()
651 655 def get_repo_refs(request, apiuser, repoid):
652 656 """
653 657 Returns a dictionary of current references. It returns
654 658 bookmarks, branches, closed_branches, and tags for given repository
655 659
656 660 It's possible to specify ret_type to show only `files` or `dirs`.
657 661
658 662 This command can only be run using an |authtoken| with admin rights,
659 663 or users with at least read rights to |repos|.
660 664
661 665 :param apiuser: This is filled automatically from the |authtoken|.
662 666 :type apiuser: AuthUser
663 667 :param repoid: The repository name or repository ID.
664 668 :type repoid: str or int
665 669
666 670 Example output:
667 671
668 672 .. code-block:: bash
669 673
670 674 id : <id_given_in_input>
671 675 "result": {
672 676 "bookmarks": {
673 677 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
674 678 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
675 679 },
676 680 "branches": {
677 681 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
678 682 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
679 683 },
680 684 "branches_closed": {},
681 685 "tags": {
682 686 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
683 687 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
684 688 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
685 689 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
686 690 }
687 691 }
688 692 error: null
689 693 """
690 694
691 695 repo = get_repo_or_error(repoid)
692 696 if not has_superadmin_permission(apiuser):
693 697 _perms = ('repository.admin', 'repository.write', 'repository.read',)
694 698 validate_repo_permissions(apiuser, repoid, repo, _perms)
695 699
696 700 try:
697 701 # check if repo is not empty by any chance, skip quicker if it is.
698 702 vcs_instance = repo.scm_instance()
699 703 refs = vcs_instance.refs()
700 704 return refs
701 705 except Exception:
702 706 log.exception("Exception occurred while trying to get repo refs")
703 707 raise JSONRPCError(
704 708 'failed to get repo: `%s` references' % repo.repo_name
705 709 )
706 710
707 711
708 712 @jsonrpc_method()
709 713 def create_repo(
710 714 request, apiuser, repo_name, repo_type,
711 715 owner=Optional(OAttr('apiuser')),
712 716 description=Optional(''),
713 717 private=Optional(False),
714 718 clone_uri=Optional(None),
715 719 push_uri=Optional(None),
716 720 landing_rev=Optional(None),
717 721 enable_statistics=Optional(False),
718 722 enable_locking=Optional(False),
719 723 enable_downloads=Optional(False),
720 724 copy_permissions=Optional(False)):
721 725 """
722 726 Creates a repository.
723 727
724 728 * If the repository name contains "/", repository will be created inside
725 729 a repository group or nested repository groups
726 730
727 731 For example "foo/bar/repo1" will create |repo| called "repo1" inside
728 732 group "foo/bar". You have to have permissions to access and write to
729 733 the last repository group ("bar" in this example)
730 734
731 735 This command can only be run using an |authtoken| with at least
732 736 permissions to create repositories, or write permissions to
733 737 parent repository groups.
734 738
735 739 :param apiuser: This is filled automatically from the |authtoken|.
736 740 :type apiuser: AuthUser
737 741 :param repo_name: Set the repository name.
738 742 :type repo_name: str
739 743 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
740 744 :type repo_type: str
741 745 :param owner: user_id or username
742 746 :type owner: Optional(str)
743 747 :param description: Set the repository description.
744 748 :type description: Optional(str)
745 749 :param private: set repository as private
746 750 :type private: bool
747 751 :param clone_uri: set clone_uri
748 752 :type clone_uri: str
749 753 :param push_uri: set push_uri
750 754 :type push_uri: str
751 755 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
752 756 :type landing_rev: str
753 757 :param enable_locking:
754 758 :type enable_locking: bool
755 759 :param enable_downloads:
756 760 :type enable_downloads: bool
757 761 :param enable_statistics:
758 762 :type enable_statistics: bool
759 763 :param copy_permissions: Copy permission from group in which the
760 764 repository is being created.
761 765 :type copy_permissions: bool
762 766
763 767
764 768 Example output:
765 769
766 770 .. code-block:: bash
767 771
768 772 id : <id_given_in_input>
769 773 result: {
770 774 "msg": "Created new repository `<reponame>`",
771 775 "success": true,
772 776 "task": "<celery task id or None if done sync>"
773 777 }
774 778 error: null
775 779
776 780
777 781 Example error output:
778 782
779 783 .. code-block:: bash
780 784
781 785 id : <id_given_in_input>
782 786 result : null
783 787 error : {
784 788 'failed to create repository `<repo_name>`'
785 789 }
786 790
787 791 """
788 792
789 793 owner = validate_set_owner_permissions(apiuser, owner)
790 794
791 795 description = Optional.extract(description)
792 796 copy_permissions = Optional.extract(copy_permissions)
793 797 clone_uri = Optional.extract(clone_uri)
794 798 push_uri = Optional.extract(push_uri)
795 799
796 800 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
797 801 if isinstance(private, Optional):
798 802 private = defs.get('repo_private') or Optional.extract(private)
799 803 if isinstance(repo_type, Optional):
800 804 repo_type = defs.get('repo_type')
801 805 if isinstance(enable_statistics, Optional):
802 806 enable_statistics = defs.get('repo_enable_statistics')
803 807 if isinstance(enable_locking, Optional):
804 808 enable_locking = defs.get('repo_enable_locking')
805 809 if isinstance(enable_downloads, Optional):
806 810 enable_downloads = defs.get('repo_enable_downloads')
807 811
808 812 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
809 813 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
810 814 ref_choices = list(set(ref_choices + [landing_ref]))
811 815
812 816 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
813 817
814 818 schema = repo_schema.RepoSchema().bind(
815 819 repo_type_options=rhodecode.BACKENDS.keys(),
816 820 repo_ref_options=ref_choices,
817 821 repo_type=repo_type,
818 822 # user caller
819 823 user=apiuser)
820 824
821 825 try:
822 826 schema_data = schema.deserialize(dict(
823 827 repo_name=repo_name,
824 828 repo_type=repo_type,
825 829 repo_owner=owner.username,
826 830 repo_description=description,
827 831 repo_landing_commit_ref=landing_commit_ref,
828 832 repo_clone_uri=clone_uri,
829 833 repo_push_uri=push_uri,
830 834 repo_private=private,
831 835 repo_copy_permissions=copy_permissions,
832 836 repo_enable_statistics=enable_statistics,
833 837 repo_enable_downloads=enable_downloads,
834 838 repo_enable_locking=enable_locking))
835 839 except validation_schema.Invalid as err:
836 840 raise JSONRPCValidationError(colander_exc=err)
837 841
838 842 try:
839 843 data = {
840 844 'owner': owner,
841 845 'repo_name': schema_data['repo_group']['repo_name_without_group'],
842 846 'repo_name_full': schema_data['repo_name'],
843 847 'repo_group': schema_data['repo_group']['repo_group_id'],
844 848 'repo_type': schema_data['repo_type'],
845 849 'repo_description': schema_data['repo_description'],
846 850 'repo_private': schema_data['repo_private'],
847 851 'clone_uri': schema_data['repo_clone_uri'],
848 852 'push_uri': schema_data['repo_push_uri'],
849 853 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
850 854 'enable_statistics': schema_data['repo_enable_statistics'],
851 855 'enable_locking': schema_data['repo_enable_locking'],
852 856 'enable_downloads': schema_data['repo_enable_downloads'],
853 857 'repo_copy_permissions': schema_data['repo_copy_permissions'],
854 858 }
855 859
856 860 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
857 861 task_id = get_task_id(task)
858 862 # no commit, it's done in RepoModel, or async via celery
859 863 return {
860 864 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
861 865 'success': True, # cannot return the repo data here since fork
862 866 # can be done async
863 867 'task': task_id
864 868 }
865 869 except Exception:
866 870 log.exception(
867 871 u"Exception while trying to create the repository %s",
868 872 schema_data['repo_name'])
869 873 raise JSONRPCError(
870 874 'failed to create repository `%s`' % (schema_data['repo_name'],))
871 875
872 876
873 877 @jsonrpc_method()
874 878 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
875 879 description=Optional('')):
876 880 """
877 881 Adds an extra field to a repository.
878 882
879 883 This command can only be run using an |authtoken| with at least
880 884 write permissions to the |repo|.
881 885
882 886 :param apiuser: This is filled automatically from the |authtoken|.
883 887 :type apiuser: AuthUser
884 888 :param repoid: Set the repository name or repository id.
885 889 :type repoid: str or int
886 890 :param key: Create a unique field key for this repository.
887 891 :type key: str
888 892 :param label:
889 893 :type label: Optional(str)
890 894 :param description:
891 895 :type description: Optional(str)
892 896 """
893 897 repo = get_repo_or_error(repoid)
894 898 if not has_superadmin_permission(apiuser):
895 899 _perms = ('repository.admin',)
896 900 validate_repo_permissions(apiuser, repoid, repo, _perms)
897 901
898 902 label = Optional.extract(label) or key
899 903 description = Optional.extract(description)
900 904
901 905 field = RepositoryField.get_by_key_name(key, repo)
902 906 if field:
903 907 raise JSONRPCError('Field with key '
904 908 '`%s` exists for repo `%s`' % (key, repoid))
905 909
906 910 try:
907 911 RepoModel().add_repo_field(repo, key, field_label=label,
908 912 field_desc=description)
909 913 Session().commit()
910 914 return {
911 915 'msg': "Added new repository field `%s`" % (key,),
912 916 'success': True,
913 917 }
914 918 except Exception:
915 919 log.exception("Exception occurred while trying to add field to repo")
916 920 raise JSONRPCError(
917 921 'failed to create new field for repository `%s`' % (repoid,))
918 922
919 923
920 924 @jsonrpc_method()
921 925 def remove_field_from_repo(request, apiuser, repoid, key):
922 926 """
923 927 Removes an extra field from a repository.
924 928
925 929 This command can only be run using an |authtoken| with at least
926 930 write permissions to the |repo|.
927 931
928 932 :param apiuser: This is filled automatically from the |authtoken|.
929 933 :type apiuser: AuthUser
930 934 :param repoid: Set the repository name or repository ID.
931 935 :type repoid: str or int
932 936 :param key: Set the unique field key for this repository.
933 937 :type key: str
934 938 """
935 939
936 940 repo = get_repo_or_error(repoid)
937 941 if not has_superadmin_permission(apiuser):
938 942 _perms = ('repository.admin',)
939 943 validate_repo_permissions(apiuser, repoid, repo, _perms)
940 944
941 945 field = RepositoryField.get_by_key_name(key, repo)
942 946 if not field:
943 947 raise JSONRPCError('Field with key `%s` does not '
944 948 'exists for repo `%s`' % (key, repoid))
945 949
946 950 try:
947 951 RepoModel().delete_repo_field(repo, field_key=key)
948 952 Session().commit()
949 953 return {
950 954 'msg': "Deleted repository field `%s`" % (key,),
951 955 'success': True,
952 956 }
953 957 except Exception:
954 958 log.exception(
955 959 "Exception occurred while trying to delete field from repo")
956 960 raise JSONRPCError(
957 961 'failed to delete field for repository `%s`' % (repoid,))
958 962
959 963
960 964 @jsonrpc_method()
961 965 def update_repo(
962 966 request, apiuser, repoid, repo_name=Optional(None),
963 967 owner=Optional(OAttr('apiuser')), description=Optional(''),
964 968 private=Optional(False),
965 969 clone_uri=Optional(None), push_uri=Optional(None),
966 970 landing_rev=Optional(None), fork_of=Optional(None),
967 971 enable_statistics=Optional(False),
968 972 enable_locking=Optional(False),
969 973 enable_downloads=Optional(False), fields=Optional('')):
970 974 """
971 975 Updates a repository with the given information.
972 976
973 977 This command can only be run using an |authtoken| with at least
974 978 admin permissions to the |repo|.
975 979
976 980 * If the repository name contains "/", repository will be updated
977 981 accordingly with a repository group or nested repository groups
978 982
979 983 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
980 984 called "repo-test" and place it inside group "foo/bar".
981 985 You have to have permissions to access and write to the last repository
982 986 group ("bar" in this example)
983 987
984 988 :param apiuser: This is filled automatically from the |authtoken|.
985 989 :type apiuser: AuthUser
986 990 :param repoid: repository name or repository ID.
987 991 :type repoid: str or int
988 992 :param repo_name: Update the |repo| name, including the
989 993 repository group it's in.
990 994 :type repo_name: str
991 995 :param owner: Set the |repo| owner.
992 996 :type owner: str
993 997 :param fork_of: Set the |repo| as fork of another |repo|.
994 998 :type fork_of: str
995 999 :param description: Update the |repo| description.
996 1000 :type description: str
997 1001 :param private: Set the |repo| as private. (True | False)
998 1002 :type private: bool
999 1003 :param clone_uri: Update the |repo| clone URI.
1000 1004 :type clone_uri: str
1001 1005 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1002 1006 :type landing_rev: str
1003 1007 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1004 1008 :type enable_statistics: bool
1005 1009 :param enable_locking: Enable |repo| locking.
1006 1010 :type enable_locking: bool
1007 1011 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1008 1012 :type enable_downloads: bool
1009 1013 :param fields: Add extra fields to the |repo|. Use the following
1010 1014 example format: ``field_key=field_val,field_key2=fieldval2``.
1011 1015 Escape ', ' with \,
1012 1016 :type fields: str
1013 1017 """
1014 1018
1015 1019 repo = get_repo_or_error(repoid)
1016 1020
1017 1021 include_secrets = False
1018 1022 if not has_superadmin_permission(apiuser):
1019 1023 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1020 1024 else:
1021 1025 include_secrets = True
1022 1026
1023 1027 updates = dict(
1024 1028 repo_name=repo_name
1025 1029 if not isinstance(repo_name, Optional) else repo.repo_name,
1026 1030
1027 1031 fork_id=fork_of
1028 1032 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1029 1033
1030 1034 user=owner
1031 1035 if not isinstance(owner, Optional) else repo.user.username,
1032 1036
1033 1037 repo_description=description
1034 1038 if not isinstance(description, Optional) else repo.description,
1035 1039
1036 1040 repo_private=private
1037 1041 if not isinstance(private, Optional) else repo.private,
1038 1042
1039 1043 clone_uri=clone_uri
1040 1044 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1041 1045
1042 1046 push_uri=push_uri
1043 1047 if not isinstance(push_uri, Optional) else repo.push_uri,
1044 1048
1045 1049 repo_landing_rev=landing_rev
1046 1050 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1047 1051
1048 1052 repo_enable_statistics=enable_statistics
1049 1053 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1050 1054
1051 1055 repo_enable_locking=enable_locking
1052 1056 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1053 1057
1054 1058 repo_enable_downloads=enable_downloads
1055 1059 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1056 1060
1057 1061 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1058 1062 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1059 1063 request.translate, repo=repo)
1060 1064 ref_choices = list(set(ref_choices + [landing_ref]))
1061 1065
1062 1066 old_values = repo.get_api_data()
1063 1067 repo_type = repo.repo_type
1064 1068 schema = repo_schema.RepoSchema().bind(
1065 1069 repo_type_options=rhodecode.BACKENDS.keys(),
1066 1070 repo_ref_options=ref_choices,
1067 1071 repo_type=repo_type,
1068 1072 # user caller
1069 1073 user=apiuser,
1070 1074 old_values=old_values)
1071 1075 try:
1072 1076 schema_data = schema.deserialize(dict(
1073 1077 # we save old value, users cannot change type
1074 1078 repo_type=repo_type,
1075 1079
1076 1080 repo_name=updates['repo_name'],
1077 1081 repo_owner=updates['user'],
1078 1082 repo_description=updates['repo_description'],
1079 1083 repo_clone_uri=updates['clone_uri'],
1080 1084 repo_push_uri=updates['push_uri'],
1081 1085 repo_fork_of=updates['fork_id'],
1082 1086 repo_private=updates['repo_private'],
1083 1087 repo_landing_commit_ref=updates['repo_landing_rev'],
1084 1088 repo_enable_statistics=updates['repo_enable_statistics'],
1085 1089 repo_enable_downloads=updates['repo_enable_downloads'],
1086 1090 repo_enable_locking=updates['repo_enable_locking']))
1087 1091 except validation_schema.Invalid as err:
1088 1092 raise JSONRPCValidationError(colander_exc=err)
1089 1093
1090 1094 # save validated data back into the updates dict
1091 1095 validated_updates = dict(
1092 1096 repo_name=schema_data['repo_group']['repo_name_without_group'],
1093 1097 repo_group=schema_data['repo_group']['repo_group_id'],
1094 1098
1095 1099 user=schema_data['repo_owner'],
1096 1100 repo_description=schema_data['repo_description'],
1097 1101 repo_private=schema_data['repo_private'],
1098 1102 clone_uri=schema_data['repo_clone_uri'],
1099 1103 push_uri=schema_data['repo_push_uri'],
1100 1104 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1101 1105 repo_enable_statistics=schema_data['repo_enable_statistics'],
1102 1106 repo_enable_locking=schema_data['repo_enable_locking'],
1103 1107 repo_enable_downloads=schema_data['repo_enable_downloads'],
1104 1108 )
1105 1109
1106 1110 if schema_data['repo_fork_of']:
1107 1111 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1108 1112 validated_updates['fork_id'] = fork_repo.repo_id
1109 1113
1110 1114 # extra fields
1111 1115 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1112 1116 if fields:
1113 1117 validated_updates.update(fields)
1114 1118
1115 1119 try:
1116 1120 RepoModel().update(repo, **validated_updates)
1117 1121 audit_logger.store_api(
1118 1122 'repo.edit', action_data={'old_data': old_values},
1119 1123 user=apiuser, repo=repo)
1120 1124 Session().commit()
1121 1125 return {
1122 1126 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1123 1127 'repository': repo.get_api_data(include_secrets=include_secrets)
1124 1128 }
1125 1129 except Exception:
1126 1130 log.exception(
1127 1131 u"Exception while trying to update the repository %s",
1128 1132 repoid)
1129 1133 raise JSONRPCError('failed to update repo `%s`' % repoid)
1130 1134
1131 1135
1132 1136 @jsonrpc_method()
1133 1137 def fork_repo(request, apiuser, repoid, fork_name,
1134 1138 owner=Optional(OAttr('apiuser')),
1135 1139 description=Optional(''),
1136 1140 private=Optional(False),
1137 1141 clone_uri=Optional(None),
1138 1142 landing_rev=Optional(None),
1139 1143 copy_permissions=Optional(False)):
1140 1144 """
1141 1145 Creates a fork of the specified |repo|.
1142 1146
1143 1147 * If the fork_name contains "/", fork will be created inside
1144 1148 a repository group or nested repository groups
1145 1149
1146 1150 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1147 1151 inside group "foo/bar". You have to have permissions to access and
1148 1152 write to the last repository group ("bar" in this example)
1149 1153
1150 1154 This command can only be run using an |authtoken| with minimum
1151 1155 read permissions of the forked repo, create fork permissions for an user.
1152 1156
1153 1157 :param apiuser: This is filled automatically from the |authtoken|.
1154 1158 :type apiuser: AuthUser
1155 1159 :param repoid: Set repository name or repository ID.
1156 1160 :type repoid: str or int
1157 1161 :param fork_name: Set the fork name, including it's repository group membership.
1158 1162 :type fork_name: str
1159 1163 :param owner: Set the fork owner.
1160 1164 :type owner: str
1161 1165 :param description: Set the fork description.
1162 1166 :type description: str
1163 1167 :param copy_permissions: Copy permissions from parent |repo|. The
1164 1168 default is False.
1165 1169 :type copy_permissions: bool
1166 1170 :param private: Make the fork private. The default is False.
1167 1171 :type private: bool
1168 1172 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1169 1173
1170 1174 Example output:
1171 1175
1172 1176 .. code-block:: bash
1173 1177
1174 1178 id : <id_for_response>
1175 1179 api_key : "<api_key>"
1176 1180 args: {
1177 1181 "repoid" : "<reponame or repo_id>",
1178 1182 "fork_name": "<forkname>",
1179 1183 "owner": "<username or user_id = Optional(=apiuser)>",
1180 1184 "description": "<description>",
1181 1185 "copy_permissions": "<bool>",
1182 1186 "private": "<bool>",
1183 1187 "landing_rev": "<landing_rev>"
1184 1188 }
1185 1189
1186 1190 Example error output:
1187 1191
1188 1192 .. code-block:: bash
1189 1193
1190 1194 id : <id_given_in_input>
1191 1195 result: {
1192 1196 "msg": "Created fork of `<reponame>` as `<forkname>`",
1193 1197 "success": true,
1194 1198 "task": "<celery task id or None if done sync>"
1195 1199 }
1196 1200 error: null
1197 1201
1198 1202 """
1199 1203
1200 1204 repo = get_repo_or_error(repoid)
1201 1205 repo_name = repo.repo_name
1202 1206
1203 1207 if not has_superadmin_permission(apiuser):
1204 1208 # check if we have at least read permission for
1205 1209 # this repo that we fork !
1206 1210 _perms = (
1207 1211 'repository.admin', 'repository.write', 'repository.read')
1208 1212 validate_repo_permissions(apiuser, repoid, repo, _perms)
1209 1213
1210 1214 # check if the regular user has at least fork permissions as well
1211 1215 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1212 1216 raise JSONRPCForbidden()
1213 1217
1214 1218 # check if user can set owner parameter
1215 1219 owner = validate_set_owner_permissions(apiuser, owner)
1216 1220
1217 1221 description = Optional.extract(description)
1218 1222 copy_permissions = Optional.extract(copy_permissions)
1219 1223 clone_uri = Optional.extract(clone_uri)
1220 1224
1221 1225 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1222 1226 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1223 1227 ref_choices = list(set(ref_choices + [landing_ref]))
1224 1228 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1225 1229
1226 1230 private = Optional.extract(private)
1227 1231
1228 1232 schema = repo_schema.RepoSchema().bind(
1229 1233 repo_type_options=rhodecode.BACKENDS.keys(),
1230 1234 repo_ref_options=ref_choices,
1231 1235 repo_type=repo.repo_type,
1232 1236 # user caller
1233 1237 user=apiuser)
1234 1238
1235 1239 try:
1236 1240 schema_data = schema.deserialize(dict(
1237 1241 repo_name=fork_name,
1238 1242 repo_type=repo.repo_type,
1239 1243 repo_owner=owner.username,
1240 1244 repo_description=description,
1241 1245 repo_landing_commit_ref=landing_commit_ref,
1242 1246 repo_clone_uri=clone_uri,
1243 1247 repo_private=private,
1244 1248 repo_copy_permissions=copy_permissions))
1245 1249 except validation_schema.Invalid as err:
1246 1250 raise JSONRPCValidationError(colander_exc=err)
1247 1251
1248 1252 try:
1249 1253 data = {
1250 1254 'fork_parent_id': repo.repo_id,
1251 1255
1252 1256 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1253 1257 'repo_name_full': schema_data['repo_name'],
1254 1258 'repo_group': schema_data['repo_group']['repo_group_id'],
1255 1259 'repo_type': schema_data['repo_type'],
1256 1260 'description': schema_data['repo_description'],
1257 1261 'private': schema_data['repo_private'],
1258 1262 'copy_permissions': schema_data['repo_copy_permissions'],
1259 1263 'landing_rev': schema_data['repo_landing_commit_ref'],
1260 1264 }
1261 1265
1262 1266 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1263 1267 # no commit, it's done in RepoModel, or async via celery
1264 1268 task_id = get_task_id(task)
1265 1269
1266 1270 return {
1267 1271 'msg': 'Created fork of `%s` as `%s`' % (
1268 1272 repo.repo_name, schema_data['repo_name']),
1269 1273 'success': True, # cannot return the repo data here since fork
1270 1274 # can be done async
1271 1275 'task': task_id
1272 1276 }
1273 1277 except Exception:
1274 1278 log.exception(
1275 1279 u"Exception while trying to create fork %s",
1276 1280 schema_data['repo_name'])
1277 1281 raise JSONRPCError(
1278 1282 'failed to fork repository `%s` as `%s`' % (
1279 1283 repo_name, schema_data['repo_name']))
1280 1284
1281 1285
1282 1286 @jsonrpc_method()
1283 1287 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1284 1288 """
1285 1289 Deletes a repository.
1286 1290
1287 1291 * When the `forks` parameter is set it's possible to detach or delete
1288 1292 forks of deleted repository.
1289 1293
1290 1294 This command can only be run using an |authtoken| with admin
1291 1295 permissions on the |repo|.
1292 1296
1293 1297 :param apiuser: This is filled automatically from the |authtoken|.
1294 1298 :type apiuser: AuthUser
1295 1299 :param repoid: Set the repository name or repository ID.
1296 1300 :type repoid: str or int
1297 1301 :param forks: Set to `detach` or `delete` forks from the |repo|.
1298 1302 :type forks: Optional(str)
1299 1303
1300 1304 Example error output:
1301 1305
1302 1306 .. code-block:: bash
1303 1307
1304 1308 id : <id_given_in_input>
1305 1309 result: {
1306 1310 "msg": "Deleted repository `<reponame>`",
1307 1311 "success": true
1308 1312 }
1309 1313 error: null
1310 1314 """
1311 1315
1312 1316 repo = get_repo_or_error(repoid)
1313 1317 repo_name = repo.repo_name
1314 1318 if not has_superadmin_permission(apiuser):
1315 1319 _perms = ('repository.admin',)
1316 1320 validate_repo_permissions(apiuser, repoid, repo, _perms)
1317 1321
1318 1322 try:
1319 1323 handle_forks = Optional.extract(forks)
1320 1324 _forks_msg = ''
1321 1325 _forks = [f for f in repo.forks]
1322 1326 if handle_forks == 'detach':
1323 1327 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1324 1328 elif handle_forks == 'delete':
1325 1329 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1326 1330 elif _forks:
1327 1331 raise JSONRPCError(
1328 1332 'Cannot delete `%s` it still contains attached forks' %
1329 1333 (repo.repo_name,)
1330 1334 )
1331 1335 old_data = repo.get_api_data()
1332 1336 RepoModel().delete(repo, forks=forks)
1333 1337
1334 1338 repo = audit_logger.RepoWrap(repo_id=None,
1335 1339 repo_name=repo.repo_name)
1336 1340
1337 1341 audit_logger.store_api(
1338 1342 'repo.delete', action_data={'old_data': old_data},
1339 1343 user=apiuser, repo=repo)
1340 1344
1341 1345 ScmModel().mark_for_invalidation(repo_name, delete=True)
1342 1346 Session().commit()
1343 1347 return {
1344 1348 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1345 1349 'success': True
1346 1350 }
1347 1351 except Exception:
1348 1352 log.exception("Exception occurred while trying to delete repo")
1349 1353 raise JSONRPCError(
1350 1354 'failed to delete repository `%s`' % (repo_name,)
1351 1355 )
1352 1356
1353 1357
1354 1358 #TODO: marcink, change name ?
1355 1359 @jsonrpc_method()
1356 1360 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1357 1361 """
1358 1362 Invalidates the cache for the specified repository.
1359 1363
1360 1364 This command can only be run using an |authtoken| with admin rights to
1361 1365 the specified repository.
1362 1366
1363 1367 This command takes the following options:
1364 1368
1365 1369 :param apiuser: This is filled automatically from |authtoken|.
1366 1370 :type apiuser: AuthUser
1367 1371 :param repoid: Sets the repository name or repository ID.
1368 1372 :type repoid: str or int
1369 1373 :param delete_keys: This deletes the invalidated keys instead of
1370 1374 just flagging them.
1371 1375 :type delete_keys: Optional(``True`` | ``False``)
1372 1376
1373 1377 Example output:
1374 1378
1375 1379 .. code-block:: bash
1376 1380
1377 1381 id : <id_given_in_input>
1378 1382 result : {
1379 1383 'msg': Cache for repository `<repository name>` was invalidated,
1380 1384 'repository': <repository name>
1381 1385 }
1382 1386 error : null
1383 1387
1384 1388 Example error output:
1385 1389
1386 1390 .. code-block:: bash
1387 1391
1388 1392 id : <id_given_in_input>
1389 1393 result : null
1390 1394 error : {
1391 1395 'Error occurred during cache invalidation action'
1392 1396 }
1393 1397
1394 1398 """
1395 1399
1396 1400 repo = get_repo_or_error(repoid)
1397 1401 if not has_superadmin_permission(apiuser):
1398 1402 _perms = ('repository.admin', 'repository.write',)
1399 1403 validate_repo_permissions(apiuser, repoid, repo, _perms)
1400 1404
1401 1405 delete = Optional.extract(delete_keys)
1402 1406 try:
1403 1407 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1404 1408 return {
1405 1409 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1406 1410 'repository': repo.repo_name
1407 1411 }
1408 1412 except Exception:
1409 1413 log.exception(
1410 1414 "Exception occurred while trying to invalidate repo cache")
1411 1415 raise JSONRPCError(
1412 1416 'Error occurred during cache invalidation action'
1413 1417 )
1414 1418
1415 1419
1416 1420 #TODO: marcink, change name ?
1417 1421 @jsonrpc_method()
1418 1422 def lock(request, apiuser, repoid, locked=Optional(None),
1419 1423 userid=Optional(OAttr('apiuser'))):
1420 1424 """
1421 1425 Sets the lock state of the specified |repo| by the given user.
1422 1426 From more information, see :ref:`repo-locking`.
1423 1427
1424 1428 * If the ``userid`` option is not set, the repository is locked to the
1425 1429 user who called the method.
1426 1430 * If the ``locked`` parameter is not set, the current lock state of the
1427 1431 repository is displayed.
1428 1432
1429 1433 This command can only be run using an |authtoken| with admin rights to
1430 1434 the specified repository.
1431 1435
1432 1436 This command takes the following options:
1433 1437
1434 1438 :param apiuser: This is filled automatically from the |authtoken|.
1435 1439 :type apiuser: AuthUser
1436 1440 :param repoid: Sets the repository name or repository ID.
1437 1441 :type repoid: str or int
1438 1442 :param locked: Sets the lock state.
1439 1443 :type locked: Optional(``True`` | ``False``)
1440 1444 :param userid: Set the repository lock to this user.
1441 1445 :type userid: Optional(str or int)
1442 1446
1443 1447 Example error output:
1444 1448
1445 1449 .. code-block:: bash
1446 1450
1447 1451 id : <id_given_in_input>
1448 1452 result : {
1449 1453 'repo': '<reponame>',
1450 1454 'locked': <bool: lock state>,
1451 1455 'locked_since': <int: lock timestamp>,
1452 1456 'locked_by': <username of person who made the lock>,
1453 1457 'lock_reason': <str: reason for locking>,
1454 1458 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1455 1459 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1456 1460 or
1457 1461 'msg': 'Repo `<repository name>` not locked.'
1458 1462 or
1459 1463 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1460 1464 }
1461 1465 error : null
1462 1466
1463 1467 Example error output:
1464 1468
1465 1469 .. code-block:: bash
1466 1470
1467 1471 id : <id_given_in_input>
1468 1472 result : null
1469 1473 error : {
1470 1474 'Error occurred locking repository `<reponame>`'
1471 1475 }
1472 1476 """
1473 1477
1474 1478 repo = get_repo_or_error(repoid)
1475 1479 if not has_superadmin_permission(apiuser):
1476 1480 # check if we have at least write permission for this repo !
1477 1481 _perms = ('repository.admin', 'repository.write',)
1478 1482 validate_repo_permissions(apiuser, repoid, repo, _perms)
1479 1483
1480 1484 # make sure normal user does not pass someone else userid,
1481 1485 # he is not allowed to do that
1482 1486 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1483 1487 raise JSONRPCError('userid is not the same as your user')
1484 1488
1485 1489 if isinstance(userid, Optional):
1486 1490 userid = apiuser.user_id
1487 1491
1488 1492 user = get_user_or_error(userid)
1489 1493
1490 1494 if isinstance(locked, Optional):
1491 1495 lockobj = repo.locked
1492 1496
1493 1497 if lockobj[0] is None:
1494 1498 _d = {
1495 1499 'repo': repo.repo_name,
1496 1500 'locked': False,
1497 1501 'locked_since': None,
1498 1502 'locked_by': None,
1499 1503 'lock_reason': None,
1500 1504 'lock_state_changed': False,
1501 1505 'msg': 'Repo `%s` not locked.' % repo.repo_name
1502 1506 }
1503 1507 return _d
1504 1508 else:
1505 1509 _user_id, _time, _reason = lockobj
1506 1510 lock_user = get_user_or_error(userid)
1507 1511 _d = {
1508 1512 'repo': repo.repo_name,
1509 1513 'locked': True,
1510 1514 'locked_since': _time,
1511 1515 'locked_by': lock_user.username,
1512 1516 'lock_reason': _reason,
1513 1517 'lock_state_changed': False,
1514 1518 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1515 1519 % (repo.repo_name, lock_user.username,
1516 1520 json.dumps(time_to_datetime(_time))))
1517 1521 }
1518 1522 return _d
1519 1523
1520 1524 # force locked state through a flag
1521 1525 else:
1522 1526 locked = str2bool(locked)
1523 1527 lock_reason = Repository.LOCK_API
1524 1528 try:
1525 1529 if locked:
1526 1530 lock_time = time.time()
1527 1531 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1528 1532 else:
1529 1533 lock_time = None
1530 1534 Repository.unlock(repo)
1531 1535 _d = {
1532 1536 'repo': repo.repo_name,
1533 1537 'locked': locked,
1534 1538 'locked_since': lock_time,
1535 1539 'locked_by': user.username,
1536 1540 'lock_reason': lock_reason,
1537 1541 'lock_state_changed': True,
1538 1542 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1539 1543 % (user.username, repo.repo_name, locked))
1540 1544 }
1541 1545 return _d
1542 1546 except Exception:
1543 1547 log.exception(
1544 1548 "Exception occurred while trying to lock repository")
1545 1549 raise JSONRPCError(
1546 1550 'Error occurred locking repository `%s`' % repo.repo_name
1547 1551 )
1548 1552
1549 1553
1550 1554 @jsonrpc_method()
1551 1555 def comment_commit(
1552 1556 request, apiuser, repoid, commit_id, message, status=Optional(None),
1553 1557 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1554 1558 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1555 1559 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1556 1560 """
1557 1561 Set a commit comment, and optionally change the status of the commit.
1558 1562
1559 1563 :param apiuser: This is filled automatically from the |authtoken|.
1560 1564 :type apiuser: AuthUser
1561 1565 :param repoid: Set the repository name or repository ID.
1562 1566 :type repoid: str or int
1563 1567 :param commit_id: Specify the commit_id for which to set a comment.
1564 1568 :type commit_id: str
1565 1569 :param message: The comment text.
1566 1570 :type message: str
1567 1571 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1568 1572 'approved', 'rejected', 'under_review'
1569 1573 :type status: str
1570 1574 :param comment_type: Comment type, one of: 'note', 'todo'
1571 1575 :type comment_type: Optional(str), default: 'note'
1572 1576 :param resolves_comment_id: id of comment which this one will resolve
1573 1577 :type resolves_comment_id: Optional(int)
1574 1578 :param extra_recipients: list of user ids or usernames to add
1575 1579 notifications for this comment. Acts like a CC for notification
1576 1580 :type extra_recipients: Optional(list)
1577 1581 :param userid: Set the user name of the comment creator.
1578 1582 :type userid: Optional(str or int)
1579 1583 :param send_email: Define if this comment should also send email notification
1580 1584 :type send_email: Optional(bool)
1581 1585
1582 1586 Example error output:
1583 1587
1584 1588 .. code-block:: bash
1585 1589
1586 1590 {
1587 1591 "id" : <id_given_in_input>,
1588 1592 "result" : {
1589 1593 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1590 1594 "status_change": null or <status>,
1591 1595 "success": true
1592 1596 },
1593 1597 "error" : null
1594 1598 }
1595 1599
1596 1600 """
1597 1601 repo = get_repo_or_error(repoid)
1598 1602 if not has_superadmin_permission(apiuser):
1599 1603 _perms = ('repository.read', 'repository.write', 'repository.admin')
1600 1604 validate_repo_permissions(apiuser, repoid, repo, _perms)
1601 1605
1602 1606 try:
1603 1607 commit = repo.scm_instance().get_commit(commit_id=commit_id)
1604 1608 commit_id = commit.raw_id
1605 1609 except Exception as e:
1606 1610 log.exception('Failed to fetch commit')
1607 1611 raise JSONRPCError(safe_str(e))
1608 1612
1609 1613 if isinstance(userid, Optional):
1610 1614 userid = apiuser.user_id
1611 1615
1612 1616 user = get_user_or_error(userid)
1613 1617 status = Optional.extract(status)
1614 1618 comment_type = Optional.extract(comment_type)
1615 1619 resolves_comment_id = Optional.extract(resolves_comment_id)
1616 1620 extra_recipients = Optional.extract(extra_recipients)
1617 1621 send_email = Optional.extract(send_email, binary=True)
1618 1622
1619 1623 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1620 1624 if status and status not in allowed_statuses:
1621 1625 raise JSONRPCError('Bad status, must be on '
1622 1626 'of %s got %s' % (allowed_statuses, status,))
1623 1627
1624 1628 if resolves_comment_id:
1625 1629 comment = ChangesetComment.get(resolves_comment_id)
1626 1630 if not comment:
1627 1631 raise JSONRPCError(
1628 1632 'Invalid resolves_comment_id `%s` for this commit.'
1629 1633 % resolves_comment_id)
1630 1634 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1631 1635 raise JSONRPCError(
1632 1636 'Comment `%s` is wrong type for setting status to resolved.'
1633 1637 % resolves_comment_id)
1634 1638
1635 1639 try:
1636 1640 rc_config = SettingsModel().get_all_settings()
1637 1641 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1638 1642 status_change_label = ChangesetStatus.get_status_lbl(status)
1639 1643 comment = CommentsModel().create(
1640 1644 message, repo, user, commit_id=commit_id,
1641 1645 status_change=status_change_label,
1642 1646 status_change_type=status,
1643 1647 renderer=renderer,
1644 1648 comment_type=comment_type,
1645 1649 resolves_comment_id=resolves_comment_id,
1646 1650 auth_user=apiuser,
1647 1651 extra_recipients=extra_recipients,
1648 1652 send_email=send_email
1649 1653 )
1650 1654 if status:
1651 1655 # also do a status change
1652 1656 try:
1653 1657 ChangesetStatusModel().set_status(
1654 1658 repo, status, user, comment, revision=commit_id,
1655 1659 dont_allow_on_closed_pull_request=True
1656 1660 )
1657 1661 except StatusChangeOnClosedPullRequestError:
1658 1662 log.exception(
1659 1663 "Exception occurred while trying to change repo commit status")
1660 1664 msg = ('Changing status on a commit associated with '
1661 1665 'a closed pull request is not allowed')
1662 1666 raise JSONRPCError(msg)
1663 1667
1664 1668 CommentsModel().trigger_commit_comment_hook(
1665 1669 repo, apiuser, 'create',
1666 1670 data={'comment': comment, 'commit': commit})
1667 1671
1668 1672 Session().commit()
1669 1673 return {
1670 1674 'msg': (
1671 1675 'Commented on commit `%s` for repository `%s`' % (
1672 1676 comment.revision, repo.repo_name)),
1673 1677 'status_change': status,
1674 1678 'success': True,
1675 1679 }
1676 1680 except JSONRPCError:
1677 1681 # catch any inside errors, and re-raise them to prevent from
1678 1682 # below global catch to silence them
1679 1683 raise
1680 1684 except Exception:
1681 1685 log.exception("Exception occurred while trying to comment on commit")
1682 1686 raise JSONRPCError(
1683 1687 'failed to set comment on repository `%s`' % (repo.repo_name,)
1684 1688 )
1685 1689
1686 1690
1687 1691 @jsonrpc_method()
1688 1692 def get_repo_comments(request, apiuser, repoid,
1689 1693 commit_id=Optional(None), comment_type=Optional(None),
1690 1694 userid=Optional(None)):
1691 1695 """
1692 1696 Get all comments for a repository
1693 1697
1694 1698 :param apiuser: This is filled automatically from the |authtoken|.
1695 1699 :type apiuser: AuthUser
1696 1700 :param repoid: Set the repository name or repository ID.
1697 1701 :type repoid: str or int
1698 1702 :param commit_id: Optionally filter the comments by the commit_id
1699 1703 :type commit_id: Optional(str), default: None
1700 1704 :param comment_type: Optionally filter the comments by the comment_type
1701 1705 one of: 'note', 'todo'
1702 1706 :type comment_type: Optional(str), default: None
1703 1707 :param userid: Optionally filter the comments by the author of comment
1704 1708 :type userid: Optional(str or int), Default: None
1705 1709
1706 1710 Example error output:
1707 1711
1708 1712 .. code-block:: bash
1709 1713
1710 1714 {
1711 1715 "id" : <id_given_in_input>,
1712 1716 "result" : [
1713 1717 {
1714 1718 "comment_author": <USER_DETAILS>,
1715 1719 "comment_created_on": "2017-02-01T14:38:16.309",
1716 1720 "comment_f_path": "file.txt",
1717 1721 "comment_id": 282,
1718 1722 "comment_lineno": "n1",
1719 1723 "comment_resolved_by": null,
1720 1724 "comment_status": [],
1721 1725 "comment_text": "This file needs a header",
1722 "comment_type": "todo"
1726 "comment_type": "todo",
1727 "comment_last_version: 0
1723 1728 }
1724 1729 ],
1725 1730 "error" : null
1726 1731 }
1727 1732
1728 1733 """
1729 1734 repo = get_repo_or_error(repoid)
1730 1735 if not has_superadmin_permission(apiuser):
1731 1736 _perms = ('repository.read', 'repository.write', 'repository.admin')
1732 1737 validate_repo_permissions(apiuser, repoid, repo, _perms)
1733 1738
1734 1739 commit_id = Optional.extract(commit_id)
1735 1740
1736 1741 userid = Optional.extract(userid)
1737 1742 if userid:
1738 1743 user = get_user_or_error(userid)
1739 1744 else:
1740 1745 user = None
1741 1746
1742 1747 comment_type = Optional.extract(comment_type)
1743 1748 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1744 1749 raise JSONRPCError(
1745 1750 'comment_type must be one of `{}` got {}'.format(
1746 1751 ChangesetComment.COMMENT_TYPES, comment_type)
1747 1752 )
1748 1753
1749 1754 comments = CommentsModel().get_repository_comments(
1750 1755 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1751 1756 return comments
1752 1757
1753 1758
1754 1759 @jsonrpc_method()
1760 def get_comment(request, apiuser, comment_id):
1761 """
1762 Get single comment from repository or pull_request
1763
1764 :param apiuser: This is filled automatically from the |authtoken|.
1765 :type apiuser: AuthUser
1766 :param comment_id: comment id found in the URL of comment
1767 :type comment_id: str or int
1768
1769 Example error output:
1770
1771 .. code-block:: bash
1772
1773 {
1774 "id" : <id_given_in_input>,
1775 "result" : {
1776 "comment_author": <USER_DETAILS>,
1777 "comment_created_on": "2017-02-01T14:38:16.309",
1778 "comment_f_path": "file.txt",
1779 "comment_id": 282,
1780 "comment_lineno": "n1",
1781 "comment_resolved_by": null,
1782 "comment_status": [],
1783 "comment_text": "This file needs a header",
1784 "comment_type": "todo",
1785 "comment_last_version: 0
1786 },
1787 "error" : null
1788 }
1789
1790 """
1791
1792 comment = ChangesetComment.get(comment_id)
1793 if not comment:
1794 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1795
1796 perms = ('repository.read', 'repository.write', 'repository.admin')
1797 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1798 (user=apiuser, repo_name=comment.repo.repo_name)
1799
1800 if not has_comment_perm:
1801 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1802
1803 return comment
1804
1805
1806 @jsonrpc_method()
1807 def edit_comment(request, apiuser, message, comment_id, version,
1808 userid=Optional(OAttr('apiuser'))):
1809 """
1810 Edit comment on the pull request or commit,
1811 specified by the `comment_id` and version. Initially version should be 0
1812
1813 :param apiuser: This is filled automatically from the |authtoken|.
1814 :type apiuser: AuthUser
1815 :param comment_id: Specify the comment_id for editing
1816 :type comment_id: int
1817 :param version: version of the comment that will be created, starts from 0
1818 :type version: int
1819 :param message: The text content of the comment.
1820 :type message: str
1821 :param userid: Comment on the pull request as this user
1822 :type userid: Optional(str or int)
1823
1824 Example output:
1825
1826 .. code-block:: bash
1827
1828 id : <id_given_in_input>
1829 result : {
1830 "comment": "<comment data>",
1831 "version": "<Integer>",
1832 },
1833 error : null
1834 """
1835
1836 auth_user = apiuser
1837 comment = ChangesetComment.get(comment_id)
1838 if not comment:
1839 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1840
1841 is_super_admin = has_superadmin_permission(apiuser)
1842 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1843 (user=apiuser, repo_name=comment.repo.repo_name)
1844
1845 if not isinstance(userid, Optional):
1846 if is_super_admin or is_repo_admin:
1847 apiuser = get_user_or_error(userid)
1848 auth_user = apiuser.AuthUser()
1849 else:
1850 raise JSONRPCError('userid is not the same as your user')
1851
1852 comment_author = comment.author.user_id == auth_user.user_id
1853 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1854 raise JSONRPCError("you don't have access to edit this comment")
1855
1856 try:
1857 comment_history = CommentsModel().edit(
1858 comment_id=comment_id,
1859 text=message,
1860 auth_user=auth_user,
1861 version=version,
1862 )
1863 Session().commit()
1864 except CommentVersionMismatch:
1865 raise JSONRPCError(
1866 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1867 )
1868 if not comment_history and not message:
1869 raise JSONRPCError(
1870 "comment ({}) can't be changed with empty string".format(comment_id)
1871 )
1872 data = {
1873 'comment': comment,
1874 'version': comment_history.version if comment_history else None,
1875 }
1876 return data
1877
1878
1879 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1880 # @jsonrpc_method()
1881 # def delete_comment(request, apiuser, comment_id):
1882 # auth_user = apiuser
1883 #
1884 # comment = ChangesetComment.get(comment_id)
1885 # if not comment:
1886 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1887 #
1888 # is_super_admin = has_superadmin_permission(apiuser)
1889 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1890 # (user=apiuser, repo_name=comment.repo.repo_name)
1891 #
1892 # comment_author = comment.author.user_id == auth_user.user_id
1893 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1894 # raise JSONRPCError("you don't have access to edit this comment")
1895
1896 @jsonrpc_method()
1755 1897 def grant_user_permission(request, apiuser, repoid, userid, perm):
1756 1898 """
1757 1899 Grant permissions for the specified user on the given repository,
1758 1900 or update existing permissions if found.
1759 1901
1760 1902 This command can only be run using an |authtoken| with admin
1761 1903 permissions on the |repo|.
1762 1904
1763 1905 :param apiuser: This is filled automatically from the |authtoken|.
1764 1906 :type apiuser: AuthUser
1765 1907 :param repoid: Set the repository name or repository ID.
1766 1908 :type repoid: str or int
1767 1909 :param userid: Set the user name.
1768 1910 :type userid: str
1769 1911 :param perm: Set the user permissions, using the following format
1770 1912 ``(repository.(none|read|write|admin))``
1771 1913 :type perm: str
1772 1914
1773 1915 Example output:
1774 1916
1775 1917 .. code-block:: bash
1776 1918
1777 1919 id : <id_given_in_input>
1778 1920 result: {
1779 1921 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1780 1922 "success": true
1781 1923 }
1782 1924 error: null
1783 1925 """
1784 1926
1785 1927 repo = get_repo_or_error(repoid)
1786 1928 user = get_user_or_error(userid)
1787 1929 perm = get_perm_or_error(perm)
1788 1930 if not has_superadmin_permission(apiuser):
1789 1931 _perms = ('repository.admin',)
1790 1932 validate_repo_permissions(apiuser, repoid, repo, _perms)
1791 1933
1792 1934 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1793 1935 try:
1794 1936 changes = RepoModel().update_permissions(
1795 1937 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1796 1938
1797 1939 action_data = {
1798 1940 'added': changes['added'],
1799 1941 'updated': changes['updated'],
1800 1942 'deleted': changes['deleted'],
1801 1943 }
1802 1944 audit_logger.store_api(
1803 1945 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1804 1946 Session().commit()
1805 1947 PermissionModel().flush_user_permission_caches(changes)
1806 1948
1807 1949 return {
1808 1950 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1809 1951 perm.permission_name, user.username, repo.repo_name
1810 1952 ),
1811 1953 'success': True
1812 1954 }
1813 1955 except Exception:
1814 1956 log.exception("Exception occurred while trying edit permissions for repo")
1815 1957 raise JSONRPCError(
1816 1958 'failed to edit permission for user: `%s` in repo: `%s`' % (
1817 1959 userid, repoid
1818 1960 )
1819 1961 )
1820 1962
1821 1963
1822 1964 @jsonrpc_method()
1823 1965 def revoke_user_permission(request, apiuser, repoid, userid):
1824 1966 """
1825 1967 Revoke permission for a user on the specified repository.
1826 1968
1827 1969 This command can only be run using an |authtoken| with admin
1828 1970 permissions on the |repo|.
1829 1971
1830 1972 :param apiuser: This is filled automatically from the |authtoken|.
1831 1973 :type apiuser: AuthUser
1832 1974 :param repoid: Set the repository name or repository ID.
1833 1975 :type repoid: str or int
1834 1976 :param userid: Set the user name of revoked user.
1835 1977 :type userid: str or int
1836 1978
1837 1979 Example error output:
1838 1980
1839 1981 .. code-block:: bash
1840 1982
1841 1983 id : <id_given_in_input>
1842 1984 result: {
1843 1985 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1844 1986 "success": true
1845 1987 }
1846 1988 error: null
1847 1989 """
1848 1990
1849 1991 repo = get_repo_or_error(repoid)
1850 1992 user = get_user_or_error(userid)
1851 1993 if not has_superadmin_permission(apiuser):
1852 1994 _perms = ('repository.admin',)
1853 1995 validate_repo_permissions(apiuser, repoid, repo, _perms)
1854 1996
1855 1997 perm_deletions = [[user.user_id, None, "user"]]
1856 1998 try:
1857 1999 changes = RepoModel().update_permissions(
1858 2000 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1859 2001
1860 2002 action_data = {
1861 2003 'added': changes['added'],
1862 2004 'updated': changes['updated'],
1863 2005 'deleted': changes['deleted'],
1864 2006 }
1865 2007 audit_logger.store_api(
1866 2008 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1867 2009 Session().commit()
1868 2010 PermissionModel().flush_user_permission_caches(changes)
1869 2011
1870 2012 return {
1871 2013 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1872 2014 user.username, repo.repo_name
1873 2015 ),
1874 2016 'success': True
1875 2017 }
1876 2018 except Exception:
1877 2019 log.exception("Exception occurred while trying revoke permissions to repo")
1878 2020 raise JSONRPCError(
1879 2021 'failed to edit permission for user: `%s` in repo: `%s`' % (
1880 2022 userid, repoid
1881 2023 )
1882 2024 )
1883 2025
1884 2026
1885 2027 @jsonrpc_method()
1886 2028 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1887 2029 """
1888 2030 Grant permission for a user group on the specified repository,
1889 2031 or update existing permissions.
1890 2032
1891 2033 This command can only be run using an |authtoken| with admin
1892 2034 permissions on the |repo|.
1893 2035
1894 2036 :param apiuser: This is filled automatically from the |authtoken|.
1895 2037 :type apiuser: AuthUser
1896 2038 :param repoid: Set the repository name or repository ID.
1897 2039 :type repoid: str or int
1898 2040 :param usergroupid: Specify the ID of the user group.
1899 2041 :type usergroupid: str or int
1900 2042 :param perm: Set the user group permissions using the following
1901 2043 format: (repository.(none|read|write|admin))
1902 2044 :type perm: str
1903 2045
1904 2046 Example output:
1905 2047
1906 2048 .. code-block:: bash
1907 2049
1908 2050 id : <id_given_in_input>
1909 2051 result : {
1910 2052 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1911 2053 "success": true
1912 2054
1913 2055 }
1914 2056 error : null
1915 2057
1916 2058 Example error output:
1917 2059
1918 2060 .. code-block:: bash
1919 2061
1920 2062 id : <id_given_in_input>
1921 2063 result : null
1922 2064 error : {
1923 2065 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1924 2066 }
1925 2067
1926 2068 """
1927 2069
1928 2070 repo = get_repo_or_error(repoid)
1929 2071 perm = get_perm_or_error(perm)
1930 2072 if not has_superadmin_permission(apiuser):
1931 2073 _perms = ('repository.admin',)
1932 2074 validate_repo_permissions(apiuser, repoid, repo, _perms)
1933 2075
1934 2076 user_group = get_user_group_or_error(usergroupid)
1935 2077 if not has_superadmin_permission(apiuser):
1936 2078 # check if we have at least read permission for this user group !
1937 2079 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1938 2080 if not HasUserGroupPermissionAnyApi(*_perms)(
1939 2081 user=apiuser, user_group_name=user_group.users_group_name):
1940 2082 raise JSONRPCError(
1941 2083 'user group `%s` does not exist' % (usergroupid,))
1942 2084
1943 2085 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1944 2086 try:
1945 2087 changes = RepoModel().update_permissions(
1946 2088 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1947 2089 action_data = {
1948 2090 'added': changes['added'],
1949 2091 'updated': changes['updated'],
1950 2092 'deleted': changes['deleted'],
1951 2093 }
1952 2094 audit_logger.store_api(
1953 2095 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1954 2096 Session().commit()
1955 2097 PermissionModel().flush_user_permission_caches(changes)
1956 2098
1957 2099 return {
1958 2100 'msg': 'Granted perm: `%s` for user group: `%s` in '
1959 2101 'repo: `%s`' % (
1960 2102 perm.permission_name, user_group.users_group_name,
1961 2103 repo.repo_name
1962 2104 ),
1963 2105 'success': True
1964 2106 }
1965 2107 except Exception:
1966 2108 log.exception(
1967 2109 "Exception occurred while trying change permission on repo")
1968 2110 raise JSONRPCError(
1969 2111 'failed to edit permission for user group: `%s` in '
1970 2112 'repo: `%s`' % (
1971 2113 usergroupid, repo.repo_name
1972 2114 )
1973 2115 )
1974 2116
1975 2117
1976 2118 @jsonrpc_method()
1977 2119 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1978 2120 """
1979 2121 Revoke the permissions of a user group on a given repository.
1980 2122
1981 2123 This command can only be run using an |authtoken| with admin
1982 2124 permissions on the |repo|.
1983 2125
1984 2126 :param apiuser: This is filled automatically from the |authtoken|.
1985 2127 :type apiuser: AuthUser
1986 2128 :param repoid: Set the repository name or repository ID.
1987 2129 :type repoid: str or int
1988 2130 :param usergroupid: Specify the user group ID.
1989 2131 :type usergroupid: str or int
1990 2132
1991 2133 Example output:
1992 2134
1993 2135 .. code-block:: bash
1994 2136
1995 2137 id : <id_given_in_input>
1996 2138 result: {
1997 2139 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1998 2140 "success": true
1999 2141 }
2000 2142 error: null
2001 2143 """
2002 2144
2003 2145 repo = get_repo_or_error(repoid)
2004 2146 if not has_superadmin_permission(apiuser):
2005 2147 _perms = ('repository.admin',)
2006 2148 validate_repo_permissions(apiuser, repoid, repo, _perms)
2007 2149
2008 2150 user_group = get_user_group_or_error(usergroupid)
2009 2151 if not has_superadmin_permission(apiuser):
2010 2152 # check if we have at least read permission for this user group !
2011 2153 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2012 2154 if not HasUserGroupPermissionAnyApi(*_perms)(
2013 2155 user=apiuser, user_group_name=user_group.users_group_name):
2014 2156 raise JSONRPCError(
2015 2157 'user group `%s` does not exist' % (usergroupid,))
2016 2158
2017 2159 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2018 2160 try:
2019 2161 changes = RepoModel().update_permissions(
2020 2162 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2021 2163 action_data = {
2022 2164 'added': changes['added'],
2023 2165 'updated': changes['updated'],
2024 2166 'deleted': changes['deleted'],
2025 2167 }
2026 2168 audit_logger.store_api(
2027 2169 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2028 2170 Session().commit()
2029 2171 PermissionModel().flush_user_permission_caches(changes)
2030 2172
2031 2173 return {
2032 2174 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2033 2175 user_group.users_group_name, repo.repo_name
2034 2176 ),
2035 2177 'success': True
2036 2178 }
2037 2179 except Exception:
2038 2180 log.exception("Exception occurred while trying revoke "
2039 2181 "user group permission on repo")
2040 2182 raise JSONRPCError(
2041 2183 'failed to edit permission for user group: `%s` in '
2042 2184 'repo: `%s`' % (
2043 2185 user_group.users_group_name, repo.repo_name
2044 2186 )
2045 2187 )
2046 2188
2047 2189
2048 2190 @jsonrpc_method()
2049 2191 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2050 2192 """
2051 2193 Triggers a pull on the given repository from a remote location. You
2052 2194 can use this to keep remote repositories up-to-date.
2053 2195
2054 2196 This command can only be run using an |authtoken| with admin
2055 2197 rights to the specified repository. For more information,
2056 2198 see :ref:`config-token-ref`.
2057 2199
2058 2200 This command takes the following options:
2059 2201
2060 2202 :param apiuser: This is filled automatically from the |authtoken|.
2061 2203 :type apiuser: AuthUser
2062 2204 :param repoid: The repository name or repository ID.
2063 2205 :type repoid: str or int
2064 2206 :param remote_uri: Optional remote URI to pass in for pull
2065 2207 :type remote_uri: str
2066 2208
2067 2209 Example output:
2068 2210
2069 2211 .. code-block:: bash
2070 2212
2071 2213 id : <id_given_in_input>
2072 2214 result : {
2073 2215 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2074 2216 "repository": "<repository name>"
2075 2217 }
2076 2218 error : null
2077 2219
2078 2220 Example error output:
2079 2221
2080 2222 .. code-block:: bash
2081 2223
2082 2224 id : <id_given_in_input>
2083 2225 result : null
2084 2226 error : {
2085 2227 "Unable to push changes from `<remote_url>`"
2086 2228 }
2087 2229
2088 2230 """
2089 2231
2090 2232 repo = get_repo_or_error(repoid)
2091 2233 remote_uri = Optional.extract(remote_uri)
2092 2234 remote_uri_display = remote_uri or repo.clone_uri_hidden
2093 2235 if not has_superadmin_permission(apiuser):
2094 2236 _perms = ('repository.admin',)
2095 2237 validate_repo_permissions(apiuser, repoid, repo, _perms)
2096 2238
2097 2239 try:
2098 2240 ScmModel().pull_changes(
2099 2241 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2100 2242 return {
2101 2243 'msg': 'Pulled from url `%s` on repo `%s`' % (
2102 2244 remote_uri_display, repo.repo_name),
2103 2245 'repository': repo.repo_name
2104 2246 }
2105 2247 except Exception:
2106 2248 log.exception("Exception occurred while trying to "
2107 2249 "pull changes from remote location")
2108 2250 raise JSONRPCError(
2109 2251 'Unable to pull changes from `%s`' % remote_uri_display
2110 2252 )
2111 2253
2112 2254
2113 2255 @jsonrpc_method()
2114 2256 def strip(request, apiuser, repoid, revision, branch):
2115 2257 """
2116 2258 Strips the given revision from the specified repository.
2117 2259
2118 2260 * This will remove the revision and all of its decendants.
2119 2261
2120 2262 This command can only be run using an |authtoken| with admin rights to
2121 2263 the specified repository.
2122 2264
2123 2265 This command takes the following options:
2124 2266
2125 2267 :param apiuser: This is filled automatically from the |authtoken|.
2126 2268 :type apiuser: AuthUser
2127 2269 :param repoid: The repository name or repository ID.
2128 2270 :type repoid: str or int
2129 2271 :param revision: The revision you wish to strip.
2130 2272 :type revision: str
2131 2273 :param branch: The branch from which to strip the revision.
2132 2274 :type branch: str
2133 2275
2134 2276 Example output:
2135 2277
2136 2278 .. code-block:: bash
2137 2279
2138 2280 id : <id_given_in_input>
2139 2281 result : {
2140 2282 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2141 2283 "repository": "<repository name>"
2142 2284 }
2143 2285 error : null
2144 2286
2145 2287 Example error output:
2146 2288
2147 2289 .. code-block:: bash
2148 2290
2149 2291 id : <id_given_in_input>
2150 2292 result : null
2151 2293 error : {
2152 2294 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2153 2295 }
2154 2296
2155 2297 """
2156 2298
2157 2299 repo = get_repo_or_error(repoid)
2158 2300 if not has_superadmin_permission(apiuser):
2159 2301 _perms = ('repository.admin',)
2160 2302 validate_repo_permissions(apiuser, repoid, repo, _perms)
2161 2303
2162 2304 try:
2163 2305 ScmModel().strip(repo, revision, branch)
2164 2306 audit_logger.store_api(
2165 2307 'repo.commit.strip', action_data={'commit_id': revision},
2166 2308 repo=repo,
2167 2309 user=apiuser, commit=True)
2168 2310
2169 2311 return {
2170 2312 'msg': 'Stripped commit %s from repo `%s`' % (
2171 2313 revision, repo.repo_name),
2172 2314 'repository': repo.repo_name
2173 2315 }
2174 2316 except Exception:
2175 2317 log.exception("Exception while trying to strip")
2176 2318 raise JSONRPCError(
2177 2319 'Unable to strip commit %s from repo `%s`' % (
2178 2320 revision, repo.repo_name)
2179 2321 )
2180 2322
2181 2323
2182 2324 @jsonrpc_method()
2183 2325 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2184 2326 """
2185 2327 Returns all settings for a repository. If key is given it only returns the
2186 2328 setting identified by the key or null.
2187 2329
2188 2330 :param apiuser: This is filled automatically from the |authtoken|.
2189 2331 :type apiuser: AuthUser
2190 2332 :param repoid: The repository name or repository id.
2191 2333 :type repoid: str or int
2192 2334 :param key: Key of the setting to return.
2193 2335 :type: key: Optional(str)
2194 2336
2195 2337 Example output:
2196 2338
2197 2339 .. code-block:: bash
2198 2340
2199 2341 {
2200 2342 "error": null,
2201 2343 "id": 237,
2202 2344 "result": {
2203 2345 "extensions_largefiles": true,
2204 2346 "extensions_evolve": true,
2205 2347 "hooks_changegroup_push_logger": true,
2206 2348 "hooks_changegroup_repo_size": false,
2207 2349 "hooks_outgoing_pull_logger": true,
2208 2350 "phases_publish": "True",
2209 2351 "rhodecode_hg_use_rebase_for_merging": true,
2210 2352 "rhodecode_pr_merge_enabled": true,
2211 2353 "rhodecode_use_outdated_comments": true
2212 2354 }
2213 2355 }
2214 2356 """
2215 2357
2216 2358 # Restrict access to this api method to admins only.
2217 2359 if not has_superadmin_permission(apiuser):
2218 2360 raise JSONRPCForbidden()
2219 2361
2220 2362 try:
2221 2363 repo = get_repo_or_error(repoid)
2222 2364 settings_model = VcsSettingsModel(repo=repo)
2223 2365 settings = settings_model.get_global_settings()
2224 2366 settings.update(settings_model.get_repo_settings())
2225 2367
2226 2368 # If only a single setting is requested fetch it from all settings.
2227 2369 key = Optional.extract(key)
2228 2370 if key is not None:
2229 2371 settings = settings.get(key, None)
2230 2372 except Exception:
2231 2373 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2232 2374 log.exception(msg)
2233 2375 raise JSONRPCError(msg)
2234 2376
2235 2377 return settings
2236 2378
2237 2379
2238 2380 @jsonrpc_method()
2239 2381 def set_repo_settings(request, apiuser, repoid, settings):
2240 2382 """
2241 2383 Update repository settings. Returns true on success.
2242 2384
2243 2385 :param apiuser: This is filled automatically from the |authtoken|.
2244 2386 :type apiuser: AuthUser
2245 2387 :param repoid: The repository name or repository id.
2246 2388 :type repoid: str or int
2247 2389 :param settings: The new settings for the repository.
2248 2390 :type: settings: dict
2249 2391
2250 2392 Example output:
2251 2393
2252 2394 .. code-block:: bash
2253 2395
2254 2396 {
2255 2397 "error": null,
2256 2398 "id": 237,
2257 2399 "result": true
2258 2400 }
2259 2401 """
2260 2402 # Restrict access to this api method to admins only.
2261 2403 if not has_superadmin_permission(apiuser):
2262 2404 raise JSONRPCForbidden()
2263 2405
2264 2406 if type(settings) is not dict:
2265 2407 raise JSONRPCError('Settings have to be a JSON Object.')
2266 2408
2267 2409 try:
2268 2410 settings_model = VcsSettingsModel(repo=repoid)
2269 2411
2270 2412 # Merge global, repo and incoming settings.
2271 2413 new_settings = settings_model.get_global_settings()
2272 2414 new_settings.update(settings_model.get_repo_settings())
2273 2415 new_settings.update(settings)
2274 2416
2275 2417 # Update the settings.
2276 2418 inherit_global_settings = new_settings.get(
2277 2419 'inherit_global_settings', False)
2278 2420 settings_model.create_or_update_repo_settings(
2279 2421 new_settings, inherit_global_settings=inherit_global_settings)
2280 2422 Session().commit()
2281 2423 except Exception:
2282 2424 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2283 2425 log.exception(msg)
2284 2426 raise JSONRPCError(msg)
2285 2427
2286 2428 # Indicate success.
2287 2429 return True
2288 2430
2289 2431
2290 2432 @jsonrpc_method()
2291 2433 def maintenance(request, apiuser, repoid):
2292 2434 """
2293 2435 Triggers a maintenance on the given repository.
2294 2436
2295 2437 This command can only be run using an |authtoken| with admin
2296 2438 rights to the specified repository. For more information,
2297 2439 see :ref:`config-token-ref`.
2298 2440
2299 2441 This command takes the following options:
2300 2442
2301 2443 :param apiuser: This is filled automatically from the |authtoken|.
2302 2444 :type apiuser: AuthUser
2303 2445 :param repoid: The repository name or repository ID.
2304 2446 :type repoid: str or int
2305 2447
2306 2448 Example output:
2307 2449
2308 2450 .. code-block:: bash
2309 2451
2310 2452 id : <id_given_in_input>
2311 2453 result : {
2312 2454 "msg": "executed maintenance command",
2313 2455 "executed_actions": [
2314 2456 <action_message>, <action_message2>...
2315 2457 ],
2316 2458 "repository": "<repository name>"
2317 2459 }
2318 2460 error : null
2319 2461
2320 2462 Example error output:
2321 2463
2322 2464 .. code-block:: bash
2323 2465
2324 2466 id : <id_given_in_input>
2325 2467 result : null
2326 2468 error : {
2327 2469 "Unable to execute maintenance on `<reponame>`"
2328 2470 }
2329 2471
2330 2472 """
2331 2473
2332 2474 repo = get_repo_or_error(repoid)
2333 2475 if not has_superadmin_permission(apiuser):
2334 2476 _perms = ('repository.admin',)
2335 2477 validate_repo_permissions(apiuser, repoid, repo, _perms)
2336 2478
2337 2479 try:
2338 2480 maintenance = repo_maintenance.RepoMaintenance()
2339 2481 executed_actions = maintenance.execute(repo)
2340 2482
2341 2483 return {
2342 2484 'msg': 'executed maintenance command',
2343 2485 'executed_actions': executed_actions,
2344 2486 'repository': repo.repo_name
2345 2487 }
2346 2488 except Exception:
2347 2489 log.exception("Exception occurred while trying to run maintenance")
2348 2490 raise JSONRPCError(
2349 2491 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,5663 +1,5672 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 time
28 28 import string
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import uuid
33 33 import warnings
34 34 import ipaddress
35 35 import functools
36 36 import traceback
37 37 import collections
38 38
39 39 from sqlalchemy import (
40 40 or_, and_, not_, func, cast, TypeDecorator, event,
41 41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 43 Text, Float, PickleType, BigInteger)
44 44 from sqlalchemy.sql.expression import true, false, case
45 45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 46 from sqlalchemy.orm import (
47 47 relationship, joinedload, class_mapper, validates, aliased)
48 48 from sqlalchemy.ext.declarative import declared_attr
49 49 from sqlalchemy.ext.hybrid import hybrid_property
50 50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 51 from sqlalchemy.dialects.mysql import LONGTEXT
52 52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 53 from pyramid import compat
54 54 from pyramid.threadlocal import get_current_request
55 55 from webhelpers2.text import remove_formatting
56 56
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 60 from rhodecode.lib.utils2 import (
61 61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 65 JsonRaw
66 66 from rhodecode.lib.ext_json import json
67 67 from rhodecode.lib.caching_query import FromCache
68 68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 69 from rhodecode.lib.encrypt2 import Encryptor
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
73 73
74 74 URL_SEP = '/'
75 75 log = logging.getLogger(__name__)
76 76
77 77 # =============================================================================
78 78 # BASE CLASSES
79 79 # =============================================================================
80 80
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 84 ENCRYPTION_KEY = None
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
88 88 PERMISSION_TYPE_SORT = {
89 89 'admin': '####',
90 90 'write': '###',
91 91 'read': '##',
92 92 'none': '#',
93 93 }
94 94
95 95
96 96 def display_user_sort(obj):
97 97 """
98 98 Sort function used to sort permissions in .permissions() function of
99 99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 100 of all other resources
101 101 """
102 102
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 106 extra_sort_num = '1' # default
107 107
108 108 # NOTE(dan): inactive duplicates goes last
109 109 if getattr(obj, 'duplicate_perm', None):
110 110 extra_sort_num = '9'
111 111 return prefix + extra_sort_num + obj.username
112 112
113 113
114 114 def display_user_group_sort(obj):
115 115 """
116 116 Sort function used to sort permissions in .permissions() function of
117 117 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 118 of all other resources
119 119 """
120 120
121 121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 122 return prefix + obj.users_group_name
123 123
124 124
125 125 def _hash_key(k):
126 126 return sha1_safe(k)
127 127
128 128
129 129 def in_filter_generator(qry, items, limit=500):
130 130 """
131 131 Splits IN() into multiple with OR
132 132 e.g.::
133 133 cnt = Repository.query().filter(
134 134 or_(
135 135 *in_filter_generator(Repository.repo_id, range(100000))
136 136 )).count()
137 137 """
138 138 if not items:
139 139 # empty list will cause empty query which might cause security issues
140 140 # this can lead to hidden unpleasant results
141 141 items = [-1]
142 142
143 143 parts = []
144 144 for chunk in xrange(0, len(items), limit):
145 145 parts.append(
146 146 qry.in_(items[chunk: chunk + limit])
147 147 )
148 148
149 149 return parts
150 150
151 151
152 152 base_table_args = {
153 153 'extend_existing': True,
154 154 'mysql_engine': 'InnoDB',
155 155 'mysql_charset': 'utf8',
156 156 'sqlite_autoincrement': True
157 157 }
158 158
159 159
160 160 class EncryptedTextValue(TypeDecorator):
161 161 """
162 162 Special column for encrypted long text data, use like::
163 163
164 164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165 165
166 166 This column is intelligent so if value is in unencrypted form it return
167 167 unencrypted form, but on save it always encrypts
168 168 """
169 169 impl = Text
170 170
171 171 def process_bind_param(self, value, dialect):
172 172 """
173 173 Setter for storing value
174 174 """
175 175 import rhodecode
176 176 if not value:
177 177 return value
178 178
179 179 # protect against double encrypting if values is already encrypted
180 180 if value.startswith('enc$aes$') \
181 181 or value.startswith('enc$aes_hmac$') \
182 182 or value.startswith('enc2$'):
183 183 raise ValueError('value needs to be in unencrypted format, '
184 184 'ie. not starting with enc$ or enc2$')
185 185
186 186 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 187 if algo == 'aes':
188 188 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
189 189 elif algo == 'fernet':
190 190 return Encryptor(ENCRYPTION_KEY).encrypt(value)
191 191 else:
192 192 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
193 193
194 194 def process_result_value(self, value, dialect):
195 195 """
196 196 Getter for retrieving value
197 197 """
198 198
199 199 import rhodecode
200 200 if not value:
201 201 return value
202 202
203 203 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
204 204 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
205 205 if algo == 'aes':
206 206 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
207 207 elif algo == 'fernet':
208 208 return Encryptor(ENCRYPTION_KEY).decrypt(value)
209 209 else:
210 210 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
211 211 return decrypted_data
212 212
213 213
214 214 class BaseModel(object):
215 215 """
216 216 Base Model for all classes
217 217 """
218 218
219 219 @classmethod
220 220 def _get_keys(cls):
221 221 """return column names for this model """
222 222 return class_mapper(cls).c.keys()
223 223
224 224 def get_dict(self):
225 225 """
226 226 return dict with keys and values corresponding
227 227 to this model data """
228 228
229 229 d = {}
230 230 for k in self._get_keys():
231 231 d[k] = getattr(self, k)
232 232
233 233 # also use __json__() if present to get additional fields
234 234 _json_attr = getattr(self, '__json__', None)
235 235 if _json_attr:
236 236 # update with attributes from __json__
237 237 if callable(_json_attr):
238 238 _json_attr = _json_attr()
239 239 for k, val in _json_attr.iteritems():
240 240 d[k] = val
241 241 return d
242 242
243 243 def get_appstruct(self):
244 244 """return list with keys and values tuples corresponding
245 245 to this model data """
246 246
247 247 lst = []
248 248 for k in self._get_keys():
249 249 lst.append((k, getattr(self, k),))
250 250 return lst
251 251
252 252 def populate_obj(self, populate_dict):
253 253 """populate model with data from given populate_dict"""
254 254
255 255 for k in self._get_keys():
256 256 if k in populate_dict:
257 257 setattr(self, k, populate_dict[k])
258 258
259 259 @classmethod
260 260 def query(cls):
261 261 return Session().query(cls)
262 262
263 263 @classmethod
264 264 def get(cls, id_):
265 265 if id_:
266 266 return cls.query().get(id_)
267 267
268 268 @classmethod
269 269 def get_or_404(cls, id_):
270 270 from pyramid.httpexceptions import HTTPNotFound
271 271
272 272 try:
273 273 id_ = int(id_)
274 274 except (TypeError, ValueError):
275 275 raise HTTPNotFound()
276 276
277 277 res = cls.query().get(id_)
278 278 if not res:
279 279 raise HTTPNotFound()
280 280 return res
281 281
282 282 @classmethod
283 283 def getAll(cls):
284 284 # deprecated and left for backward compatibility
285 285 return cls.get_all()
286 286
287 287 @classmethod
288 288 def get_all(cls):
289 289 return cls.query().all()
290 290
291 291 @classmethod
292 292 def delete(cls, id_):
293 293 obj = cls.query().get(id_)
294 294 Session().delete(obj)
295 295
296 296 @classmethod
297 297 def identity_cache(cls, session, attr_name, value):
298 298 exist_in_session = []
299 299 for (item_cls, pkey), instance in session.identity_map.items():
300 300 if cls == item_cls and getattr(instance, attr_name) == value:
301 301 exist_in_session.append(instance)
302 302 if exist_in_session:
303 303 if len(exist_in_session) == 1:
304 304 return exist_in_session[0]
305 305 log.exception(
306 306 'multiple objects with attr %s and '
307 307 'value %s found with same name: %r',
308 308 attr_name, value, exist_in_session)
309 309
310 310 def __repr__(self):
311 311 if hasattr(self, '__unicode__'):
312 312 # python repr needs to return str
313 313 try:
314 314 return safe_str(self.__unicode__())
315 315 except UnicodeDecodeError:
316 316 pass
317 317 return '<DB:%s>' % (self.__class__.__name__)
318 318
319 319
320 320 class RhodeCodeSetting(Base, BaseModel):
321 321 __tablename__ = 'rhodecode_settings'
322 322 __table_args__ = (
323 323 UniqueConstraint('app_settings_name'),
324 324 base_table_args
325 325 )
326 326
327 327 SETTINGS_TYPES = {
328 328 'str': safe_str,
329 329 'int': safe_int,
330 330 'unicode': safe_unicode,
331 331 'bool': str2bool,
332 332 'list': functools.partial(aslist, sep=',')
333 333 }
334 334 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
335 335 GLOBAL_CONF_KEY = 'app_settings'
336 336
337 337 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
338 338 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
339 339 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
340 340 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
341 341
342 342 def __init__(self, key='', val='', type='unicode'):
343 343 self.app_settings_name = key
344 344 self.app_settings_type = type
345 345 self.app_settings_value = val
346 346
347 347 @validates('_app_settings_value')
348 348 def validate_settings_value(self, key, val):
349 349 assert type(val) == unicode
350 350 return val
351 351
352 352 @hybrid_property
353 353 def app_settings_value(self):
354 354 v = self._app_settings_value
355 355 _type = self.app_settings_type
356 356 if _type:
357 357 _type = self.app_settings_type.split('.')[0]
358 358 # decode the encrypted value
359 359 if 'encrypted' in self.app_settings_type:
360 360 cipher = EncryptedTextValue()
361 361 v = safe_unicode(cipher.process_result_value(v, None))
362 362
363 363 converter = self.SETTINGS_TYPES.get(_type) or \
364 364 self.SETTINGS_TYPES['unicode']
365 365 return converter(v)
366 366
367 367 @app_settings_value.setter
368 368 def app_settings_value(self, val):
369 369 """
370 370 Setter that will always make sure we use unicode in app_settings_value
371 371
372 372 :param val:
373 373 """
374 374 val = safe_unicode(val)
375 375 # encode the encrypted value
376 376 if 'encrypted' in self.app_settings_type:
377 377 cipher = EncryptedTextValue()
378 378 val = safe_unicode(cipher.process_bind_param(val, None))
379 379 self._app_settings_value = val
380 380
381 381 @hybrid_property
382 382 def app_settings_type(self):
383 383 return self._app_settings_type
384 384
385 385 @app_settings_type.setter
386 386 def app_settings_type(self, val):
387 387 if val.split('.')[0] not in self.SETTINGS_TYPES:
388 388 raise Exception('type must be one of %s got %s'
389 389 % (self.SETTINGS_TYPES.keys(), val))
390 390 self._app_settings_type = val
391 391
392 392 @classmethod
393 393 def get_by_prefix(cls, prefix):
394 394 return RhodeCodeSetting.query()\
395 395 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
396 396 .all()
397 397
398 398 def __unicode__(self):
399 399 return u"<%s('%s:%s[%s]')>" % (
400 400 self.__class__.__name__,
401 401 self.app_settings_name, self.app_settings_value,
402 402 self.app_settings_type
403 403 )
404 404
405 405
406 406 class RhodeCodeUi(Base, BaseModel):
407 407 __tablename__ = 'rhodecode_ui'
408 408 __table_args__ = (
409 409 UniqueConstraint('ui_key'),
410 410 base_table_args
411 411 )
412 412
413 413 HOOK_REPO_SIZE = 'changegroup.repo_size'
414 414 # HG
415 415 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
416 416 HOOK_PULL = 'outgoing.pull_logger'
417 417 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
418 418 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
419 419 HOOK_PUSH = 'changegroup.push_logger'
420 420 HOOK_PUSH_KEY = 'pushkey.key_push'
421 421
422 422 HOOKS_BUILTIN = [
423 423 HOOK_PRE_PULL,
424 424 HOOK_PULL,
425 425 HOOK_PRE_PUSH,
426 426 HOOK_PRETX_PUSH,
427 427 HOOK_PUSH,
428 428 HOOK_PUSH_KEY,
429 429 ]
430 430
431 431 # TODO: johbo: Unify way how hooks are configured for git and hg,
432 432 # git part is currently hardcoded.
433 433
434 434 # SVN PATTERNS
435 435 SVN_BRANCH_ID = 'vcs_svn_branch'
436 436 SVN_TAG_ID = 'vcs_svn_tag'
437 437
438 438 ui_id = Column(
439 439 "ui_id", Integer(), nullable=False, unique=True, default=None,
440 440 primary_key=True)
441 441 ui_section = Column(
442 442 "ui_section", String(255), nullable=True, unique=None, default=None)
443 443 ui_key = Column(
444 444 "ui_key", String(255), nullable=True, unique=None, default=None)
445 445 ui_value = Column(
446 446 "ui_value", String(255), nullable=True, unique=None, default=None)
447 447 ui_active = Column(
448 448 "ui_active", Boolean(), nullable=True, unique=None, default=True)
449 449
450 450 def __repr__(self):
451 451 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
452 452 self.ui_key, self.ui_value)
453 453
454 454
455 455 class RepoRhodeCodeSetting(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_settings'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'app_settings_name', 'repository_id',
460 460 name='uq_repo_rhodecode_setting_name_repo_id'),
461 461 base_table_args
462 462 )
463 463
464 464 repository_id = Column(
465 465 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 466 nullable=False)
467 467 app_settings_id = Column(
468 468 "app_settings_id", Integer(), nullable=False, unique=True,
469 469 default=None, primary_key=True)
470 470 app_settings_name = Column(
471 471 "app_settings_name", String(255), nullable=True, unique=None,
472 472 default=None)
473 473 _app_settings_value = Column(
474 474 "app_settings_value", String(4096), nullable=True, unique=None,
475 475 default=None)
476 476 _app_settings_type = Column(
477 477 "app_settings_type", String(255), nullable=True, unique=None,
478 478 default=None)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __init__(self, repository_id, key='', val='', type='unicode'):
483 483 self.repository_id = repository_id
484 484 self.app_settings_name = key
485 485 self.app_settings_type = type
486 486 self.app_settings_value = val
487 487
488 488 @validates('_app_settings_value')
489 489 def validate_settings_value(self, key, val):
490 490 assert type(val) == unicode
491 491 return val
492 492
493 493 @hybrid_property
494 494 def app_settings_value(self):
495 495 v = self._app_settings_value
496 496 type_ = self.app_settings_type
497 497 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
498 498 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
499 499 return converter(v)
500 500
501 501 @app_settings_value.setter
502 502 def app_settings_value(self, val):
503 503 """
504 504 Setter that will always make sure we use unicode in app_settings_value
505 505
506 506 :param val:
507 507 """
508 508 self._app_settings_value = safe_unicode(val)
509 509
510 510 @hybrid_property
511 511 def app_settings_type(self):
512 512 return self._app_settings_type
513 513
514 514 @app_settings_type.setter
515 515 def app_settings_type(self, val):
516 516 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
517 517 if val not in SETTINGS_TYPES:
518 518 raise Exception('type must be one of %s got %s'
519 519 % (SETTINGS_TYPES.keys(), val))
520 520 self._app_settings_type = val
521 521
522 522 def __unicode__(self):
523 523 return u"<%s('%s:%s:%s[%s]')>" % (
524 524 self.__class__.__name__, self.repository.repo_name,
525 525 self.app_settings_name, self.app_settings_value,
526 526 self.app_settings_type
527 527 )
528 528
529 529
530 530 class RepoRhodeCodeUi(Base, BaseModel):
531 531 __tablename__ = 'repo_rhodecode_ui'
532 532 __table_args__ = (
533 533 UniqueConstraint(
534 534 'repository_id', 'ui_section', 'ui_key',
535 535 name='uq_repo_rhodecode_ui_repository_id_section_key'),
536 536 base_table_args
537 537 )
538 538
539 539 repository_id = Column(
540 540 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
541 541 nullable=False)
542 542 ui_id = Column(
543 543 "ui_id", Integer(), nullable=False, unique=True, default=None,
544 544 primary_key=True)
545 545 ui_section = Column(
546 546 "ui_section", String(255), nullable=True, unique=None, default=None)
547 547 ui_key = Column(
548 548 "ui_key", String(255), nullable=True, unique=None, default=None)
549 549 ui_value = Column(
550 550 "ui_value", String(255), nullable=True, unique=None, default=None)
551 551 ui_active = Column(
552 552 "ui_active", Boolean(), nullable=True, unique=None, default=True)
553 553
554 554 repository = relationship('Repository')
555 555
556 556 def __repr__(self):
557 557 return '<%s[%s:%s]%s=>%s]>' % (
558 558 self.__class__.__name__, self.repository.repo_name,
559 559 self.ui_section, self.ui_key, self.ui_value)
560 560
561 561
562 562 class User(Base, BaseModel):
563 563 __tablename__ = 'users'
564 564 __table_args__ = (
565 565 UniqueConstraint('username'), UniqueConstraint('email'),
566 566 Index('u_username_idx', 'username'),
567 567 Index('u_email_idx', 'email'),
568 568 base_table_args
569 569 )
570 570
571 571 DEFAULT_USER = 'default'
572 572 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
573 573 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
574 574
575 575 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
576 576 username = Column("username", String(255), nullable=True, unique=None, default=None)
577 577 password = Column("password", String(255), nullable=True, unique=None, default=None)
578 578 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
579 579 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
580 580 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
581 581 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
582 582 _email = Column("email", String(255), nullable=True, unique=None, default=None)
583 583 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
584 584 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
585 585 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
586 586
587 587 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
588 588 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
589 589 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
590 590 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
591 591 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
592 592 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
593 593
594 594 user_log = relationship('UserLog')
595 595 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
596 596
597 597 repositories = relationship('Repository')
598 598 repository_groups = relationship('RepoGroup')
599 599 user_groups = relationship('UserGroup')
600 600
601 601 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
602 602 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
603 603
604 604 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 605 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 606 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 607
608 608 group_member = relationship('UserGroupMember', cascade='all')
609 609
610 610 notifications = relationship('UserNotification', cascade='all')
611 611 # notifications assigned to this user
612 612 user_created_notifications = relationship('Notification', cascade='all')
613 613 # comments created by this user
614 614 user_comments = relationship('ChangesetComment', cascade='all')
615 615 # user profile extra info
616 616 user_emails = relationship('UserEmailMap', cascade='all')
617 617 user_ip_map = relationship('UserIpMap', cascade='all')
618 618 user_auth_tokens = relationship('UserApiKeys', cascade='all')
619 619 user_ssh_keys = relationship('UserSshKeys', cascade='all')
620 620
621 621 # gists
622 622 user_gists = relationship('Gist', cascade='all')
623 623 # user pull requests
624 624 user_pull_requests = relationship('PullRequest', cascade='all')
625 625
626 626 # external identities
627 627 external_identities = relationship(
628 628 'ExternalIdentity',
629 629 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
630 630 cascade='all')
631 631 # review rules
632 632 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
633 633
634 634 # artifacts owned
635 635 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
636 636
637 637 # no cascade, set NULL
638 638 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
639 639
640 640 def __unicode__(self):
641 641 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
642 642 self.user_id, self.username)
643 643
644 644 @hybrid_property
645 645 def email(self):
646 646 return self._email
647 647
648 648 @email.setter
649 649 def email(self, val):
650 650 self._email = val.lower() if val else None
651 651
652 652 @hybrid_property
653 653 def first_name(self):
654 654 from rhodecode.lib import helpers as h
655 655 if self.name:
656 656 return h.escape(self.name)
657 657 return self.name
658 658
659 659 @hybrid_property
660 660 def last_name(self):
661 661 from rhodecode.lib import helpers as h
662 662 if self.lastname:
663 663 return h.escape(self.lastname)
664 664 return self.lastname
665 665
666 666 @hybrid_property
667 667 def api_key(self):
668 668 """
669 669 Fetch if exist an auth-token with role ALL connected to this user
670 670 """
671 671 user_auth_token = UserApiKeys.query()\
672 672 .filter(UserApiKeys.user_id == self.user_id)\
673 673 .filter(or_(UserApiKeys.expires == -1,
674 674 UserApiKeys.expires >= time.time()))\
675 675 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
676 676 if user_auth_token:
677 677 user_auth_token = user_auth_token.api_key
678 678
679 679 return user_auth_token
680 680
681 681 @api_key.setter
682 682 def api_key(self, val):
683 683 # don't allow to set API key this is deprecated for now
684 684 self._api_key = None
685 685
686 686 @property
687 687 def reviewer_pull_requests(self):
688 688 return PullRequestReviewers.query() \
689 689 .options(joinedload(PullRequestReviewers.pull_request)) \
690 690 .filter(PullRequestReviewers.user_id == self.user_id) \
691 691 .all()
692 692
693 693 @property
694 694 def firstname(self):
695 695 # alias for future
696 696 return self.name
697 697
698 698 @property
699 699 def emails(self):
700 700 other = UserEmailMap.query()\
701 701 .filter(UserEmailMap.user == self) \
702 702 .order_by(UserEmailMap.email_id.asc()) \
703 703 .all()
704 704 return [self.email] + [x.email for x in other]
705 705
706 706 def emails_cached(self):
707 707 emails = UserEmailMap.query()\
708 708 .filter(UserEmailMap.user == self) \
709 709 .order_by(UserEmailMap.email_id.asc())
710 710
711 711 emails = emails.options(
712 712 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
713 713 )
714 714
715 715 return [self.email] + [x.email for x in emails]
716 716
717 717 @property
718 718 def auth_tokens(self):
719 719 auth_tokens = self.get_auth_tokens()
720 720 return [x.api_key for x in auth_tokens]
721 721
722 722 def get_auth_tokens(self):
723 723 return UserApiKeys.query()\
724 724 .filter(UserApiKeys.user == self)\
725 725 .order_by(UserApiKeys.user_api_key_id.asc())\
726 726 .all()
727 727
728 728 @LazyProperty
729 729 def feed_token(self):
730 730 return self.get_feed_token()
731 731
732 732 def get_feed_token(self, cache=True):
733 733 feed_tokens = UserApiKeys.query()\
734 734 .filter(UserApiKeys.user == self)\
735 735 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
736 736 if cache:
737 737 feed_tokens = feed_tokens.options(
738 738 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
739 739
740 740 feed_tokens = feed_tokens.all()
741 741 if feed_tokens:
742 742 return feed_tokens[0].api_key
743 743 return 'NO_FEED_TOKEN_AVAILABLE'
744 744
745 745 @LazyProperty
746 746 def artifact_token(self):
747 747 return self.get_artifact_token()
748 748
749 749 def get_artifact_token(self, cache=True):
750 750 artifacts_tokens = UserApiKeys.query()\
751 751 .filter(UserApiKeys.user == self)\
752 752 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
753 753 if cache:
754 754 artifacts_tokens = artifacts_tokens.options(
755 755 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
756 756
757 757 artifacts_tokens = artifacts_tokens.all()
758 758 if artifacts_tokens:
759 759 return artifacts_tokens[0].api_key
760 760 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
761 761
762 762 @classmethod
763 763 def get(cls, user_id, cache=False):
764 764 if not user_id:
765 765 return
766 766
767 767 user = cls.query()
768 768 if cache:
769 769 user = user.options(
770 770 FromCache("sql_cache_short", "get_users_%s" % user_id))
771 771 return user.get(user_id)
772 772
773 773 @classmethod
774 774 def extra_valid_auth_tokens(cls, user, role=None):
775 775 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
776 776 .filter(or_(UserApiKeys.expires == -1,
777 777 UserApiKeys.expires >= time.time()))
778 778 if role:
779 779 tokens = tokens.filter(or_(UserApiKeys.role == role,
780 780 UserApiKeys.role == UserApiKeys.ROLE_ALL))
781 781 return tokens.all()
782 782
783 783 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
784 784 from rhodecode.lib import auth
785 785
786 786 log.debug('Trying to authenticate user: %s via auth-token, '
787 787 'and roles: %s', self, roles)
788 788
789 789 if not auth_token:
790 790 return False
791 791
792 792 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
793 793 tokens_q = UserApiKeys.query()\
794 794 .filter(UserApiKeys.user_id == self.user_id)\
795 795 .filter(or_(UserApiKeys.expires == -1,
796 796 UserApiKeys.expires >= time.time()))
797 797
798 798 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
799 799
800 800 crypto_backend = auth.crypto_backend()
801 801 enc_token_map = {}
802 802 plain_token_map = {}
803 803 for token in tokens_q:
804 804 if token.api_key.startswith(crypto_backend.ENC_PREF):
805 805 enc_token_map[token.api_key] = token
806 806 else:
807 807 plain_token_map[token.api_key] = token
808 808 log.debug(
809 809 'Found %s plain and %s encrypted tokens to check for authentication for this user',
810 810 len(plain_token_map), len(enc_token_map))
811 811
812 812 # plain token match comes first
813 813 match = plain_token_map.get(auth_token)
814 814
815 815 # check encrypted tokens now
816 816 if not match:
817 817 for token_hash, token in enc_token_map.items():
818 818 # NOTE(marcink): this is expensive to calculate, but most secure
819 819 if crypto_backend.hash_check(auth_token, token_hash):
820 820 match = token
821 821 break
822 822
823 823 if match:
824 824 log.debug('Found matching token %s', match)
825 825 if match.repo_id:
826 826 log.debug('Found scope, checking for scope match of token %s', match)
827 827 if match.repo_id == scope_repo_id:
828 828 return True
829 829 else:
830 830 log.debug(
831 831 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
832 832 'and calling scope is:%s, skipping further checks',
833 833 match.repo, scope_repo_id)
834 834 return False
835 835 else:
836 836 return True
837 837
838 838 return False
839 839
840 840 @property
841 841 def ip_addresses(self):
842 842 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
843 843 return [x.ip_addr for x in ret]
844 844
845 845 @property
846 846 def username_and_name(self):
847 847 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
848 848
849 849 @property
850 850 def username_or_name_or_email(self):
851 851 full_name = self.full_name if self.full_name is not ' ' else None
852 852 return self.username or full_name or self.email
853 853
854 854 @property
855 855 def full_name(self):
856 856 return '%s %s' % (self.first_name, self.last_name)
857 857
858 858 @property
859 859 def full_name_or_username(self):
860 860 return ('%s %s' % (self.first_name, self.last_name)
861 861 if (self.first_name and self.last_name) else self.username)
862 862
863 863 @property
864 864 def full_contact(self):
865 865 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
866 866
867 867 @property
868 868 def short_contact(self):
869 869 return '%s %s' % (self.first_name, self.last_name)
870 870
871 871 @property
872 872 def is_admin(self):
873 873 return self.admin
874 874
875 875 @property
876 876 def language(self):
877 877 return self.user_data.get('language')
878 878
879 879 def AuthUser(self, **kwargs):
880 880 """
881 881 Returns instance of AuthUser for this user
882 882 """
883 883 from rhodecode.lib.auth import AuthUser
884 884 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
885 885
886 886 @hybrid_property
887 887 def user_data(self):
888 888 if not self._user_data:
889 889 return {}
890 890
891 891 try:
892 892 return json.loads(self._user_data)
893 893 except TypeError:
894 894 return {}
895 895
896 896 @user_data.setter
897 897 def user_data(self, val):
898 898 if not isinstance(val, dict):
899 899 raise Exception('user_data must be dict, got %s' % type(val))
900 900 try:
901 901 self._user_data = json.dumps(val)
902 902 except Exception:
903 903 log.error(traceback.format_exc())
904 904
905 905 @classmethod
906 906 def get_by_username(cls, username, case_insensitive=False,
907 907 cache=False, identity_cache=False):
908 908 session = Session()
909 909
910 910 if case_insensitive:
911 911 q = cls.query().filter(
912 912 func.lower(cls.username) == func.lower(username))
913 913 else:
914 914 q = cls.query().filter(cls.username == username)
915 915
916 916 if cache:
917 917 if identity_cache:
918 918 val = cls.identity_cache(session, 'username', username)
919 919 if val:
920 920 return val
921 921 else:
922 922 cache_key = "get_user_by_name_%s" % _hash_key(username)
923 923 q = q.options(
924 924 FromCache("sql_cache_short", cache_key))
925 925
926 926 return q.scalar()
927 927
928 928 @classmethod
929 929 def get_by_auth_token(cls, auth_token, cache=False):
930 930 q = UserApiKeys.query()\
931 931 .filter(UserApiKeys.api_key == auth_token)\
932 932 .filter(or_(UserApiKeys.expires == -1,
933 933 UserApiKeys.expires >= time.time()))
934 934 if cache:
935 935 q = q.options(
936 936 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
937 937
938 938 match = q.first()
939 939 if match:
940 940 return match.user
941 941
942 942 @classmethod
943 943 def get_by_email(cls, email, case_insensitive=False, cache=False):
944 944
945 945 if case_insensitive:
946 946 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
947 947
948 948 else:
949 949 q = cls.query().filter(cls.email == email)
950 950
951 951 email_key = _hash_key(email)
952 952 if cache:
953 953 q = q.options(
954 954 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
955 955
956 956 ret = q.scalar()
957 957 if ret is None:
958 958 q = UserEmailMap.query()
959 959 # try fetching in alternate email map
960 960 if case_insensitive:
961 961 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
962 962 else:
963 963 q = q.filter(UserEmailMap.email == email)
964 964 q = q.options(joinedload(UserEmailMap.user))
965 965 if cache:
966 966 q = q.options(
967 967 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
968 968 ret = getattr(q.scalar(), 'user', None)
969 969
970 970 return ret
971 971
972 972 @classmethod
973 973 def get_from_cs_author(cls, author):
974 974 """
975 975 Tries to get User objects out of commit author string
976 976
977 977 :param author:
978 978 """
979 979 from rhodecode.lib.helpers import email, author_name
980 980 # Valid email in the attribute passed, see if they're in the system
981 981 _email = email(author)
982 982 if _email:
983 983 user = cls.get_by_email(_email, case_insensitive=True)
984 984 if user:
985 985 return user
986 986 # Maybe we can match by username?
987 987 _author = author_name(author)
988 988 user = cls.get_by_username(_author, case_insensitive=True)
989 989 if user:
990 990 return user
991 991
992 992 def update_userdata(self, **kwargs):
993 993 usr = self
994 994 old = usr.user_data
995 995 old.update(**kwargs)
996 996 usr.user_data = old
997 997 Session().add(usr)
998 998 log.debug('updated userdata with %s', kwargs)
999 999
1000 1000 def update_lastlogin(self):
1001 1001 """Update user lastlogin"""
1002 1002 self.last_login = datetime.datetime.now()
1003 1003 Session().add(self)
1004 1004 log.debug('updated user %s lastlogin', self.username)
1005 1005
1006 1006 def update_password(self, new_password):
1007 1007 from rhodecode.lib.auth import get_crypt_password
1008 1008
1009 1009 self.password = get_crypt_password(new_password)
1010 1010 Session().add(self)
1011 1011
1012 1012 @classmethod
1013 1013 def get_first_super_admin(cls):
1014 1014 user = User.query()\
1015 1015 .filter(User.admin == true()) \
1016 1016 .order_by(User.user_id.asc()) \
1017 1017 .first()
1018 1018
1019 1019 if user is None:
1020 1020 raise Exception('FATAL: Missing administrative account!')
1021 1021 return user
1022 1022
1023 1023 @classmethod
1024 1024 def get_all_super_admins(cls, only_active=False):
1025 1025 """
1026 1026 Returns all admin accounts sorted by username
1027 1027 """
1028 1028 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1029 1029 if only_active:
1030 1030 qry = qry.filter(User.active == true())
1031 1031 return qry.all()
1032 1032
1033 1033 @classmethod
1034 1034 def get_all_user_ids(cls, only_active=True):
1035 1035 """
1036 1036 Returns all users IDs
1037 1037 """
1038 1038 qry = Session().query(User.user_id)
1039 1039
1040 1040 if only_active:
1041 1041 qry = qry.filter(User.active == true())
1042 1042 return [x.user_id for x in qry]
1043 1043
1044 1044 @classmethod
1045 1045 def get_default_user(cls, cache=False, refresh=False):
1046 1046 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1047 1047 if user is None:
1048 1048 raise Exception('FATAL: Missing default account!')
1049 1049 if refresh:
1050 1050 # The default user might be based on outdated state which
1051 1051 # has been loaded from the cache.
1052 1052 # A call to refresh() ensures that the
1053 1053 # latest state from the database is used.
1054 1054 Session().refresh(user)
1055 1055 return user
1056 1056
1057 1057 @classmethod
1058 1058 def get_default_user_id(cls):
1059 1059 import rhodecode
1060 1060 return rhodecode.CONFIG['default_user_id']
1061 1061
1062 1062 def _get_default_perms(self, user, suffix=''):
1063 1063 from rhodecode.model.permission import PermissionModel
1064 1064 return PermissionModel().get_default_perms(user.user_perms, suffix)
1065 1065
1066 1066 def get_default_perms(self, suffix=''):
1067 1067 return self._get_default_perms(self, suffix)
1068 1068
1069 1069 def get_api_data(self, include_secrets=False, details='full'):
1070 1070 """
1071 1071 Common function for generating user related data for API
1072 1072
1073 1073 :param include_secrets: By default secrets in the API data will be replaced
1074 1074 by a placeholder value to prevent exposing this data by accident. In case
1075 1075 this data shall be exposed, set this flag to ``True``.
1076 1076
1077 1077 :param details: details can be 'basic|full' basic gives only a subset of
1078 1078 the available user information that includes user_id, name and emails.
1079 1079 """
1080 1080 user = self
1081 1081 user_data = self.user_data
1082 1082 data = {
1083 1083 'user_id': user.user_id,
1084 1084 'username': user.username,
1085 1085 'firstname': user.name,
1086 1086 'lastname': user.lastname,
1087 1087 'description': user.description,
1088 1088 'email': user.email,
1089 1089 'emails': user.emails,
1090 1090 }
1091 1091 if details == 'basic':
1092 1092 return data
1093 1093
1094 1094 auth_token_length = 40
1095 1095 auth_token_replacement = '*' * auth_token_length
1096 1096
1097 1097 extras = {
1098 1098 'auth_tokens': [auth_token_replacement],
1099 1099 'active': user.active,
1100 1100 'admin': user.admin,
1101 1101 'extern_type': user.extern_type,
1102 1102 'extern_name': user.extern_name,
1103 1103 'last_login': user.last_login,
1104 1104 'last_activity': user.last_activity,
1105 1105 'ip_addresses': user.ip_addresses,
1106 1106 'language': user_data.get('language')
1107 1107 }
1108 1108 data.update(extras)
1109 1109
1110 1110 if include_secrets:
1111 1111 data['auth_tokens'] = user.auth_tokens
1112 1112 return data
1113 1113
1114 1114 def __json__(self):
1115 1115 data = {
1116 1116 'full_name': self.full_name,
1117 1117 'full_name_or_username': self.full_name_or_username,
1118 1118 'short_contact': self.short_contact,
1119 1119 'full_contact': self.full_contact,
1120 1120 }
1121 1121 data.update(self.get_api_data())
1122 1122 return data
1123 1123
1124 1124
1125 1125 class UserApiKeys(Base, BaseModel):
1126 1126 __tablename__ = 'user_api_keys'
1127 1127 __table_args__ = (
1128 1128 Index('uak_api_key_idx', 'api_key'),
1129 1129 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1130 1130 base_table_args
1131 1131 )
1132 1132 __mapper_args__ = {}
1133 1133
1134 1134 # ApiKey role
1135 1135 ROLE_ALL = 'token_role_all'
1136 1136 ROLE_VCS = 'token_role_vcs'
1137 1137 ROLE_API = 'token_role_api'
1138 1138 ROLE_HTTP = 'token_role_http'
1139 1139 ROLE_FEED = 'token_role_feed'
1140 1140 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1141 1141 # The last one is ignored in the list as we only
1142 1142 # use it for one action, and cannot be created by users
1143 1143 ROLE_PASSWORD_RESET = 'token_password_reset'
1144 1144
1145 1145 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1146 1146
1147 1147 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1148 1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1149 1149 api_key = Column("api_key", String(255), nullable=False, unique=True)
1150 1150 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1151 1151 expires = Column('expires', Float(53), nullable=False)
1152 1152 role = Column('role', String(255), nullable=True)
1153 1153 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1154 1154
1155 1155 # scope columns
1156 1156 repo_id = Column(
1157 1157 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1158 1158 nullable=True, unique=None, default=None)
1159 1159 repo = relationship('Repository', lazy='joined')
1160 1160
1161 1161 repo_group_id = Column(
1162 1162 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1163 1163 nullable=True, unique=None, default=None)
1164 1164 repo_group = relationship('RepoGroup', lazy='joined')
1165 1165
1166 1166 user = relationship('User', lazy='joined')
1167 1167
1168 1168 def __unicode__(self):
1169 1169 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1170 1170
1171 1171 def __json__(self):
1172 1172 data = {
1173 1173 'auth_token': self.api_key,
1174 1174 'role': self.role,
1175 1175 'scope': self.scope_humanized,
1176 1176 'expired': self.expired
1177 1177 }
1178 1178 return data
1179 1179
1180 1180 def get_api_data(self, include_secrets=False):
1181 1181 data = self.__json__()
1182 1182 if include_secrets:
1183 1183 return data
1184 1184 else:
1185 1185 data['auth_token'] = self.token_obfuscated
1186 1186 return data
1187 1187
1188 1188 @hybrid_property
1189 1189 def description_safe(self):
1190 1190 from rhodecode.lib import helpers as h
1191 1191 return h.escape(self.description)
1192 1192
1193 1193 @property
1194 1194 def expired(self):
1195 1195 if self.expires == -1:
1196 1196 return False
1197 1197 return time.time() > self.expires
1198 1198
1199 1199 @classmethod
1200 1200 def _get_role_name(cls, role):
1201 1201 return {
1202 1202 cls.ROLE_ALL: _('all'),
1203 1203 cls.ROLE_HTTP: _('http/web interface'),
1204 1204 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1205 1205 cls.ROLE_API: _('api calls'),
1206 1206 cls.ROLE_FEED: _('feed access'),
1207 1207 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1208 1208 }.get(role, role)
1209 1209
1210 1210 @classmethod
1211 1211 def _get_role_description(cls, role):
1212 1212 return {
1213 1213 cls.ROLE_ALL: _('Token for all actions.'),
1214 1214 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1215 1215 'login using `api_access_controllers_whitelist` functionality.'),
1216 1216 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1217 1217 'Requires auth_token authentication plugin to be active. <br/>'
1218 1218 'Such Token should be used then instead of a password to '
1219 1219 'interact with a repository, and additionally can be '
1220 1220 'limited to single repository using repo scope.'),
1221 1221 cls.ROLE_API: _('Token limited to api calls.'),
1222 1222 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1223 1223 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1224 1224 }.get(role, role)
1225 1225
1226 1226 @property
1227 1227 def role_humanized(self):
1228 1228 return self._get_role_name(self.role)
1229 1229
1230 1230 def _get_scope(self):
1231 1231 if self.repo:
1232 1232 return 'Repository: {}'.format(self.repo.repo_name)
1233 1233 if self.repo_group:
1234 1234 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1235 1235 return 'Global'
1236 1236
1237 1237 @property
1238 1238 def scope_humanized(self):
1239 1239 return self._get_scope()
1240 1240
1241 1241 @property
1242 1242 def token_obfuscated(self):
1243 1243 if self.api_key:
1244 1244 return self.api_key[:4] + "****"
1245 1245
1246 1246
1247 1247 class UserEmailMap(Base, BaseModel):
1248 1248 __tablename__ = 'user_email_map'
1249 1249 __table_args__ = (
1250 1250 Index('uem_email_idx', 'email'),
1251 1251 UniqueConstraint('email'),
1252 1252 base_table_args
1253 1253 )
1254 1254 __mapper_args__ = {}
1255 1255
1256 1256 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1257 1257 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1258 1258 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1259 1259 user = relationship('User', lazy='joined')
1260 1260
1261 1261 @validates('_email')
1262 1262 def validate_email(self, key, email):
1263 1263 # check if this email is not main one
1264 1264 main_email = Session().query(User).filter(User.email == email).scalar()
1265 1265 if main_email is not None:
1266 1266 raise AttributeError('email %s is present is user table' % email)
1267 1267 return email
1268 1268
1269 1269 @hybrid_property
1270 1270 def email(self):
1271 1271 return self._email
1272 1272
1273 1273 @email.setter
1274 1274 def email(self, val):
1275 1275 self._email = val.lower() if val else None
1276 1276
1277 1277
1278 1278 class UserIpMap(Base, BaseModel):
1279 1279 __tablename__ = 'user_ip_map'
1280 1280 __table_args__ = (
1281 1281 UniqueConstraint('user_id', 'ip_addr'),
1282 1282 base_table_args
1283 1283 )
1284 1284 __mapper_args__ = {}
1285 1285
1286 1286 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1287 1287 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1288 1288 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1289 1289 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1290 1290 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1291 1291 user = relationship('User', lazy='joined')
1292 1292
1293 1293 @hybrid_property
1294 1294 def description_safe(self):
1295 1295 from rhodecode.lib import helpers as h
1296 1296 return h.escape(self.description)
1297 1297
1298 1298 @classmethod
1299 1299 def _get_ip_range(cls, ip_addr):
1300 1300 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1301 1301 return [str(net.network_address), str(net.broadcast_address)]
1302 1302
1303 1303 def __json__(self):
1304 1304 return {
1305 1305 'ip_addr': self.ip_addr,
1306 1306 'ip_range': self._get_ip_range(self.ip_addr),
1307 1307 }
1308 1308
1309 1309 def __unicode__(self):
1310 1310 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1311 1311 self.user_id, self.ip_addr)
1312 1312
1313 1313
1314 1314 class UserSshKeys(Base, BaseModel):
1315 1315 __tablename__ = 'user_ssh_keys'
1316 1316 __table_args__ = (
1317 1317 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1318 1318
1319 1319 UniqueConstraint('ssh_key_fingerprint'),
1320 1320
1321 1321 base_table_args
1322 1322 )
1323 1323 __mapper_args__ = {}
1324 1324
1325 1325 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1326 1326 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1327 1327 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1328 1328
1329 1329 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1330 1330
1331 1331 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1332 1332 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1333 1333 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1334 1334
1335 1335 user = relationship('User', lazy='joined')
1336 1336
1337 1337 def __json__(self):
1338 1338 data = {
1339 1339 'ssh_fingerprint': self.ssh_key_fingerprint,
1340 1340 'description': self.description,
1341 1341 'created_on': self.created_on
1342 1342 }
1343 1343 return data
1344 1344
1345 1345 def get_api_data(self):
1346 1346 data = self.__json__()
1347 1347 return data
1348 1348
1349 1349
1350 1350 class UserLog(Base, BaseModel):
1351 1351 __tablename__ = 'user_logs'
1352 1352 __table_args__ = (
1353 1353 base_table_args,
1354 1354 )
1355 1355
1356 1356 VERSION_1 = 'v1'
1357 1357 VERSION_2 = 'v2'
1358 1358 VERSIONS = [VERSION_1, VERSION_2]
1359 1359
1360 1360 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1361 1361 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1362 1362 username = Column("username", String(255), nullable=True, unique=None, default=None)
1363 1363 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1364 1364 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1365 1365 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1366 1366 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1367 1367 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1368 1368
1369 1369 version = Column("version", String(255), nullable=True, default=VERSION_1)
1370 1370 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1371 1371 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1372 1372
1373 1373 def __unicode__(self):
1374 1374 return u"<%s('id:%s:%s')>" % (
1375 1375 self.__class__.__name__, self.repository_name, self.action)
1376 1376
1377 1377 def __json__(self):
1378 1378 return {
1379 1379 'user_id': self.user_id,
1380 1380 'username': self.username,
1381 1381 'repository_id': self.repository_id,
1382 1382 'repository_name': self.repository_name,
1383 1383 'user_ip': self.user_ip,
1384 1384 'action_date': self.action_date,
1385 1385 'action': self.action,
1386 1386 }
1387 1387
1388 1388 @hybrid_property
1389 1389 def entry_id(self):
1390 1390 return self.user_log_id
1391 1391
1392 1392 @property
1393 1393 def action_as_day(self):
1394 1394 return datetime.date(*self.action_date.timetuple()[:3])
1395 1395
1396 1396 user = relationship('User')
1397 1397 repository = relationship('Repository', cascade='')
1398 1398
1399 1399
1400 1400 class UserGroup(Base, BaseModel):
1401 1401 __tablename__ = 'users_groups'
1402 1402 __table_args__ = (
1403 1403 base_table_args,
1404 1404 )
1405 1405
1406 1406 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1407 1407 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1408 1408 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1409 1409 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1410 1410 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1411 1411 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1412 1412 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1413 1413 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1414 1414
1415 1415 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1416 1416 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1417 1417 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1418 1418 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1419 1419 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1420 1420 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1421 1421
1422 1422 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1423 1423 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1424 1424
1425 1425 @classmethod
1426 1426 def _load_group_data(cls, column):
1427 1427 if not column:
1428 1428 return {}
1429 1429
1430 1430 try:
1431 1431 return json.loads(column) or {}
1432 1432 except TypeError:
1433 1433 return {}
1434 1434
1435 1435 @hybrid_property
1436 1436 def description_safe(self):
1437 1437 from rhodecode.lib import helpers as h
1438 1438 return h.escape(self.user_group_description)
1439 1439
1440 1440 @hybrid_property
1441 1441 def group_data(self):
1442 1442 return self._load_group_data(self._group_data)
1443 1443
1444 1444 @group_data.expression
1445 1445 def group_data(self, **kwargs):
1446 1446 return self._group_data
1447 1447
1448 1448 @group_data.setter
1449 1449 def group_data(self, val):
1450 1450 try:
1451 1451 self._group_data = json.dumps(val)
1452 1452 except Exception:
1453 1453 log.error(traceback.format_exc())
1454 1454
1455 1455 @classmethod
1456 1456 def _load_sync(cls, group_data):
1457 1457 if group_data:
1458 1458 return group_data.get('extern_type')
1459 1459
1460 1460 @property
1461 1461 def sync(self):
1462 1462 return self._load_sync(self.group_data)
1463 1463
1464 1464 def __unicode__(self):
1465 1465 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1466 1466 self.users_group_id,
1467 1467 self.users_group_name)
1468 1468
1469 1469 @classmethod
1470 1470 def get_by_group_name(cls, group_name, cache=False,
1471 1471 case_insensitive=False):
1472 1472 if case_insensitive:
1473 1473 q = cls.query().filter(func.lower(cls.users_group_name) ==
1474 1474 func.lower(group_name))
1475 1475
1476 1476 else:
1477 1477 q = cls.query().filter(cls.users_group_name == group_name)
1478 1478 if cache:
1479 1479 q = q.options(
1480 1480 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1481 1481 return q.scalar()
1482 1482
1483 1483 @classmethod
1484 1484 def get(cls, user_group_id, cache=False):
1485 1485 if not user_group_id:
1486 1486 return
1487 1487
1488 1488 user_group = cls.query()
1489 1489 if cache:
1490 1490 user_group = user_group.options(
1491 1491 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1492 1492 return user_group.get(user_group_id)
1493 1493
1494 1494 def permissions(self, with_admins=True, with_owner=True,
1495 1495 expand_from_user_groups=False):
1496 1496 """
1497 1497 Permissions for user groups
1498 1498 """
1499 1499 _admin_perm = 'usergroup.admin'
1500 1500
1501 1501 owner_row = []
1502 1502 if with_owner:
1503 1503 usr = AttributeDict(self.user.get_dict())
1504 1504 usr.owner_row = True
1505 1505 usr.permission = _admin_perm
1506 1506 owner_row.append(usr)
1507 1507
1508 1508 super_admin_ids = []
1509 1509 super_admin_rows = []
1510 1510 if with_admins:
1511 1511 for usr in User.get_all_super_admins():
1512 1512 super_admin_ids.append(usr.user_id)
1513 1513 # if this admin is also owner, don't double the record
1514 1514 if usr.user_id == owner_row[0].user_id:
1515 1515 owner_row[0].admin_row = True
1516 1516 else:
1517 1517 usr = AttributeDict(usr.get_dict())
1518 1518 usr.admin_row = True
1519 1519 usr.permission = _admin_perm
1520 1520 super_admin_rows.append(usr)
1521 1521
1522 1522 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1523 1523 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1524 1524 joinedload(UserUserGroupToPerm.user),
1525 1525 joinedload(UserUserGroupToPerm.permission),)
1526 1526
1527 1527 # get owners and admins and permissions. We do a trick of re-writing
1528 1528 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1529 1529 # has a global reference and changing one object propagates to all
1530 1530 # others. This means if admin is also an owner admin_row that change
1531 1531 # would propagate to both objects
1532 1532 perm_rows = []
1533 1533 for _usr in q.all():
1534 1534 usr = AttributeDict(_usr.user.get_dict())
1535 1535 # if this user is also owner/admin, mark as duplicate record
1536 1536 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1537 1537 usr.duplicate_perm = True
1538 1538 usr.permission = _usr.permission.permission_name
1539 1539 perm_rows.append(usr)
1540 1540
1541 1541 # filter the perm rows by 'default' first and then sort them by
1542 1542 # admin,write,read,none permissions sorted again alphabetically in
1543 1543 # each group
1544 1544 perm_rows = sorted(perm_rows, key=display_user_sort)
1545 1545
1546 1546 user_groups_rows = []
1547 1547 if expand_from_user_groups:
1548 1548 for ug in self.permission_user_groups(with_members=True):
1549 1549 for user_data in ug.members:
1550 1550 user_groups_rows.append(user_data)
1551 1551
1552 1552 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1553 1553
1554 1554 def permission_user_groups(self, with_members=False):
1555 1555 q = UserGroupUserGroupToPerm.query()\
1556 1556 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1557 1557 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1558 1558 joinedload(UserGroupUserGroupToPerm.target_user_group),
1559 1559 joinedload(UserGroupUserGroupToPerm.permission),)
1560 1560
1561 1561 perm_rows = []
1562 1562 for _user_group in q.all():
1563 1563 entry = AttributeDict(_user_group.user_group.get_dict())
1564 1564 entry.permission = _user_group.permission.permission_name
1565 1565 if with_members:
1566 1566 entry.members = [x.user.get_dict()
1567 1567 for x in _user_group.user_group.members]
1568 1568 perm_rows.append(entry)
1569 1569
1570 1570 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1571 1571 return perm_rows
1572 1572
1573 1573 def _get_default_perms(self, user_group, suffix=''):
1574 1574 from rhodecode.model.permission import PermissionModel
1575 1575 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1576 1576
1577 1577 def get_default_perms(self, suffix=''):
1578 1578 return self._get_default_perms(self, suffix)
1579 1579
1580 1580 def get_api_data(self, with_group_members=True, include_secrets=False):
1581 1581 """
1582 1582 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1583 1583 basically forwarded.
1584 1584
1585 1585 """
1586 1586 user_group = self
1587 1587 data = {
1588 1588 'users_group_id': user_group.users_group_id,
1589 1589 'group_name': user_group.users_group_name,
1590 1590 'group_description': user_group.user_group_description,
1591 1591 'active': user_group.users_group_active,
1592 1592 'owner': user_group.user.username,
1593 1593 'sync': user_group.sync,
1594 1594 'owner_email': user_group.user.email,
1595 1595 }
1596 1596
1597 1597 if with_group_members:
1598 1598 users = []
1599 1599 for user in user_group.members:
1600 1600 user = user.user
1601 1601 users.append(user.get_api_data(include_secrets=include_secrets))
1602 1602 data['users'] = users
1603 1603
1604 1604 return data
1605 1605
1606 1606
1607 1607 class UserGroupMember(Base, BaseModel):
1608 1608 __tablename__ = 'users_groups_members'
1609 1609 __table_args__ = (
1610 1610 base_table_args,
1611 1611 )
1612 1612
1613 1613 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1614 1614 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1615 1615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1616 1616
1617 1617 user = relationship('User', lazy='joined')
1618 1618 users_group = relationship('UserGroup')
1619 1619
1620 1620 def __init__(self, gr_id='', u_id=''):
1621 1621 self.users_group_id = gr_id
1622 1622 self.user_id = u_id
1623 1623
1624 1624
1625 1625 class RepositoryField(Base, BaseModel):
1626 1626 __tablename__ = 'repositories_fields'
1627 1627 __table_args__ = (
1628 1628 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1629 1629 base_table_args,
1630 1630 )
1631 1631
1632 1632 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1633 1633
1634 1634 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 1635 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1636 1636 field_key = Column("field_key", String(250))
1637 1637 field_label = Column("field_label", String(1024), nullable=False)
1638 1638 field_value = Column("field_value", String(10000), nullable=False)
1639 1639 field_desc = Column("field_desc", String(1024), nullable=False)
1640 1640 field_type = Column("field_type", String(255), nullable=False, unique=None)
1641 1641 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1642 1642
1643 1643 repository = relationship('Repository')
1644 1644
1645 1645 @property
1646 1646 def field_key_prefixed(self):
1647 1647 return 'ex_%s' % self.field_key
1648 1648
1649 1649 @classmethod
1650 1650 def un_prefix_key(cls, key):
1651 1651 if key.startswith(cls.PREFIX):
1652 1652 return key[len(cls.PREFIX):]
1653 1653 return key
1654 1654
1655 1655 @classmethod
1656 1656 def get_by_key_name(cls, key, repo):
1657 1657 row = cls.query()\
1658 1658 .filter(cls.repository == repo)\
1659 1659 .filter(cls.field_key == key).scalar()
1660 1660 return row
1661 1661
1662 1662
1663 1663 class Repository(Base, BaseModel):
1664 1664 __tablename__ = 'repositories'
1665 1665 __table_args__ = (
1666 1666 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1667 1667 base_table_args,
1668 1668 )
1669 1669 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1670 1670 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1671 1671 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1672 1672
1673 1673 STATE_CREATED = 'repo_state_created'
1674 1674 STATE_PENDING = 'repo_state_pending'
1675 1675 STATE_ERROR = 'repo_state_error'
1676 1676
1677 1677 LOCK_AUTOMATIC = 'lock_auto'
1678 1678 LOCK_API = 'lock_api'
1679 1679 LOCK_WEB = 'lock_web'
1680 1680 LOCK_PULL = 'lock_pull'
1681 1681
1682 1682 NAME_SEP = URL_SEP
1683 1683
1684 1684 repo_id = Column(
1685 1685 "repo_id", Integer(), nullable=False, unique=True, default=None,
1686 1686 primary_key=True)
1687 1687 _repo_name = Column(
1688 1688 "repo_name", Text(), nullable=False, default=None)
1689 1689 repo_name_hash = Column(
1690 1690 "repo_name_hash", String(255), nullable=False, unique=True)
1691 1691 repo_state = Column("repo_state", String(255), nullable=True)
1692 1692
1693 1693 clone_uri = Column(
1694 1694 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1695 1695 default=None)
1696 1696 push_uri = Column(
1697 1697 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1698 1698 default=None)
1699 1699 repo_type = Column(
1700 1700 "repo_type", String(255), nullable=False, unique=False, default=None)
1701 1701 user_id = Column(
1702 1702 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1703 1703 unique=False, default=None)
1704 1704 private = Column(
1705 1705 "private", Boolean(), nullable=True, unique=None, default=None)
1706 1706 archived = Column(
1707 1707 "archived", Boolean(), nullable=True, unique=None, default=None)
1708 1708 enable_statistics = Column(
1709 1709 "statistics", Boolean(), nullable=True, unique=None, default=True)
1710 1710 enable_downloads = Column(
1711 1711 "downloads", Boolean(), nullable=True, unique=None, default=True)
1712 1712 description = Column(
1713 1713 "description", String(10000), nullable=True, unique=None, default=None)
1714 1714 created_on = Column(
1715 1715 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1716 1716 default=datetime.datetime.now)
1717 1717 updated_on = Column(
1718 1718 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1719 1719 default=datetime.datetime.now)
1720 1720 _landing_revision = Column(
1721 1721 "landing_revision", String(255), nullable=False, unique=False,
1722 1722 default=None)
1723 1723 enable_locking = Column(
1724 1724 "enable_locking", Boolean(), nullable=False, unique=None,
1725 1725 default=False)
1726 1726 _locked = Column(
1727 1727 "locked", String(255), nullable=True, unique=False, default=None)
1728 1728 _changeset_cache = Column(
1729 1729 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1730 1730
1731 1731 fork_id = Column(
1732 1732 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1733 1733 nullable=True, unique=False, default=None)
1734 1734 group_id = Column(
1735 1735 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1736 1736 unique=False, default=None)
1737 1737
1738 1738 user = relationship('User', lazy='joined')
1739 1739 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1740 1740 group = relationship('RepoGroup', lazy='joined')
1741 1741 repo_to_perm = relationship(
1742 1742 'UserRepoToPerm', cascade='all',
1743 1743 order_by='UserRepoToPerm.repo_to_perm_id')
1744 1744 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1745 1745 stats = relationship('Statistics', cascade='all', uselist=False)
1746 1746
1747 1747 followers = relationship(
1748 1748 'UserFollowing',
1749 1749 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1750 1750 cascade='all')
1751 1751 extra_fields = relationship(
1752 1752 'RepositoryField', cascade="all, delete-orphan")
1753 1753 logs = relationship('UserLog')
1754 1754 comments = relationship(
1755 1755 'ChangesetComment', cascade="all, delete-orphan")
1756 1756 pull_requests_source = relationship(
1757 1757 'PullRequest',
1758 1758 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1759 1759 cascade="all, delete-orphan")
1760 1760 pull_requests_target = relationship(
1761 1761 'PullRequest',
1762 1762 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1763 1763 cascade="all, delete-orphan")
1764 1764 ui = relationship('RepoRhodeCodeUi', cascade="all")
1765 1765 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1766 1766 integrations = relationship('Integration', cascade="all, delete-orphan")
1767 1767
1768 1768 scoped_tokens = relationship('UserApiKeys', cascade="all")
1769 1769
1770 1770 # no cascade, set NULL
1771 1771 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1772 1772
1773 1773 def __unicode__(self):
1774 1774 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1775 1775 safe_unicode(self.repo_name))
1776 1776
1777 1777 @hybrid_property
1778 1778 def description_safe(self):
1779 1779 from rhodecode.lib import helpers as h
1780 1780 return h.escape(self.description)
1781 1781
1782 1782 @hybrid_property
1783 1783 def landing_rev(self):
1784 1784 # always should return [rev_type, rev], e.g ['branch', 'master']
1785 1785 if self._landing_revision:
1786 1786 _rev_info = self._landing_revision.split(':')
1787 1787 if len(_rev_info) < 2:
1788 1788 _rev_info.insert(0, 'rev')
1789 1789 return [_rev_info[0], _rev_info[1]]
1790 1790 return [None, None]
1791 1791
1792 1792 @property
1793 1793 def landing_ref_type(self):
1794 1794 return self.landing_rev[0]
1795 1795
1796 1796 @property
1797 1797 def landing_ref_name(self):
1798 1798 return self.landing_rev[1]
1799 1799
1800 1800 @landing_rev.setter
1801 1801 def landing_rev(self, val):
1802 1802 if ':' not in val:
1803 1803 raise ValueError('value must be delimited with `:` and consist '
1804 1804 'of <rev_type>:<rev>, got %s instead' % val)
1805 1805 self._landing_revision = val
1806 1806
1807 1807 @hybrid_property
1808 1808 def locked(self):
1809 1809 if self._locked:
1810 1810 user_id, timelocked, reason = self._locked.split(':')
1811 1811 lock_values = int(user_id), timelocked, reason
1812 1812 else:
1813 1813 lock_values = [None, None, None]
1814 1814 return lock_values
1815 1815
1816 1816 @locked.setter
1817 1817 def locked(self, val):
1818 1818 if val and isinstance(val, (list, tuple)):
1819 1819 self._locked = ':'.join(map(str, val))
1820 1820 else:
1821 1821 self._locked = None
1822 1822
1823 1823 @classmethod
1824 1824 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1825 1825 from rhodecode.lib.vcs.backends.base import EmptyCommit
1826 1826 dummy = EmptyCommit().__json__()
1827 1827 if not changeset_cache_raw:
1828 1828 dummy['source_repo_id'] = repo_id
1829 1829 return json.loads(json.dumps(dummy))
1830 1830
1831 1831 try:
1832 1832 return json.loads(changeset_cache_raw)
1833 1833 except TypeError:
1834 1834 return dummy
1835 1835 except Exception:
1836 1836 log.error(traceback.format_exc())
1837 1837 return dummy
1838 1838
1839 1839 @hybrid_property
1840 1840 def changeset_cache(self):
1841 1841 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1842 1842
1843 1843 @changeset_cache.setter
1844 1844 def changeset_cache(self, val):
1845 1845 try:
1846 1846 self._changeset_cache = json.dumps(val)
1847 1847 except Exception:
1848 1848 log.error(traceback.format_exc())
1849 1849
1850 1850 @hybrid_property
1851 1851 def repo_name(self):
1852 1852 return self._repo_name
1853 1853
1854 1854 @repo_name.setter
1855 1855 def repo_name(self, value):
1856 1856 self._repo_name = value
1857 1857 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1858 1858
1859 1859 @classmethod
1860 1860 def normalize_repo_name(cls, repo_name):
1861 1861 """
1862 1862 Normalizes os specific repo_name to the format internally stored inside
1863 1863 database using URL_SEP
1864 1864
1865 1865 :param cls:
1866 1866 :param repo_name:
1867 1867 """
1868 1868 return cls.NAME_SEP.join(repo_name.split(os.sep))
1869 1869
1870 1870 @classmethod
1871 1871 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1872 1872 session = Session()
1873 1873 q = session.query(cls).filter(cls.repo_name == repo_name)
1874 1874
1875 1875 if cache:
1876 1876 if identity_cache:
1877 1877 val = cls.identity_cache(session, 'repo_name', repo_name)
1878 1878 if val:
1879 1879 return val
1880 1880 else:
1881 1881 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1882 1882 q = q.options(
1883 1883 FromCache("sql_cache_short", cache_key))
1884 1884
1885 1885 return q.scalar()
1886 1886
1887 1887 @classmethod
1888 1888 def get_by_id_or_repo_name(cls, repoid):
1889 1889 if isinstance(repoid, (int, long)):
1890 1890 try:
1891 1891 repo = cls.get(repoid)
1892 1892 except ValueError:
1893 1893 repo = None
1894 1894 else:
1895 1895 repo = cls.get_by_repo_name(repoid)
1896 1896 return repo
1897 1897
1898 1898 @classmethod
1899 1899 def get_by_full_path(cls, repo_full_path):
1900 1900 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1901 1901 repo_name = cls.normalize_repo_name(repo_name)
1902 1902 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1903 1903
1904 1904 @classmethod
1905 1905 def get_repo_forks(cls, repo_id):
1906 1906 return cls.query().filter(Repository.fork_id == repo_id)
1907 1907
1908 1908 @classmethod
1909 1909 def base_path(cls):
1910 1910 """
1911 1911 Returns base path when all repos are stored
1912 1912
1913 1913 :param cls:
1914 1914 """
1915 1915 q = Session().query(RhodeCodeUi)\
1916 1916 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1917 1917 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1918 1918 return q.one().ui_value
1919 1919
1920 1920 @classmethod
1921 1921 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1922 1922 case_insensitive=True, archived=False):
1923 1923 q = Repository.query()
1924 1924
1925 1925 if not archived:
1926 1926 q = q.filter(Repository.archived.isnot(true()))
1927 1927
1928 1928 if not isinstance(user_id, Optional):
1929 1929 q = q.filter(Repository.user_id == user_id)
1930 1930
1931 1931 if not isinstance(group_id, Optional):
1932 1932 q = q.filter(Repository.group_id == group_id)
1933 1933
1934 1934 if case_insensitive:
1935 1935 q = q.order_by(func.lower(Repository.repo_name))
1936 1936 else:
1937 1937 q = q.order_by(Repository.repo_name)
1938 1938
1939 1939 return q.all()
1940 1940
1941 1941 @property
1942 1942 def repo_uid(self):
1943 1943 return '_{}'.format(self.repo_id)
1944 1944
1945 1945 @property
1946 1946 def forks(self):
1947 1947 """
1948 1948 Return forks of this repo
1949 1949 """
1950 1950 return Repository.get_repo_forks(self.repo_id)
1951 1951
1952 1952 @property
1953 1953 def parent(self):
1954 1954 """
1955 1955 Returns fork parent
1956 1956 """
1957 1957 return self.fork
1958 1958
1959 1959 @property
1960 1960 def just_name(self):
1961 1961 return self.repo_name.split(self.NAME_SEP)[-1]
1962 1962
1963 1963 @property
1964 1964 def groups_with_parents(self):
1965 1965 groups = []
1966 1966 if self.group is None:
1967 1967 return groups
1968 1968
1969 1969 cur_gr = self.group
1970 1970 groups.insert(0, cur_gr)
1971 1971 while 1:
1972 1972 gr = getattr(cur_gr, 'parent_group', None)
1973 1973 cur_gr = cur_gr.parent_group
1974 1974 if gr is None:
1975 1975 break
1976 1976 groups.insert(0, gr)
1977 1977
1978 1978 return groups
1979 1979
1980 1980 @property
1981 1981 def groups_and_repo(self):
1982 1982 return self.groups_with_parents, self
1983 1983
1984 1984 @LazyProperty
1985 1985 def repo_path(self):
1986 1986 """
1987 1987 Returns base full path for that repository means where it actually
1988 1988 exists on a filesystem
1989 1989 """
1990 1990 q = Session().query(RhodeCodeUi).filter(
1991 1991 RhodeCodeUi.ui_key == self.NAME_SEP)
1992 1992 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1993 1993 return q.one().ui_value
1994 1994
1995 1995 @property
1996 1996 def repo_full_path(self):
1997 1997 p = [self.repo_path]
1998 1998 # we need to split the name by / since this is how we store the
1999 1999 # names in the database, but that eventually needs to be converted
2000 2000 # into a valid system path
2001 2001 p += self.repo_name.split(self.NAME_SEP)
2002 2002 return os.path.join(*map(safe_unicode, p))
2003 2003
2004 2004 @property
2005 2005 def cache_keys(self):
2006 2006 """
2007 2007 Returns associated cache keys for that repo
2008 2008 """
2009 2009 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2010 2010 repo_id=self.repo_id)
2011 2011 return CacheKey.query()\
2012 2012 .filter(CacheKey.cache_args == invalidation_namespace)\
2013 2013 .order_by(CacheKey.cache_key)\
2014 2014 .all()
2015 2015
2016 2016 @property
2017 2017 def cached_diffs_relative_dir(self):
2018 2018 """
2019 2019 Return a relative to the repository store path of cached diffs
2020 2020 used for safe display for users, who shouldn't know the absolute store
2021 2021 path
2022 2022 """
2023 2023 return os.path.join(
2024 2024 os.path.dirname(self.repo_name),
2025 2025 self.cached_diffs_dir.split(os.path.sep)[-1])
2026 2026
2027 2027 @property
2028 2028 def cached_diffs_dir(self):
2029 2029 path = self.repo_full_path
2030 2030 return os.path.join(
2031 2031 os.path.dirname(path),
2032 2032 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2033 2033
2034 2034 def cached_diffs(self):
2035 2035 diff_cache_dir = self.cached_diffs_dir
2036 2036 if os.path.isdir(diff_cache_dir):
2037 2037 return os.listdir(diff_cache_dir)
2038 2038 return []
2039 2039
2040 2040 def shadow_repos(self):
2041 2041 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2042 2042 return [
2043 2043 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2044 2044 if x.startswith(shadow_repos_pattern)]
2045 2045
2046 2046 def get_new_name(self, repo_name):
2047 2047 """
2048 2048 returns new full repository name based on assigned group and new new
2049 2049
2050 2050 :param group_name:
2051 2051 """
2052 2052 path_prefix = self.group.full_path_splitted if self.group else []
2053 2053 return self.NAME_SEP.join(path_prefix + [repo_name])
2054 2054
2055 2055 @property
2056 2056 def _config(self):
2057 2057 """
2058 2058 Returns db based config object.
2059 2059 """
2060 2060 from rhodecode.lib.utils import make_db_config
2061 2061 return make_db_config(clear_session=False, repo=self)
2062 2062
2063 2063 def permissions(self, with_admins=True, with_owner=True,
2064 2064 expand_from_user_groups=False):
2065 2065 """
2066 2066 Permissions for repositories
2067 2067 """
2068 2068 _admin_perm = 'repository.admin'
2069 2069
2070 2070 owner_row = []
2071 2071 if with_owner:
2072 2072 usr = AttributeDict(self.user.get_dict())
2073 2073 usr.owner_row = True
2074 2074 usr.permission = _admin_perm
2075 2075 usr.permission_id = None
2076 2076 owner_row.append(usr)
2077 2077
2078 2078 super_admin_ids = []
2079 2079 super_admin_rows = []
2080 2080 if with_admins:
2081 2081 for usr in User.get_all_super_admins():
2082 2082 super_admin_ids.append(usr.user_id)
2083 2083 # if this admin is also owner, don't double the record
2084 2084 if usr.user_id == owner_row[0].user_id:
2085 2085 owner_row[0].admin_row = True
2086 2086 else:
2087 2087 usr = AttributeDict(usr.get_dict())
2088 2088 usr.admin_row = True
2089 2089 usr.permission = _admin_perm
2090 2090 usr.permission_id = None
2091 2091 super_admin_rows.append(usr)
2092 2092
2093 2093 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2094 2094 q = q.options(joinedload(UserRepoToPerm.repository),
2095 2095 joinedload(UserRepoToPerm.user),
2096 2096 joinedload(UserRepoToPerm.permission),)
2097 2097
2098 2098 # get owners and admins and permissions. We do a trick of re-writing
2099 2099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2100 2100 # has a global reference and changing one object propagates to all
2101 2101 # others. This means if admin is also an owner admin_row that change
2102 2102 # would propagate to both objects
2103 2103 perm_rows = []
2104 2104 for _usr in q.all():
2105 2105 usr = AttributeDict(_usr.user.get_dict())
2106 2106 # if this user is also owner/admin, mark as duplicate record
2107 2107 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2108 2108 usr.duplicate_perm = True
2109 2109 # also check if this permission is maybe used by branch_permissions
2110 2110 if _usr.branch_perm_entry:
2111 2111 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2112 2112
2113 2113 usr.permission = _usr.permission.permission_name
2114 2114 usr.permission_id = _usr.repo_to_perm_id
2115 2115 perm_rows.append(usr)
2116 2116
2117 2117 # filter the perm rows by 'default' first and then sort them by
2118 2118 # admin,write,read,none permissions sorted again alphabetically in
2119 2119 # each group
2120 2120 perm_rows = sorted(perm_rows, key=display_user_sort)
2121 2121
2122 2122 user_groups_rows = []
2123 2123 if expand_from_user_groups:
2124 2124 for ug in self.permission_user_groups(with_members=True):
2125 2125 for user_data in ug.members:
2126 2126 user_groups_rows.append(user_data)
2127 2127
2128 2128 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2129 2129
2130 2130 def permission_user_groups(self, with_members=True):
2131 2131 q = UserGroupRepoToPerm.query()\
2132 2132 .filter(UserGroupRepoToPerm.repository == self)
2133 2133 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2134 2134 joinedload(UserGroupRepoToPerm.users_group),
2135 2135 joinedload(UserGroupRepoToPerm.permission),)
2136 2136
2137 2137 perm_rows = []
2138 2138 for _user_group in q.all():
2139 2139 entry = AttributeDict(_user_group.users_group.get_dict())
2140 2140 entry.permission = _user_group.permission.permission_name
2141 2141 if with_members:
2142 2142 entry.members = [x.user.get_dict()
2143 2143 for x in _user_group.users_group.members]
2144 2144 perm_rows.append(entry)
2145 2145
2146 2146 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2147 2147 return perm_rows
2148 2148
2149 2149 def get_api_data(self, include_secrets=False):
2150 2150 """
2151 2151 Common function for generating repo api data
2152 2152
2153 2153 :param include_secrets: See :meth:`User.get_api_data`.
2154 2154
2155 2155 """
2156 2156 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2157 2157 # move this methods on models level.
2158 2158 from rhodecode.model.settings import SettingsModel
2159 2159 from rhodecode.model.repo import RepoModel
2160 2160
2161 2161 repo = self
2162 2162 _user_id, _time, _reason = self.locked
2163 2163
2164 2164 data = {
2165 2165 'repo_id': repo.repo_id,
2166 2166 'repo_name': repo.repo_name,
2167 2167 'repo_type': repo.repo_type,
2168 2168 'clone_uri': repo.clone_uri or '',
2169 2169 'push_uri': repo.push_uri or '',
2170 2170 'url': RepoModel().get_url(self),
2171 2171 'private': repo.private,
2172 2172 'created_on': repo.created_on,
2173 2173 'description': repo.description_safe,
2174 2174 'landing_rev': repo.landing_rev,
2175 2175 'owner': repo.user.username,
2176 2176 'fork_of': repo.fork.repo_name if repo.fork else None,
2177 2177 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2178 2178 'enable_statistics': repo.enable_statistics,
2179 2179 'enable_locking': repo.enable_locking,
2180 2180 'enable_downloads': repo.enable_downloads,
2181 2181 'last_changeset': repo.changeset_cache,
2182 2182 'locked_by': User.get(_user_id).get_api_data(
2183 2183 include_secrets=include_secrets) if _user_id else None,
2184 2184 'locked_date': time_to_datetime(_time) if _time else None,
2185 2185 'lock_reason': _reason if _reason else None,
2186 2186 }
2187 2187
2188 2188 # TODO: mikhail: should be per-repo settings here
2189 2189 rc_config = SettingsModel().get_all_settings()
2190 2190 repository_fields = str2bool(
2191 2191 rc_config.get('rhodecode_repository_fields'))
2192 2192 if repository_fields:
2193 2193 for f in self.extra_fields:
2194 2194 data[f.field_key_prefixed] = f.field_value
2195 2195
2196 2196 return data
2197 2197
2198 2198 @classmethod
2199 2199 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2200 2200 if not lock_time:
2201 2201 lock_time = time.time()
2202 2202 if not lock_reason:
2203 2203 lock_reason = cls.LOCK_AUTOMATIC
2204 2204 repo.locked = [user_id, lock_time, lock_reason]
2205 2205 Session().add(repo)
2206 2206 Session().commit()
2207 2207
2208 2208 @classmethod
2209 2209 def unlock(cls, repo):
2210 2210 repo.locked = None
2211 2211 Session().add(repo)
2212 2212 Session().commit()
2213 2213
2214 2214 @classmethod
2215 2215 def getlock(cls, repo):
2216 2216 return repo.locked
2217 2217
2218 2218 def is_user_lock(self, user_id):
2219 2219 if self.lock[0]:
2220 2220 lock_user_id = safe_int(self.lock[0])
2221 2221 user_id = safe_int(user_id)
2222 2222 # both are ints, and they are equal
2223 2223 return all([lock_user_id, user_id]) and lock_user_id == user_id
2224 2224
2225 2225 return False
2226 2226
2227 2227 def get_locking_state(self, action, user_id, only_when_enabled=True):
2228 2228 """
2229 2229 Checks locking on this repository, if locking is enabled and lock is
2230 2230 present returns a tuple of make_lock, locked, locked_by.
2231 2231 make_lock can have 3 states None (do nothing) True, make lock
2232 2232 False release lock, This value is later propagated to hooks, which
2233 2233 do the locking. Think about this as signals passed to hooks what to do.
2234 2234
2235 2235 """
2236 2236 # TODO: johbo: This is part of the business logic and should be moved
2237 2237 # into the RepositoryModel.
2238 2238
2239 2239 if action not in ('push', 'pull'):
2240 2240 raise ValueError("Invalid action value: %s" % repr(action))
2241 2241
2242 2242 # defines if locked error should be thrown to user
2243 2243 currently_locked = False
2244 2244 # defines if new lock should be made, tri-state
2245 2245 make_lock = None
2246 2246 repo = self
2247 2247 user = User.get(user_id)
2248 2248
2249 2249 lock_info = repo.locked
2250 2250
2251 2251 if repo and (repo.enable_locking or not only_when_enabled):
2252 2252 if action == 'push':
2253 2253 # check if it's already locked !, if it is compare users
2254 2254 locked_by_user_id = lock_info[0]
2255 2255 if user.user_id == locked_by_user_id:
2256 2256 log.debug(
2257 2257 'Got `push` action from user %s, now unlocking', user)
2258 2258 # unlock if we have push from user who locked
2259 2259 make_lock = False
2260 2260 else:
2261 2261 # we're not the same user who locked, ban with
2262 2262 # code defined in settings (default is 423 HTTP Locked) !
2263 2263 log.debug('Repo %s is currently locked by %s', repo, user)
2264 2264 currently_locked = True
2265 2265 elif action == 'pull':
2266 2266 # [0] user [1] date
2267 2267 if lock_info[0] and lock_info[1]:
2268 2268 log.debug('Repo %s is currently locked by %s', repo, user)
2269 2269 currently_locked = True
2270 2270 else:
2271 2271 log.debug('Setting lock on repo %s by %s', repo, user)
2272 2272 make_lock = True
2273 2273
2274 2274 else:
2275 2275 log.debug('Repository %s do not have locking enabled', repo)
2276 2276
2277 2277 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2278 2278 make_lock, currently_locked, lock_info)
2279 2279
2280 2280 from rhodecode.lib.auth import HasRepoPermissionAny
2281 2281 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2282 2282 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2283 2283 # if we don't have at least write permission we cannot make a lock
2284 2284 log.debug('lock state reset back to FALSE due to lack '
2285 2285 'of at least read permission')
2286 2286 make_lock = False
2287 2287
2288 2288 return make_lock, currently_locked, lock_info
2289 2289
2290 2290 @property
2291 2291 def last_commit_cache_update_diff(self):
2292 2292 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2293 2293
2294 2294 @classmethod
2295 2295 def _load_commit_change(cls, last_commit_cache):
2296 2296 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2297 2297 empty_date = datetime.datetime.fromtimestamp(0)
2298 2298 date_latest = last_commit_cache.get('date', empty_date)
2299 2299 try:
2300 2300 return parse_datetime(date_latest)
2301 2301 except Exception:
2302 2302 return empty_date
2303 2303
2304 2304 @property
2305 2305 def last_commit_change(self):
2306 2306 return self._load_commit_change(self.changeset_cache)
2307 2307
2308 2308 @property
2309 2309 def last_db_change(self):
2310 2310 return self.updated_on
2311 2311
2312 2312 @property
2313 2313 def clone_uri_hidden(self):
2314 2314 clone_uri = self.clone_uri
2315 2315 if clone_uri:
2316 2316 import urlobject
2317 2317 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2318 2318 if url_obj.password:
2319 2319 clone_uri = url_obj.with_password('*****')
2320 2320 return clone_uri
2321 2321
2322 2322 @property
2323 2323 def push_uri_hidden(self):
2324 2324 push_uri = self.push_uri
2325 2325 if push_uri:
2326 2326 import urlobject
2327 2327 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2328 2328 if url_obj.password:
2329 2329 push_uri = url_obj.with_password('*****')
2330 2330 return push_uri
2331 2331
2332 2332 def clone_url(self, **override):
2333 2333 from rhodecode.model.settings import SettingsModel
2334 2334
2335 2335 uri_tmpl = None
2336 2336 if 'with_id' in override:
2337 2337 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2338 2338 del override['with_id']
2339 2339
2340 2340 if 'uri_tmpl' in override:
2341 2341 uri_tmpl = override['uri_tmpl']
2342 2342 del override['uri_tmpl']
2343 2343
2344 2344 ssh = False
2345 2345 if 'ssh' in override:
2346 2346 ssh = True
2347 2347 del override['ssh']
2348 2348
2349 2349 # we didn't override our tmpl from **overrides
2350 2350 request = get_current_request()
2351 2351 if not uri_tmpl:
2352 2352 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2353 2353 rc_config = request.call_context.rc_config
2354 2354 else:
2355 2355 rc_config = SettingsModel().get_all_settings(cache=True)
2356 2356
2357 2357 if ssh:
2358 2358 uri_tmpl = rc_config.get(
2359 2359 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2360 2360
2361 2361 else:
2362 2362 uri_tmpl = rc_config.get(
2363 2363 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2364 2364
2365 2365 return get_clone_url(request=request,
2366 2366 uri_tmpl=uri_tmpl,
2367 2367 repo_name=self.repo_name,
2368 2368 repo_id=self.repo_id,
2369 2369 repo_type=self.repo_type,
2370 2370 **override)
2371 2371
2372 2372 def set_state(self, state):
2373 2373 self.repo_state = state
2374 2374 Session().add(self)
2375 2375 #==========================================================================
2376 2376 # SCM PROPERTIES
2377 2377 #==========================================================================
2378 2378
2379 2379 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2380 2380 return get_commit_safe(
2381 2381 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2382 2382 maybe_unreachable=maybe_unreachable)
2383 2383
2384 2384 def get_changeset(self, rev=None, pre_load=None):
2385 2385 warnings.warn("Use get_commit", DeprecationWarning)
2386 2386 commit_id = None
2387 2387 commit_idx = None
2388 2388 if isinstance(rev, compat.string_types):
2389 2389 commit_id = rev
2390 2390 else:
2391 2391 commit_idx = rev
2392 2392 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2393 2393 pre_load=pre_load)
2394 2394
2395 2395 def get_landing_commit(self):
2396 2396 """
2397 2397 Returns landing commit, or if that doesn't exist returns the tip
2398 2398 """
2399 2399 _rev_type, _rev = self.landing_rev
2400 2400 commit = self.get_commit(_rev)
2401 2401 if isinstance(commit, EmptyCommit):
2402 2402 return self.get_commit()
2403 2403 return commit
2404 2404
2405 2405 def flush_commit_cache(self):
2406 2406 self.update_commit_cache(cs_cache={'raw_id':'0'})
2407 2407 self.update_commit_cache()
2408 2408
2409 2409 def update_commit_cache(self, cs_cache=None, config=None):
2410 2410 """
2411 2411 Update cache of last commit for repository
2412 2412 cache_keys should be::
2413 2413
2414 2414 source_repo_id
2415 2415 short_id
2416 2416 raw_id
2417 2417 revision
2418 2418 parents
2419 2419 message
2420 2420 date
2421 2421 author
2422 2422 updated_on
2423 2423
2424 2424 """
2425 2425 from rhodecode.lib.vcs.backends.base import BaseChangeset
2426 2426 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2427 2427 empty_date = datetime.datetime.fromtimestamp(0)
2428 2428
2429 2429 if cs_cache is None:
2430 2430 # use no-cache version here
2431 2431 try:
2432 2432 scm_repo = self.scm_instance(cache=False, config=config)
2433 2433 except VCSError:
2434 2434 scm_repo = None
2435 2435 empty = scm_repo is None or scm_repo.is_empty()
2436 2436
2437 2437 if not empty:
2438 2438 cs_cache = scm_repo.get_commit(
2439 2439 pre_load=["author", "date", "message", "parents", "branch"])
2440 2440 else:
2441 2441 cs_cache = EmptyCommit()
2442 2442
2443 2443 if isinstance(cs_cache, BaseChangeset):
2444 2444 cs_cache = cs_cache.__json__()
2445 2445
2446 2446 def is_outdated(new_cs_cache):
2447 2447 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2448 2448 new_cs_cache['revision'] != self.changeset_cache['revision']):
2449 2449 return True
2450 2450 return False
2451 2451
2452 2452 # check if we have maybe already latest cached revision
2453 2453 if is_outdated(cs_cache) or not self.changeset_cache:
2454 2454 _current_datetime = datetime.datetime.utcnow()
2455 2455 last_change = cs_cache.get('date') or _current_datetime
2456 2456 # we check if last update is newer than the new value
2457 2457 # if yes, we use the current timestamp instead. Imagine you get
2458 2458 # old commit pushed 1y ago, we'd set last update 1y to ago.
2459 2459 last_change_timestamp = datetime_to_time(last_change)
2460 2460 current_timestamp = datetime_to_time(last_change)
2461 2461 if last_change_timestamp > current_timestamp and not empty:
2462 2462 cs_cache['date'] = _current_datetime
2463 2463
2464 2464 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2465 2465 cs_cache['updated_on'] = time.time()
2466 2466 self.changeset_cache = cs_cache
2467 2467 self.updated_on = last_change
2468 2468 Session().add(self)
2469 2469 Session().commit()
2470 2470
2471 2471 else:
2472 2472 if empty:
2473 2473 cs_cache = EmptyCommit().__json__()
2474 2474 else:
2475 2475 cs_cache = self.changeset_cache
2476 2476
2477 2477 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2478 2478
2479 2479 cs_cache['updated_on'] = time.time()
2480 2480 self.changeset_cache = cs_cache
2481 2481 self.updated_on = _date_latest
2482 2482 Session().add(self)
2483 2483 Session().commit()
2484 2484
2485 2485 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2486 2486 self.repo_name, cs_cache, _date_latest)
2487 2487
2488 2488 @property
2489 2489 def tip(self):
2490 2490 return self.get_commit('tip')
2491 2491
2492 2492 @property
2493 2493 def author(self):
2494 2494 return self.tip.author
2495 2495
2496 2496 @property
2497 2497 def last_change(self):
2498 2498 return self.scm_instance().last_change
2499 2499
2500 2500 def get_comments(self, revisions=None):
2501 2501 """
2502 2502 Returns comments for this repository grouped by revisions
2503 2503
2504 2504 :param revisions: filter query by revisions only
2505 2505 """
2506 2506 cmts = ChangesetComment.query()\
2507 2507 .filter(ChangesetComment.repo == self)
2508 2508 if revisions:
2509 2509 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2510 2510 grouped = collections.defaultdict(list)
2511 2511 for cmt in cmts.all():
2512 2512 grouped[cmt.revision].append(cmt)
2513 2513 return grouped
2514 2514
2515 2515 def statuses(self, revisions=None):
2516 2516 """
2517 2517 Returns statuses for this repository
2518 2518
2519 2519 :param revisions: list of revisions to get statuses for
2520 2520 """
2521 2521 statuses = ChangesetStatus.query()\
2522 2522 .filter(ChangesetStatus.repo == self)\
2523 2523 .filter(ChangesetStatus.version == 0)
2524 2524
2525 2525 if revisions:
2526 2526 # Try doing the filtering in chunks to avoid hitting limits
2527 2527 size = 500
2528 2528 status_results = []
2529 2529 for chunk in xrange(0, len(revisions), size):
2530 2530 status_results += statuses.filter(
2531 2531 ChangesetStatus.revision.in_(
2532 2532 revisions[chunk: chunk+size])
2533 2533 ).all()
2534 2534 else:
2535 2535 status_results = statuses.all()
2536 2536
2537 2537 grouped = {}
2538 2538
2539 2539 # maybe we have open new pullrequest without a status?
2540 2540 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2541 2541 status_lbl = ChangesetStatus.get_status_lbl(stat)
2542 2542 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2543 2543 for rev in pr.revisions:
2544 2544 pr_id = pr.pull_request_id
2545 2545 pr_repo = pr.target_repo.repo_name
2546 2546 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2547 2547
2548 2548 for stat in status_results:
2549 2549 pr_id = pr_repo = None
2550 2550 if stat.pull_request:
2551 2551 pr_id = stat.pull_request.pull_request_id
2552 2552 pr_repo = stat.pull_request.target_repo.repo_name
2553 2553 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2554 2554 pr_id, pr_repo]
2555 2555 return grouped
2556 2556
2557 2557 # ==========================================================================
2558 2558 # SCM CACHE INSTANCE
2559 2559 # ==========================================================================
2560 2560
2561 2561 def scm_instance(self, **kwargs):
2562 2562 import rhodecode
2563 2563
2564 2564 # Passing a config will not hit the cache currently only used
2565 2565 # for repo2dbmapper
2566 2566 config = kwargs.pop('config', None)
2567 2567 cache = kwargs.pop('cache', None)
2568 2568 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2569 2569 if vcs_full_cache is not None:
2570 2570 # allows override global config
2571 2571 full_cache = vcs_full_cache
2572 2572 else:
2573 2573 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2574 2574 # if cache is NOT defined use default global, else we have a full
2575 2575 # control over cache behaviour
2576 2576 if cache is None and full_cache and not config:
2577 2577 log.debug('Initializing pure cached instance for %s', self.repo_path)
2578 2578 return self._get_instance_cached()
2579 2579
2580 2580 # cache here is sent to the "vcs server"
2581 2581 return self._get_instance(cache=bool(cache), config=config)
2582 2582
2583 2583 def _get_instance_cached(self):
2584 2584 from rhodecode.lib import rc_cache
2585 2585
2586 2586 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2587 2587 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2588 2588 repo_id=self.repo_id)
2589 2589 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2590 2590
2591 2591 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2592 2592 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2593 2593 return self._get_instance(repo_state_uid=_cache_state_uid)
2594 2594
2595 2595 # we must use thread scoped cache here,
2596 2596 # because each thread of gevent needs it's own not shared connection and cache
2597 2597 # we also alter `args` so the cache key is individual for every green thread.
2598 2598 inv_context_manager = rc_cache.InvalidationContext(
2599 2599 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2600 2600 thread_scoped=True)
2601 2601 with inv_context_manager as invalidation_context:
2602 2602 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2603 2603 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2604 2604
2605 2605 # re-compute and store cache if we get invalidate signal
2606 2606 if invalidation_context.should_invalidate():
2607 2607 instance = get_instance_cached.refresh(*args)
2608 2608 else:
2609 2609 instance = get_instance_cached(*args)
2610 2610
2611 2611 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2612 2612 return instance
2613 2613
2614 2614 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2615 2615 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2616 2616 self.repo_type, self.repo_path, cache)
2617 2617 config = config or self._config
2618 2618 custom_wire = {
2619 2619 'cache': cache, # controls the vcs.remote cache
2620 2620 'repo_state_uid': repo_state_uid
2621 2621 }
2622 2622 repo = get_vcs_instance(
2623 2623 repo_path=safe_str(self.repo_full_path),
2624 2624 config=config,
2625 2625 with_wire=custom_wire,
2626 2626 create=False,
2627 2627 _vcs_alias=self.repo_type)
2628 2628 if repo is not None:
2629 2629 repo.count() # cache rebuild
2630 2630 return repo
2631 2631
2632 2632 def get_shadow_repository_path(self, workspace_id):
2633 2633 from rhodecode.lib.vcs.backends.base import BaseRepository
2634 2634 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2635 2635 self.repo_full_path, self.repo_id, workspace_id)
2636 2636 return shadow_repo_path
2637 2637
2638 2638 def __json__(self):
2639 2639 return {'landing_rev': self.landing_rev}
2640 2640
2641 2641 def get_dict(self):
2642 2642
2643 2643 # Since we transformed `repo_name` to a hybrid property, we need to
2644 2644 # keep compatibility with the code which uses `repo_name` field.
2645 2645
2646 2646 result = super(Repository, self).get_dict()
2647 2647 result['repo_name'] = result.pop('_repo_name', None)
2648 2648 return result
2649 2649
2650 2650
2651 2651 class RepoGroup(Base, BaseModel):
2652 2652 __tablename__ = 'groups'
2653 2653 __table_args__ = (
2654 2654 UniqueConstraint('group_name', 'group_parent_id'),
2655 2655 base_table_args,
2656 2656 )
2657 2657 __mapper_args__ = {'order_by': 'group_name'}
2658 2658
2659 2659 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2660 2660
2661 2661 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2662 2662 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2663 2663 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2664 2664 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2665 2665 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2666 2666 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2667 2667 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2668 2668 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2669 2669 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2670 2670 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2671 2671 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2672 2672
2673 2673 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2674 2674 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2675 2675 parent_group = relationship('RepoGroup', remote_side=group_id)
2676 2676 user = relationship('User')
2677 2677 integrations = relationship('Integration', cascade="all, delete-orphan")
2678 2678
2679 2679 # no cascade, set NULL
2680 2680 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2681 2681
2682 2682 def __init__(self, group_name='', parent_group=None):
2683 2683 self.group_name = group_name
2684 2684 self.parent_group = parent_group
2685 2685
2686 2686 def __unicode__(self):
2687 2687 return u"<%s('id:%s:%s')>" % (
2688 2688 self.__class__.__name__, self.group_id, self.group_name)
2689 2689
2690 2690 @hybrid_property
2691 2691 def group_name(self):
2692 2692 return self._group_name
2693 2693
2694 2694 @group_name.setter
2695 2695 def group_name(self, value):
2696 2696 self._group_name = value
2697 2697 self.group_name_hash = self.hash_repo_group_name(value)
2698 2698
2699 2699 @classmethod
2700 2700 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2701 2701 from rhodecode.lib.vcs.backends.base import EmptyCommit
2702 2702 dummy = EmptyCommit().__json__()
2703 2703 if not changeset_cache_raw:
2704 2704 dummy['source_repo_id'] = repo_id
2705 2705 return json.loads(json.dumps(dummy))
2706 2706
2707 2707 try:
2708 2708 return json.loads(changeset_cache_raw)
2709 2709 except TypeError:
2710 2710 return dummy
2711 2711 except Exception:
2712 2712 log.error(traceback.format_exc())
2713 2713 return dummy
2714 2714
2715 2715 @hybrid_property
2716 2716 def changeset_cache(self):
2717 2717 return self._load_changeset_cache('', self._changeset_cache)
2718 2718
2719 2719 @changeset_cache.setter
2720 2720 def changeset_cache(self, val):
2721 2721 try:
2722 2722 self._changeset_cache = json.dumps(val)
2723 2723 except Exception:
2724 2724 log.error(traceback.format_exc())
2725 2725
2726 2726 @validates('group_parent_id')
2727 2727 def validate_group_parent_id(self, key, val):
2728 2728 """
2729 2729 Check cycle references for a parent group to self
2730 2730 """
2731 2731 if self.group_id and val:
2732 2732 assert val != self.group_id
2733 2733
2734 2734 return val
2735 2735
2736 2736 @hybrid_property
2737 2737 def description_safe(self):
2738 2738 from rhodecode.lib import helpers as h
2739 2739 return h.escape(self.group_description)
2740 2740
2741 2741 @classmethod
2742 2742 def hash_repo_group_name(cls, repo_group_name):
2743 2743 val = remove_formatting(repo_group_name)
2744 2744 val = safe_str(val).lower()
2745 2745 chars = []
2746 2746 for c in val:
2747 2747 if c not in string.ascii_letters:
2748 2748 c = str(ord(c))
2749 2749 chars.append(c)
2750 2750
2751 2751 return ''.join(chars)
2752 2752
2753 2753 @classmethod
2754 2754 def _generate_choice(cls, repo_group):
2755 2755 from webhelpers2.html import literal as _literal
2756 2756 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2757 2757 return repo_group.group_id, _name(repo_group.full_path_splitted)
2758 2758
2759 2759 @classmethod
2760 2760 def groups_choices(cls, groups=None, show_empty_group=True):
2761 2761 if not groups:
2762 2762 groups = cls.query().all()
2763 2763
2764 2764 repo_groups = []
2765 2765 if show_empty_group:
2766 2766 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2767 2767
2768 2768 repo_groups.extend([cls._generate_choice(x) for x in groups])
2769 2769
2770 2770 repo_groups = sorted(
2771 2771 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2772 2772 return repo_groups
2773 2773
2774 2774 @classmethod
2775 2775 def url_sep(cls):
2776 2776 return URL_SEP
2777 2777
2778 2778 @classmethod
2779 2779 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2780 2780 if case_insensitive:
2781 2781 gr = cls.query().filter(func.lower(cls.group_name)
2782 2782 == func.lower(group_name))
2783 2783 else:
2784 2784 gr = cls.query().filter(cls.group_name == group_name)
2785 2785 if cache:
2786 2786 name_key = _hash_key(group_name)
2787 2787 gr = gr.options(
2788 2788 FromCache("sql_cache_short", "get_group_%s" % name_key))
2789 2789 return gr.scalar()
2790 2790
2791 2791 @classmethod
2792 2792 def get_user_personal_repo_group(cls, user_id):
2793 2793 user = User.get(user_id)
2794 2794 if user.username == User.DEFAULT_USER:
2795 2795 return None
2796 2796
2797 2797 return cls.query()\
2798 2798 .filter(cls.personal == true()) \
2799 2799 .filter(cls.user == user) \
2800 2800 .order_by(cls.group_id.asc()) \
2801 2801 .first()
2802 2802
2803 2803 @classmethod
2804 2804 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2805 2805 case_insensitive=True):
2806 2806 q = RepoGroup.query()
2807 2807
2808 2808 if not isinstance(user_id, Optional):
2809 2809 q = q.filter(RepoGroup.user_id == user_id)
2810 2810
2811 2811 if not isinstance(group_id, Optional):
2812 2812 q = q.filter(RepoGroup.group_parent_id == group_id)
2813 2813
2814 2814 if case_insensitive:
2815 2815 q = q.order_by(func.lower(RepoGroup.group_name))
2816 2816 else:
2817 2817 q = q.order_by(RepoGroup.group_name)
2818 2818 return q.all()
2819 2819
2820 2820 @property
2821 2821 def parents(self, parents_recursion_limit=10):
2822 2822 groups = []
2823 2823 if self.parent_group is None:
2824 2824 return groups
2825 2825 cur_gr = self.parent_group
2826 2826 groups.insert(0, cur_gr)
2827 2827 cnt = 0
2828 2828 while 1:
2829 2829 cnt += 1
2830 2830 gr = getattr(cur_gr, 'parent_group', None)
2831 2831 cur_gr = cur_gr.parent_group
2832 2832 if gr is None:
2833 2833 break
2834 2834 if cnt == parents_recursion_limit:
2835 2835 # this will prevent accidental infinit loops
2836 2836 log.error('more than %s parents found for group %s, stopping '
2837 2837 'recursive parent fetching', parents_recursion_limit, self)
2838 2838 break
2839 2839
2840 2840 groups.insert(0, gr)
2841 2841 return groups
2842 2842
2843 2843 @property
2844 2844 def last_commit_cache_update_diff(self):
2845 2845 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2846 2846
2847 2847 @classmethod
2848 2848 def _load_commit_change(cls, last_commit_cache):
2849 2849 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2850 2850 empty_date = datetime.datetime.fromtimestamp(0)
2851 2851 date_latest = last_commit_cache.get('date', empty_date)
2852 2852 try:
2853 2853 return parse_datetime(date_latest)
2854 2854 except Exception:
2855 2855 return empty_date
2856 2856
2857 2857 @property
2858 2858 def last_commit_change(self):
2859 2859 return self._load_commit_change(self.changeset_cache)
2860 2860
2861 2861 @property
2862 2862 def last_db_change(self):
2863 2863 return self.updated_on
2864 2864
2865 2865 @property
2866 2866 def children(self):
2867 2867 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2868 2868
2869 2869 @property
2870 2870 def name(self):
2871 2871 return self.group_name.split(RepoGroup.url_sep())[-1]
2872 2872
2873 2873 @property
2874 2874 def full_path(self):
2875 2875 return self.group_name
2876 2876
2877 2877 @property
2878 2878 def full_path_splitted(self):
2879 2879 return self.group_name.split(RepoGroup.url_sep())
2880 2880
2881 2881 @property
2882 2882 def repositories(self):
2883 2883 return Repository.query()\
2884 2884 .filter(Repository.group == self)\
2885 2885 .order_by(Repository.repo_name)
2886 2886
2887 2887 @property
2888 2888 def repositories_recursive_count(self):
2889 2889 cnt = self.repositories.count()
2890 2890
2891 2891 def children_count(group):
2892 2892 cnt = 0
2893 2893 for child in group.children:
2894 2894 cnt += child.repositories.count()
2895 2895 cnt += children_count(child)
2896 2896 return cnt
2897 2897
2898 2898 return cnt + children_count(self)
2899 2899
2900 2900 def _recursive_objects(self, include_repos=True, include_groups=True):
2901 2901 all_ = []
2902 2902
2903 2903 def _get_members(root_gr):
2904 2904 if include_repos:
2905 2905 for r in root_gr.repositories:
2906 2906 all_.append(r)
2907 2907 childs = root_gr.children.all()
2908 2908 if childs:
2909 2909 for gr in childs:
2910 2910 if include_groups:
2911 2911 all_.append(gr)
2912 2912 _get_members(gr)
2913 2913
2914 2914 root_group = []
2915 2915 if include_groups:
2916 2916 root_group = [self]
2917 2917
2918 2918 _get_members(self)
2919 2919 return root_group + all_
2920 2920
2921 2921 def recursive_groups_and_repos(self):
2922 2922 """
2923 2923 Recursive return all groups, with repositories in those groups
2924 2924 """
2925 2925 return self._recursive_objects()
2926 2926
2927 2927 def recursive_groups(self):
2928 2928 """
2929 2929 Returns all children groups for this group including children of children
2930 2930 """
2931 2931 return self._recursive_objects(include_repos=False)
2932 2932
2933 2933 def recursive_repos(self):
2934 2934 """
2935 2935 Returns all children repositories for this group
2936 2936 """
2937 2937 return self._recursive_objects(include_groups=False)
2938 2938
2939 2939 def get_new_name(self, group_name):
2940 2940 """
2941 2941 returns new full group name based on parent and new name
2942 2942
2943 2943 :param group_name:
2944 2944 """
2945 2945 path_prefix = (self.parent_group.full_path_splitted if
2946 2946 self.parent_group else [])
2947 2947 return RepoGroup.url_sep().join(path_prefix + [group_name])
2948 2948
2949 2949 def update_commit_cache(self, config=None):
2950 2950 """
2951 2951 Update cache of last commit for newest repository inside this repository group.
2952 2952 cache_keys should be::
2953 2953
2954 2954 source_repo_id
2955 2955 short_id
2956 2956 raw_id
2957 2957 revision
2958 2958 parents
2959 2959 message
2960 2960 date
2961 2961 author
2962 2962
2963 2963 """
2964 2964 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2965 2965 empty_date = datetime.datetime.fromtimestamp(0)
2966 2966
2967 2967 def repo_groups_and_repos(root_gr):
2968 2968 for _repo in root_gr.repositories:
2969 2969 yield _repo
2970 2970 for child_group in root_gr.children.all():
2971 2971 yield child_group
2972 2972
2973 2973 latest_repo_cs_cache = {}
2974 2974 for obj in repo_groups_and_repos(self):
2975 2975 repo_cs_cache = obj.changeset_cache
2976 2976 date_latest = latest_repo_cs_cache.get('date', empty_date)
2977 2977 date_current = repo_cs_cache.get('date', empty_date)
2978 2978 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2979 2979 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2980 2980 latest_repo_cs_cache = repo_cs_cache
2981 2981 if hasattr(obj, 'repo_id'):
2982 2982 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2983 2983 else:
2984 2984 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2985 2985
2986 2986 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2987 2987
2988 2988 latest_repo_cs_cache['updated_on'] = time.time()
2989 2989 self.changeset_cache = latest_repo_cs_cache
2990 2990 self.updated_on = _date_latest
2991 2991 Session().add(self)
2992 2992 Session().commit()
2993 2993
2994 2994 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2995 2995 self.group_name, latest_repo_cs_cache, _date_latest)
2996 2996
2997 2997 def permissions(self, with_admins=True, with_owner=True,
2998 2998 expand_from_user_groups=False):
2999 2999 """
3000 3000 Permissions for repository groups
3001 3001 """
3002 3002 _admin_perm = 'group.admin'
3003 3003
3004 3004 owner_row = []
3005 3005 if with_owner:
3006 3006 usr = AttributeDict(self.user.get_dict())
3007 3007 usr.owner_row = True
3008 3008 usr.permission = _admin_perm
3009 3009 owner_row.append(usr)
3010 3010
3011 3011 super_admin_ids = []
3012 3012 super_admin_rows = []
3013 3013 if with_admins:
3014 3014 for usr in User.get_all_super_admins():
3015 3015 super_admin_ids.append(usr.user_id)
3016 3016 # if this admin is also owner, don't double the record
3017 3017 if usr.user_id == owner_row[0].user_id:
3018 3018 owner_row[0].admin_row = True
3019 3019 else:
3020 3020 usr = AttributeDict(usr.get_dict())
3021 3021 usr.admin_row = True
3022 3022 usr.permission = _admin_perm
3023 3023 super_admin_rows.append(usr)
3024 3024
3025 3025 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3026 3026 q = q.options(joinedload(UserRepoGroupToPerm.group),
3027 3027 joinedload(UserRepoGroupToPerm.user),
3028 3028 joinedload(UserRepoGroupToPerm.permission),)
3029 3029
3030 3030 # get owners and admins and permissions. We do a trick of re-writing
3031 3031 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3032 3032 # has a global reference and changing one object propagates to all
3033 3033 # others. This means if admin is also an owner admin_row that change
3034 3034 # would propagate to both objects
3035 3035 perm_rows = []
3036 3036 for _usr in q.all():
3037 3037 usr = AttributeDict(_usr.user.get_dict())
3038 3038 # if this user is also owner/admin, mark as duplicate record
3039 3039 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3040 3040 usr.duplicate_perm = True
3041 3041 usr.permission = _usr.permission.permission_name
3042 3042 perm_rows.append(usr)
3043 3043
3044 3044 # filter the perm rows by 'default' first and then sort them by
3045 3045 # admin,write,read,none permissions sorted again alphabetically in
3046 3046 # each group
3047 3047 perm_rows = sorted(perm_rows, key=display_user_sort)
3048 3048
3049 3049 user_groups_rows = []
3050 3050 if expand_from_user_groups:
3051 3051 for ug in self.permission_user_groups(with_members=True):
3052 3052 for user_data in ug.members:
3053 3053 user_groups_rows.append(user_data)
3054 3054
3055 3055 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3056 3056
3057 3057 def permission_user_groups(self, with_members=False):
3058 3058 q = UserGroupRepoGroupToPerm.query()\
3059 3059 .filter(UserGroupRepoGroupToPerm.group == self)
3060 3060 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3061 3061 joinedload(UserGroupRepoGroupToPerm.users_group),
3062 3062 joinedload(UserGroupRepoGroupToPerm.permission),)
3063 3063
3064 3064 perm_rows = []
3065 3065 for _user_group in q.all():
3066 3066 entry = AttributeDict(_user_group.users_group.get_dict())
3067 3067 entry.permission = _user_group.permission.permission_name
3068 3068 if with_members:
3069 3069 entry.members = [x.user.get_dict()
3070 3070 for x in _user_group.users_group.members]
3071 3071 perm_rows.append(entry)
3072 3072
3073 3073 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3074 3074 return perm_rows
3075 3075
3076 3076 def get_api_data(self):
3077 3077 """
3078 3078 Common function for generating api data
3079 3079
3080 3080 """
3081 3081 group = self
3082 3082 data = {
3083 3083 'group_id': group.group_id,
3084 3084 'group_name': group.group_name,
3085 3085 'group_description': group.description_safe,
3086 3086 'parent_group': group.parent_group.group_name if group.parent_group else None,
3087 3087 'repositories': [x.repo_name for x in group.repositories],
3088 3088 'owner': group.user.username,
3089 3089 }
3090 3090 return data
3091 3091
3092 3092 def get_dict(self):
3093 3093 # Since we transformed `group_name` to a hybrid property, we need to
3094 3094 # keep compatibility with the code which uses `group_name` field.
3095 3095 result = super(RepoGroup, self).get_dict()
3096 3096 result['group_name'] = result.pop('_group_name', None)
3097 3097 return result
3098 3098
3099 3099
3100 3100 class Permission(Base, BaseModel):
3101 3101 __tablename__ = 'permissions'
3102 3102 __table_args__ = (
3103 3103 Index('p_perm_name_idx', 'permission_name'),
3104 3104 base_table_args,
3105 3105 )
3106 3106
3107 3107 PERMS = [
3108 3108 ('hg.admin', _('RhodeCode Super Administrator')),
3109 3109
3110 3110 ('repository.none', _('Repository no access')),
3111 3111 ('repository.read', _('Repository read access')),
3112 3112 ('repository.write', _('Repository write access')),
3113 3113 ('repository.admin', _('Repository admin access')),
3114 3114
3115 3115 ('group.none', _('Repository group no access')),
3116 3116 ('group.read', _('Repository group read access')),
3117 3117 ('group.write', _('Repository group write access')),
3118 3118 ('group.admin', _('Repository group admin access')),
3119 3119
3120 3120 ('usergroup.none', _('User group no access')),
3121 3121 ('usergroup.read', _('User group read access')),
3122 3122 ('usergroup.write', _('User group write access')),
3123 3123 ('usergroup.admin', _('User group admin access')),
3124 3124
3125 3125 ('branch.none', _('Branch no permissions')),
3126 3126 ('branch.merge', _('Branch access by web merge')),
3127 3127 ('branch.push', _('Branch access by push')),
3128 3128 ('branch.push_force', _('Branch access by push with force')),
3129 3129
3130 3130 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3131 3131 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3132 3132
3133 3133 ('hg.usergroup.create.false', _('User Group creation disabled')),
3134 3134 ('hg.usergroup.create.true', _('User Group creation enabled')),
3135 3135
3136 3136 ('hg.create.none', _('Repository creation disabled')),
3137 3137 ('hg.create.repository', _('Repository creation enabled')),
3138 3138 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3139 3139 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3140 3140
3141 3141 ('hg.fork.none', _('Repository forking disabled')),
3142 3142 ('hg.fork.repository', _('Repository forking enabled')),
3143 3143
3144 3144 ('hg.register.none', _('Registration disabled')),
3145 3145 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3146 3146 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3147 3147
3148 3148 ('hg.password_reset.enabled', _('Password reset enabled')),
3149 3149 ('hg.password_reset.hidden', _('Password reset hidden')),
3150 3150 ('hg.password_reset.disabled', _('Password reset disabled')),
3151 3151
3152 3152 ('hg.extern_activate.manual', _('Manual activation of external account')),
3153 3153 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3154 3154
3155 3155 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3156 3156 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3157 3157 ]
3158 3158
3159 3159 # definition of system default permissions for DEFAULT user, created on
3160 3160 # system setup
3161 3161 DEFAULT_USER_PERMISSIONS = [
3162 3162 # object perms
3163 3163 'repository.read',
3164 3164 'group.read',
3165 3165 'usergroup.read',
3166 3166 # branch, for backward compat we need same value as before so forced pushed
3167 3167 'branch.push_force',
3168 3168 # global
3169 3169 'hg.create.repository',
3170 3170 'hg.repogroup.create.false',
3171 3171 'hg.usergroup.create.false',
3172 3172 'hg.create.write_on_repogroup.true',
3173 3173 'hg.fork.repository',
3174 3174 'hg.register.manual_activate',
3175 3175 'hg.password_reset.enabled',
3176 3176 'hg.extern_activate.auto',
3177 3177 'hg.inherit_default_perms.true',
3178 3178 ]
3179 3179
3180 3180 # defines which permissions are more important higher the more important
3181 3181 # Weight defines which permissions are more important.
3182 3182 # The higher number the more important.
3183 3183 PERM_WEIGHTS = {
3184 3184 'repository.none': 0,
3185 3185 'repository.read': 1,
3186 3186 'repository.write': 3,
3187 3187 'repository.admin': 4,
3188 3188
3189 3189 'group.none': 0,
3190 3190 'group.read': 1,
3191 3191 'group.write': 3,
3192 3192 'group.admin': 4,
3193 3193
3194 3194 'usergroup.none': 0,
3195 3195 'usergroup.read': 1,
3196 3196 'usergroup.write': 3,
3197 3197 'usergroup.admin': 4,
3198 3198
3199 3199 'branch.none': 0,
3200 3200 'branch.merge': 1,
3201 3201 'branch.push': 3,
3202 3202 'branch.push_force': 4,
3203 3203
3204 3204 'hg.repogroup.create.false': 0,
3205 3205 'hg.repogroup.create.true': 1,
3206 3206
3207 3207 'hg.usergroup.create.false': 0,
3208 3208 'hg.usergroup.create.true': 1,
3209 3209
3210 3210 'hg.fork.none': 0,
3211 3211 'hg.fork.repository': 1,
3212 3212 'hg.create.none': 0,
3213 3213 'hg.create.repository': 1
3214 3214 }
3215 3215
3216 3216 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3217 3217 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3218 3218 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3219 3219
3220 3220 def __unicode__(self):
3221 3221 return u"<%s('%s:%s')>" % (
3222 3222 self.__class__.__name__, self.permission_id, self.permission_name
3223 3223 )
3224 3224
3225 3225 @classmethod
3226 3226 def get_by_key(cls, key):
3227 3227 return cls.query().filter(cls.permission_name == key).scalar()
3228 3228
3229 3229 @classmethod
3230 3230 def get_default_repo_perms(cls, user_id, repo_id=None):
3231 3231 q = Session().query(UserRepoToPerm, Repository, Permission)\
3232 3232 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3233 3233 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3234 3234 .filter(UserRepoToPerm.user_id == user_id)
3235 3235 if repo_id:
3236 3236 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3237 3237 return q.all()
3238 3238
3239 3239 @classmethod
3240 3240 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3241 3241 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3242 3242 .join(
3243 3243 Permission,
3244 3244 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3245 3245 .join(
3246 3246 UserRepoToPerm,
3247 3247 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3248 3248 .filter(UserRepoToPerm.user_id == user_id)
3249 3249
3250 3250 if repo_id:
3251 3251 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3252 3252 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3253 3253
3254 3254 @classmethod
3255 3255 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3256 3256 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3257 3257 .join(
3258 3258 Permission,
3259 3259 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3260 3260 .join(
3261 3261 Repository,
3262 3262 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3263 3263 .join(
3264 3264 UserGroup,
3265 3265 UserGroupRepoToPerm.users_group_id ==
3266 3266 UserGroup.users_group_id)\
3267 3267 .join(
3268 3268 UserGroupMember,
3269 3269 UserGroupRepoToPerm.users_group_id ==
3270 3270 UserGroupMember.users_group_id)\
3271 3271 .filter(
3272 3272 UserGroupMember.user_id == user_id,
3273 3273 UserGroup.users_group_active == true())
3274 3274 if repo_id:
3275 3275 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3276 3276 return q.all()
3277 3277
3278 3278 @classmethod
3279 3279 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3280 3280 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3281 3281 .join(
3282 3282 Permission,
3283 3283 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3284 3284 .join(
3285 3285 UserGroupRepoToPerm,
3286 3286 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3287 3287 .join(
3288 3288 UserGroup,
3289 3289 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3290 3290 .join(
3291 3291 UserGroupMember,
3292 3292 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3293 3293 .filter(
3294 3294 UserGroupMember.user_id == user_id,
3295 3295 UserGroup.users_group_active == true())
3296 3296
3297 3297 if repo_id:
3298 3298 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3299 3299 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3300 3300
3301 3301 @classmethod
3302 3302 def get_default_group_perms(cls, user_id, repo_group_id=None):
3303 3303 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3304 3304 .join(
3305 3305 Permission,
3306 3306 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3307 3307 .join(
3308 3308 RepoGroup,
3309 3309 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3310 3310 .filter(UserRepoGroupToPerm.user_id == user_id)
3311 3311 if repo_group_id:
3312 3312 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3313 3313 return q.all()
3314 3314
3315 3315 @classmethod
3316 3316 def get_default_group_perms_from_user_group(
3317 3317 cls, user_id, repo_group_id=None):
3318 3318 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3319 3319 .join(
3320 3320 Permission,
3321 3321 UserGroupRepoGroupToPerm.permission_id ==
3322 3322 Permission.permission_id)\
3323 3323 .join(
3324 3324 RepoGroup,
3325 3325 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3326 3326 .join(
3327 3327 UserGroup,
3328 3328 UserGroupRepoGroupToPerm.users_group_id ==
3329 3329 UserGroup.users_group_id)\
3330 3330 .join(
3331 3331 UserGroupMember,
3332 3332 UserGroupRepoGroupToPerm.users_group_id ==
3333 3333 UserGroupMember.users_group_id)\
3334 3334 .filter(
3335 3335 UserGroupMember.user_id == user_id,
3336 3336 UserGroup.users_group_active == true())
3337 3337 if repo_group_id:
3338 3338 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3339 3339 return q.all()
3340 3340
3341 3341 @classmethod
3342 3342 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3343 3343 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3344 3344 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3345 3345 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3346 3346 .filter(UserUserGroupToPerm.user_id == user_id)
3347 3347 if user_group_id:
3348 3348 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3349 3349 return q.all()
3350 3350
3351 3351 @classmethod
3352 3352 def get_default_user_group_perms_from_user_group(
3353 3353 cls, user_id, user_group_id=None):
3354 3354 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3355 3355 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3356 3356 .join(
3357 3357 Permission,
3358 3358 UserGroupUserGroupToPerm.permission_id ==
3359 3359 Permission.permission_id)\
3360 3360 .join(
3361 3361 TargetUserGroup,
3362 3362 UserGroupUserGroupToPerm.target_user_group_id ==
3363 3363 TargetUserGroup.users_group_id)\
3364 3364 .join(
3365 3365 UserGroup,
3366 3366 UserGroupUserGroupToPerm.user_group_id ==
3367 3367 UserGroup.users_group_id)\
3368 3368 .join(
3369 3369 UserGroupMember,
3370 3370 UserGroupUserGroupToPerm.user_group_id ==
3371 3371 UserGroupMember.users_group_id)\
3372 3372 .filter(
3373 3373 UserGroupMember.user_id == user_id,
3374 3374 UserGroup.users_group_active == true())
3375 3375 if user_group_id:
3376 3376 q = q.filter(
3377 3377 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3378 3378
3379 3379 return q.all()
3380 3380
3381 3381
3382 3382 class UserRepoToPerm(Base, BaseModel):
3383 3383 __tablename__ = 'repo_to_perm'
3384 3384 __table_args__ = (
3385 3385 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3386 3386 base_table_args
3387 3387 )
3388 3388
3389 3389 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3390 3390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3391 3391 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3392 3392 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3393 3393
3394 3394 user = relationship('User')
3395 3395 repository = relationship('Repository')
3396 3396 permission = relationship('Permission')
3397 3397
3398 3398 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3399 3399
3400 3400 @classmethod
3401 3401 def create(cls, user, repository, permission):
3402 3402 n = cls()
3403 3403 n.user = user
3404 3404 n.repository = repository
3405 3405 n.permission = permission
3406 3406 Session().add(n)
3407 3407 return n
3408 3408
3409 3409 def __unicode__(self):
3410 3410 return u'<%s => %s >' % (self.user, self.repository)
3411 3411
3412 3412
3413 3413 class UserUserGroupToPerm(Base, BaseModel):
3414 3414 __tablename__ = 'user_user_group_to_perm'
3415 3415 __table_args__ = (
3416 3416 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3417 3417 base_table_args
3418 3418 )
3419 3419
3420 3420 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3421 3421 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3422 3422 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3423 3423 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3424 3424
3425 3425 user = relationship('User')
3426 3426 user_group = relationship('UserGroup')
3427 3427 permission = relationship('Permission')
3428 3428
3429 3429 @classmethod
3430 3430 def create(cls, user, user_group, permission):
3431 3431 n = cls()
3432 3432 n.user = user
3433 3433 n.user_group = user_group
3434 3434 n.permission = permission
3435 3435 Session().add(n)
3436 3436 return n
3437 3437
3438 3438 def __unicode__(self):
3439 3439 return u'<%s => %s >' % (self.user, self.user_group)
3440 3440
3441 3441
3442 3442 class UserToPerm(Base, BaseModel):
3443 3443 __tablename__ = 'user_to_perm'
3444 3444 __table_args__ = (
3445 3445 UniqueConstraint('user_id', 'permission_id'),
3446 3446 base_table_args
3447 3447 )
3448 3448
3449 3449 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3450 3450 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3451 3451 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3452 3452
3453 3453 user = relationship('User')
3454 3454 permission = relationship('Permission', lazy='joined')
3455 3455
3456 3456 def __unicode__(self):
3457 3457 return u'<%s => %s >' % (self.user, self.permission)
3458 3458
3459 3459
3460 3460 class UserGroupRepoToPerm(Base, BaseModel):
3461 3461 __tablename__ = 'users_group_repo_to_perm'
3462 3462 __table_args__ = (
3463 3463 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3464 3464 base_table_args
3465 3465 )
3466 3466
3467 3467 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3468 3468 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3469 3469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3470 3470 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3471 3471
3472 3472 users_group = relationship('UserGroup')
3473 3473 permission = relationship('Permission')
3474 3474 repository = relationship('Repository')
3475 3475 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3476 3476
3477 3477 @classmethod
3478 3478 def create(cls, users_group, repository, permission):
3479 3479 n = cls()
3480 3480 n.users_group = users_group
3481 3481 n.repository = repository
3482 3482 n.permission = permission
3483 3483 Session().add(n)
3484 3484 return n
3485 3485
3486 3486 def __unicode__(self):
3487 3487 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3488 3488
3489 3489
3490 3490 class UserGroupUserGroupToPerm(Base, BaseModel):
3491 3491 __tablename__ = 'user_group_user_group_to_perm'
3492 3492 __table_args__ = (
3493 3493 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3494 3494 CheckConstraint('target_user_group_id != user_group_id'),
3495 3495 base_table_args
3496 3496 )
3497 3497
3498 3498 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)
3499 3499 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3500 3500 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3501 3501 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3502 3502
3503 3503 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3504 3504 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3505 3505 permission = relationship('Permission')
3506 3506
3507 3507 @classmethod
3508 3508 def create(cls, target_user_group, user_group, permission):
3509 3509 n = cls()
3510 3510 n.target_user_group = target_user_group
3511 3511 n.user_group = user_group
3512 3512 n.permission = permission
3513 3513 Session().add(n)
3514 3514 return n
3515 3515
3516 3516 def __unicode__(self):
3517 3517 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3518 3518
3519 3519
3520 3520 class UserGroupToPerm(Base, BaseModel):
3521 3521 __tablename__ = 'users_group_to_perm'
3522 3522 __table_args__ = (
3523 3523 UniqueConstraint('users_group_id', 'permission_id',),
3524 3524 base_table_args
3525 3525 )
3526 3526
3527 3527 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3528 3528 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3529 3529 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3530 3530
3531 3531 users_group = relationship('UserGroup')
3532 3532 permission = relationship('Permission')
3533 3533
3534 3534
3535 3535 class UserRepoGroupToPerm(Base, BaseModel):
3536 3536 __tablename__ = 'user_repo_group_to_perm'
3537 3537 __table_args__ = (
3538 3538 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3539 3539 base_table_args
3540 3540 )
3541 3541
3542 3542 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3543 3543 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3544 3544 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3545 3545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3546 3546
3547 3547 user = relationship('User')
3548 3548 group = relationship('RepoGroup')
3549 3549 permission = relationship('Permission')
3550 3550
3551 3551 @classmethod
3552 3552 def create(cls, user, repository_group, permission):
3553 3553 n = cls()
3554 3554 n.user = user
3555 3555 n.group = repository_group
3556 3556 n.permission = permission
3557 3557 Session().add(n)
3558 3558 return n
3559 3559
3560 3560
3561 3561 class UserGroupRepoGroupToPerm(Base, BaseModel):
3562 3562 __tablename__ = 'users_group_repo_group_to_perm'
3563 3563 __table_args__ = (
3564 3564 UniqueConstraint('users_group_id', 'group_id'),
3565 3565 base_table_args
3566 3566 )
3567 3567
3568 3568 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)
3569 3569 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3570 3570 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3571 3571 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3572 3572
3573 3573 users_group = relationship('UserGroup')
3574 3574 permission = relationship('Permission')
3575 3575 group = relationship('RepoGroup')
3576 3576
3577 3577 @classmethod
3578 3578 def create(cls, user_group, repository_group, permission):
3579 3579 n = cls()
3580 3580 n.users_group = user_group
3581 3581 n.group = repository_group
3582 3582 n.permission = permission
3583 3583 Session().add(n)
3584 3584 return n
3585 3585
3586 3586 def __unicode__(self):
3587 3587 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3588 3588
3589 3589
3590 3590 class Statistics(Base, BaseModel):
3591 3591 __tablename__ = 'statistics'
3592 3592 __table_args__ = (
3593 3593 base_table_args
3594 3594 )
3595 3595
3596 3596 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3597 3597 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3598 3598 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3599 3599 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3600 3600 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3601 3601 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3602 3602
3603 3603 repository = relationship('Repository', single_parent=True)
3604 3604
3605 3605
3606 3606 class UserFollowing(Base, BaseModel):
3607 3607 __tablename__ = 'user_followings'
3608 3608 __table_args__ = (
3609 3609 UniqueConstraint('user_id', 'follows_repository_id'),
3610 3610 UniqueConstraint('user_id', 'follows_user_id'),
3611 3611 base_table_args
3612 3612 )
3613 3613
3614 3614 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3615 3615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3616 3616 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3617 3617 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3618 3618 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3619 3619
3620 3620 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3621 3621
3622 3622 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3623 3623 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3624 3624
3625 3625 @classmethod
3626 3626 def get_repo_followers(cls, repo_id):
3627 3627 return cls.query().filter(cls.follows_repo_id == repo_id)
3628 3628
3629 3629
3630 3630 class CacheKey(Base, BaseModel):
3631 3631 __tablename__ = 'cache_invalidation'
3632 3632 __table_args__ = (
3633 3633 UniqueConstraint('cache_key'),
3634 3634 Index('key_idx', 'cache_key'),
3635 3635 base_table_args,
3636 3636 )
3637 3637
3638 3638 CACHE_TYPE_FEED = 'FEED'
3639 3639
3640 3640 # namespaces used to register process/thread aware caches
3641 3641 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3642 3642 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3643 3643
3644 3644 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3645 3645 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3646 3646 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3647 3647 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3648 3648 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3649 3649
3650 3650 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3651 3651 self.cache_key = cache_key
3652 3652 self.cache_args = cache_args
3653 3653 self.cache_active = False
3654 3654 # first key should be same for all entries, since all workers should share it
3655 3655 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3656 3656
3657 3657 def __unicode__(self):
3658 3658 return u"<%s('%s:%s[%s]')>" % (
3659 3659 self.__class__.__name__,
3660 3660 self.cache_id, self.cache_key, self.cache_active)
3661 3661
3662 3662 def _cache_key_partition(self):
3663 3663 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3664 3664 return prefix, repo_name, suffix
3665 3665
3666 3666 def get_prefix(self):
3667 3667 """
3668 3668 Try to extract prefix from existing cache key. The key could consist
3669 3669 of prefix, repo_name, suffix
3670 3670 """
3671 3671 # this returns prefix, repo_name, suffix
3672 3672 return self._cache_key_partition()[0]
3673 3673
3674 3674 def get_suffix(self):
3675 3675 """
3676 3676 get suffix that might have been used in _get_cache_key to
3677 3677 generate self.cache_key. Only used for informational purposes
3678 3678 in repo_edit.mako.
3679 3679 """
3680 3680 # prefix, repo_name, suffix
3681 3681 return self._cache_key_partition()[2]
3682 3682
3683 3683 @classmethod
3684 3684 def generate_new_state_uid(cls, based_on=None):
3685 3685 if based_on:
3686 3686 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3687 3687 else:
3688 3688 return str(uuid.uuid4())
3689 3689
3690 3690 @classmethod
3691 3691 def delete_all_cache(cls):
3692 3692 """
3693 3693 Delete all cache keys from database.
3694 3694 Should only be run when all instances are down and all entries
3695 3695 thus stale.
3696 3696 """
3697 3697 cls.query().delete()
3698 3698 Session().commit()
3699 3699
3700 3700 @classmethod
3701 3701 def set_invalidate(cls, cache_uid, delete=False):
3702 3702 """
3703 3703 Mark all caches of a repo as invalid in the database.
3704 3704 """
3705 3705
3706 3706 try:
3707 3707 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3708 3708 if delete:
3709 3709 qry.delete()
3710 3710 log.debug('cache objects deleted for cache args %s',
3711 3711 safe_str(cache_uid))
3712 3712 else:
3713 3713 qry.update({"cache_active": False,
3714 3714 "cache_state_uid": cls.generate_new_state_uid()})
3715 3715 log.debug('cache objects marked as invalid for cache args %s',
3716 3716 safe_str(cache_uid))
3717 3717
3718 3718 Session().commit()
3719 3719 except Exception:
3720 3720 log.exception(
3721 3721 'Cache key invalidation failed for cache args %s',
3722 3722 safe_str(cache_uid))
3723 3723 Session().rollback()
3724 3724
3725 3725 @classmethod
3726 3726 def get_active_cache(cls, cache_key):
3727 3727 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3728 3728 if inv_obj:
3729 3729 return inv_obj
3730 3730 return None
3731 3731
3732 3732 @classmethod
3733 3733 def get_namespace_map(cls, namespace):
3734 3734 return {
3735 3735 x.cache_key: x
3736 3736 for x in cls.query().filter(cls.cache_args == namespace)}
3737 3737
3738 3738
3739 3739 class ChangesetComment(Base, BaseModel):
3740 3740 __tablename__ = 'changeset_comments'
3741 3741 __table_args__ = (
3742 3742 Index('cc_revision_idx', 'revision'),
3743 3743 base_table_args,
3744 3744 )
3745 3745
3746 3746 COMMENT_OUTDATED = u'comment_outdated'
3747 3747 COMMENT_TYPE_NOTE = u'note'
3748 3748 COMMENT_TYPE_TODO = u'todo'
3749 3749 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3750 3750
3751 3751 OP_IMMUTABLE = u'immutable'
3752 3752 OP_CHANGEABLE = u'changeable'
3753 3753
3754 3754 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3755 3755 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3756 3756 revision = Column('revision', String(40), nullable=True)
3757 3757 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3758 3758 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3759 3759 line_no = Column('line_no', Unicode(10), nullable=True)
3760 3760 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3761 3761 f_path = Column('f_path', Unicode(1000), nullable=True)
3762 3762 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3763 3763 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3764 3764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3765 3765 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3766 3766 renderer = Column('renderer', Unicode(64), nullable=True)
3767 3767 display_state = Column('display_state', Unicode(128), nullable=True)
3768 3768 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3769 3769
3770 3770 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3771 3771 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3772 3772
3773 3773 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3774 3774 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3775 3775
3776 3776 author = relationship('User', lazy='joined')
3777 3777 repo = relationship('Repository')
3778 3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3779 3779 pull_request = relationship('PullRequest', lazy='joined')
3780 3780 pull_request_version = relationship('PullRequestVersion')
3781 3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3782 3782
3783 3783 @classmethod
3784 3784 def get_users(cls, revision=None, pull_request_id=None):
3785 3785 """
3786 3786 Returns user associated with this ChangesetComment. ie those
3787 3787 who actually commented
3788 3788
3789 3789 :param cls:
3790 3790 :param revision:
3791 3791 """
3792 3792 q = Session().query(User)\
3793 3793 .join(ChangesetComment.author)
3794 3794 if revision:
3795 3795 q = q.filter(cls.revision == revision)
3796 3796 elif pull_request_id:
3797 3797 q = q.filter(cls.pull_request_id == pull_request_id)
3798 3798 return q.all()
3799 3799
3800 3800 @classmethod
3801 3801 def get_index_from_version(cls, pr_version, versions):
3802 3802 num_versions = [x.pull_request_version_id for x in versions]
3803 3803 try:
3804 3804 return num_versions.index(pr_version) + 1
3805 3805 except (IndexError, ValueError):
3806 3806 return
3807 3807
3808 3808 @property
3809 3809 def outdated(self):
3810 3810 return self.display_state == self.COMMENT_OUTDATED
3811 3811
3812 3812 @property
3813 3813 def immutable(self):
3814 3814 return self.immutable_state == self.OP_IMMUTABLE
3815 3815
3816 3816 def outdated_at_version(self, version):
3817 3817 """
3818 3818 Checks if comment is outdated for given pull request version
3819 3819 """
3820 3820 return self.outdated and self.pull_request_version_id != version
3821 3821
3822 3822 def older_than_version(self, version):
3823 3823 """
3824 3824 Checks if comment is made from previous version than given
3825 3825 """
3826 3826 if version is None:
3827 3827 return self.pull_request_version_id is not None
3828 3828
3829 3829 return self.pull_request_version_id < version
3830 3830
3831 3831 @property
3832 3832 def commit_id(self):
3833 3833 """New style naming to stop using .revision"""
3834 3834 return self.revision
3835 3835
3836 3836 @property
3837 3837 def resolved(self):
3838 3838 return self.resolved_by[0] if self.resolved_by else None
3839 3839
3840 3840 @property
3841 3841 def is_todo(self):
3842 3842 return self.comment_type == self.COMMENT_TYPE_TODO
3843 3843
3844 3844 @property
3845 3845 def is_inline(self):
3846 3846 return self.line_no and self.f_path
3847 3847
3848 @property
3849 def last_version(self):
3850 version = 0
3851 if self.history:
3852 version = self.history[-1].version
3853 return version
3854
3848 3855 def get_index_version(self, versions):
3849 3856 return self.get_index_from_version(
3850 3857 self.pull_request_version_id, versions)
3851 3858
3852 3859 def __repr__(self):
3853 3860 if self.comment_id:
3854 3861 return '<DB:Comment #%s>' % self.comment_id
3855 3862 else:
3856 3863 return '<DB:Comment at %#x>' % id(self)
3857 3864
3858 3865 def get_api_data(self):
3859 3866 comment = self
3867
3860 3868 data = {
3861 3869 'comment_id': comment.comment_id,
3862 3870 'comment_type': comment.comment_type,
3863 3871 'comment_text': comment.text,
3864 3872 'comment_status': comment.status_change,
3865 3873 'comment_f_path': comment.f_path,
3866 3874 'comment_lineno': comment.line_no,
3867 3875 'comment_author': comment.author,
3868 3876 'comment_created_on': comment.created_on,
3869 3877 'comment_resolved_by': self.resolved,
3870 3878 'comment_commit_id': comment.revision,
3871 3879 'comment_pull_request_id': comment.pull_request_id,
3880 'comment_last_version': self.last_version
3872 3881 }
3873 3882 return data
3874 3883
3875 3884 def __json__(self):
3876 3885 data = dict()
3877 3886 data.update(self.get_api_data())
3878 3887 return data
3879 3888
3880 3889
3881 3890 class ChangesetCommentHistory(Base, BaseModel):
3882 3891 __tablename__ = 'changeset_comments_history'
3883 3892 __table_args__ = (
3884 3893 Index('cch_comment_id_idx', 'comment_id'),
3885 3894 base_table_args,
3886 3895 )
3887 3896
3888 3897 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3889 3898 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3890 3899 version = Column("version", Integer(), nullable=False, default=0)
3891 3900 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3892 3901 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3893 3902 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3894 3903 deleted = Column('deleted', Boolean(), default=False)
3895 3904
3896 3905 author = relationship('User', lazy='joined')
3897 3906 comment = relationship('ChangesetComment', cascade="all, delete")
3898 3907
3899 3908 @classmethod
3900 3909 def get_version(cls, comment_id):
3901 3910 q = Session().query(ChangesetCommentHistory).filter(
3902 3911 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3903 3912 if q.count() == 0:
3904 3913 return 1
3905 3914 elif q.count() >= q[0].version:
3906 3915 return q.count() + 1
3907 3916 else:
3908 3917 return q[0].version + 1
3909 3918
3910 3919
3911 3920 class ChangesetStatus(Base, BaseModel):
3912 3921 __tablename__ = 'changeset_statuses'
3913 3922 __table_args__ = (
3914 3923 Index('cs_revision_idx', 'revision'),
3915 3924 Index('cs_version_idx', 'version'),
3916 3925 UniqueConstraint('repo_id', 'revision', 'version'),
3917 3926 base_table_args
3918 3927 )
3919 3928
3920 3929 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3921 3930 STATUS_APPROVED = 'approved'
3922 3931 STATUS_REJECTED = 'rejected'
3923 3932 STATUS_UNDER_REVIEW = 'under_review'
3924 3933
3925 3934 STATUSES = [
3926 3935 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3927 3936 (STATUS_APPROVED, _("Approved")),
3928 3937 (STATUS_REJECTED, _("Rejected")),
3929 3938 (STATUS_UNDER_REVIEW, _("Under Review")),
3930 3939 ]
3931 3940
3932 3941 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3933 3942 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3934 3943 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3935 3944 revision = Column('revision', String(40), nullable=False)
3936 3945 status = Column('status', String(128), nullable=False, default=DEFAULT)
3937 3946 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3938 3947 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3939 3948 version = Column('version', Integer(), nullable=False, default=0)
3940 3949 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3941 3950
3942 3951 author = relationship('User', lazy='joined')
3943 3952 repo = relationship('Repository')
3944 3953 comment = relationship('ChangesetComment', lazy='joined')
3945 3954 pull_request = relationship('PullRequest', lazy='joined')
3946 3955
3947 3956 def __unicode__(self):
3948 3957 return u"<%s('%s[v%s]:%s')>" % (
3949 3958 self.__class__.__name__,
3950 3959 self.status, self.version, self.author
3951 3960 )
3952 3961
3953 3962 @classmethod
3954 3963 def get_status_lbl(cls, value):
3955 3964 return dict(cls.STATUSES).get(value)
3956 3965
3957 3966 @property
3958 3967 def status_lbl(self):
3959 3968 return ChangesetStatus.get_status_lbl(self.status)
3960 3969
3961 3970 def get_api_data(self):
3962 3971 status = self
3963 3972 data = {
3964 3973 'status_id': status.changeset_status_id,
3965 3974 'status': status.status,
3966 3975 }
3967 3976 return data
3968 3977
3969 3978 def __json__(self):
3970 3979 data = dict()
3971 3980 data.update(self.get_api_data())
3972 3981 return data
3973 3982
3974 3983
3975 3984 class _SetState(object):
3976 3985 """
3977 3986 Context processor allowing changing state for sensitive operation such as
3978 3987 pull request update or merge
3979 3988 """
3980 3989
3981 3990 def __init__(self, pull_request, pr_state, back_state=None):
3982 3991 self._pr = pull_request
3983 3992 self._org_state = back_state or pull_request.pull_request_state
3984 3993 self._pr_state = pr_state
3985 3994 self._current_state = None
3986 3995
3987 3996 def __enter__(self):
3988 3997 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
3989 3998 self._pr, self._pr_state)
3990 3999 self.set_pr_state(self._pr_state)
3991 4000 return self
3992 4001
3993 4002 def __exit__(self, exc_type, exc_val, exc_tb):
3994 4003 if exc_val is not None:
3995 4004 log.error(traceback.format_exc(exc_tb))
3996 4005 return None
3997 4006
3998 4007 self.set_pr_state(self._org_state)
3999 4008 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4000 4009 self._pr, self._org_state)
4001 4010
4002 4011 @property
4003 4012 def state(self):
4004 4013 return self._current_state
4005 4014
4006 4015 def set_pr_state(self, pr_state):
4007 4016 try:
4008 4017 self._pr.pull_request_state = pr_state
4009 4018 Session().add(self._pr)
4010 4019 Session().commit()
4011 4020 self._current_state = pr_state
4012 4021 except Exception:
4013 4022 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4014 4023 raise
4015 4024
4016 4025
4017 4026 class _PullRequestBase(BaseModel):
4018 4027 """
4019 4028 Common attributes of pull request and version entries.
4020 4029 """
4021 4030
4022 4031 # .status values
4023 4032 STATUS_NEW = u'new'
4024 4033 STATUS_OPEN = u'open'
4025 4034 STATUS_CLOSED = u'closed'
4026 4035
4027 4036 # available states
4028 4037 STATE_CREATING = u'creating'
4029 4038 STATE_UPDATING = u'updating'
4030 4039 STATE_MERGING = u'merging'
4031 4040 STATE_CREATED = u'created'
4032 4041
4033 4042 title = Column('title', Unicode(255), nullable=True)
4034 4043 description = Column(
4035 4044 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4036 4045 nullable=True)
4037 4046 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4038 4047
4039 4048 # new/open/closed status of pull request (not approve/reject/etc)
4040 4049 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4041 4050 created_on = Column(
4042 4051 'created_on', DateTime(timezone=False), nullable=False,
4043 4052 default=datetime.datetime.now)
4044 4053 updated_on = Column(
4045 4054 'updated_on', DateTime(timezone=False), nullable=False,
4046 4055 default=datetime.datetime.now)
4047 4056
4048 4057 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4049 4058
4050 4059 @declared_attr
4051 4060 def user_id(cls):
4052 4061 return Column(
4053 4062 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4054 4063 unique=None)
4055 4064
4056 4065 # 500 revisions max
4057 4066 _revisions = Column(
4058 4067 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4059 4068
4060 4069 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4061 4070
4062 4071 @declared_attr
4063 4072 def source_repo_id(cls):
4064 4073 # TODO: dan: rename column to source_repo_id
4065 4074 return Column(
4066 4075 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4067 4076 nullable=False)
4068 4077
4069 4078 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4070 4079
4071 4080 @hybrid_property
4072 4081 def source_ref(self):
4073 4082 return self._source_ref
4074 4083
4075 4084 @source_ref.setter
4076 4085 def source_ref(self, val):
4077 4086 parts = (val or '').split(':')
4078 4087 if len(parts) != 3:
4079 4088 raise ValueError(
4080 4089 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4081 4090 self._source_ref = safe_unicode(val)
4082 4091
4083 4092 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4084 4093
4085 4094 @hybrid_property
4086 4095 def target_ref(self):
4087 4096 return self._target_ref
4088 4097
4089 4098 @target_ref.setter
4090 4099 def target_ref(self, val):
4091 4100 parts = (val or '').split(':')
4092 4101 if len(parts) != 3:
4093 4102 raise ValueError(
4094 4103 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4095 4104 self._target_ref = safe_unicode(val)
4096 4105
4097 4106 @declared_attr
4098 4107 def target_repo_id(cls):
4099 4108 # TODO: dan: rename column to target_repo_id
4100 4109 return Column(
4101 4110 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4102 4111 nullable=False)
4103 4112
4104 4113 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4105 4114
4106 4115 # TODO: dan: rename column to last_merge_source_rev
4107 4116 _last_merge_source_rev = Column(
4108 4117 'last_merge_org_rev', String(40), nullable=True)
4109 4118 # TODO: dan: rename column to last_merge_target_rev
4110 4119 _last_merge_target_rev = Column(
4111 4120 'last_merge_other_rev', String(40), nullable=True)
4112 4121 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4113 4122 last_merge_metadata = Column(
4114 4123 'last_merge_metadata', MutationObj.as_mutable(
4115 4124 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4116 4125
4117 4126 merge_rev = Column('merge_rev', String(40), nullable=True)
4118 4127
4119 4128 reviewer_data = Column(
4120 4129 'reviewer_data_json', MutationObj.as_mutable(
4121 4130 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4122 4131
4123 4132 @property
4124 4133 def reviewer_data_json(self):
4125 4134 return json.dumps(self.reviewer_data)
4126 4135
4127 4136 @property
4128 4137 def work_in_progress(self):
4129 4138 """checks if pull request is work in progress by checking the title"""
4130 4139 title = self.title.upper()
4131 4140 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4132 4141 return True
4133 4142 return False
4134 4143
4135 4144 @hybrid_property
4136 4145 def description_safe(self):
4137 4146 from rhodecode.lib import helpers as h
4138 4147 return h.escape(self.description)
4139 4148
4140 4149 @hybrid_property
4141 4150 def revisions(self):
4142 4151 return self._revisions.split(':') if self._revisions else []
4143 4152
4144 4153 @revisions.setter
4145 4154 def revisions(self, val):
4146 4155 self._revisions = u':'.join(val)
4147 4156
4148 4157 @hybrid_property
4149 4158 def last_merge_status(self):
4150 4159 return safe_int(self._last_merge_status)
4151 4160
4152 4161 @last_merge_status.setter
4153 4162 def last_merge_status(self, val):
4154 4163 self._last_merge_status = val
4155 4164
4156 4165 @declared_attr
4157 4166 def author(cls):
4158 4167 return relationship('User', lazy='joined')
4159 4168
4160 4169 @declared_attr
4161 4170 def source_repo(cls):
4162 4171 return relationship(
4163 4172 'Repository',
4164 4173 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4165 4174
4166 4175 @property
4167 4176 def source_ref_parts(self):
4168 4177 return self.unicode_to_reference(self.source_ref)
4169 4178
4170 4179 @declared_attr
4171 4180 def target_repo(cls):
4172 4181 return relationship(
4173 4182 'Repository',
4174 4183 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4175 4184
4176 4185 @property
4177 4186 def target_ref_parts(self):
4178 4187 return self.unicode_to_reference(self.target_ref)
4179 4188
4180 4189 @property
4181 4190 def shadow_merge_ref(self):
4182 4191 return self.unicode_to_reference(self._shadow_merge_ref)
4183 4192
4184 4193 @shadow_merge_ref.setter
4185 4194 def shadow_merge_ref(self, ref):
4186 4195 self._shadow_merge_ref = self.reference_to_unicode(ref)
4187 4196
4188 4197 @staticmethod
4189 4198 def unicode_to_reference(raw):
4190 4199 """
4191 4200 Convert a unicode (or string) to a reference object.
4192 4201 If unicode evaluates to False it returns None.
4193 4202 """
4194 4203 if raw:
4195 4204 refs = raw.split(':')
4196 4205 return Reference(*refs)
4197 4206 else:
4198 4207 return None
4199 4208
4200 4209 @staticmethod
4201 4210 def reference_to_unicode(ref):
4202 4211 """
4203 4212 Convert a reference object to unicode.
4204 4213 If reference is None it returns None.
4205 4214 """
4206 4215 if ref:
4207 4216 return u':'.join(ref)
4208 4217 else:
4209 4218 return None
4210 4219
4211 4220 def get_api_data(self, with_merge_state=True):
4212 4221 from rhodecode.model.pull_request import PullRequestModel
4213 4222
4214 4223 pull_request = self
4215 4224 if with_merge_state:
4216 4225 merge_response, merge_status, msg = \
4217 4226 PullRequestModel().merge_status(pull_request)
4218 4227 merge_state = {
4219 4228 'status': merge_status,
4220 4229 'message': safe_unicode(msg),
4221 4230 }
4222 4231 else:
4223 4232 merge_state = {'status': 'not_available',
4224 4233 'message': 'not_available'}
4225 4234
4226 4235 merge_data = {
4227 4236 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4228 4237 'reference': (
4229 4238 pull_request.shadow_merge_ref._asdict()
4230 4239 if pull_request.shadow_merge_ref else None),
4231 4240 }
4232 4241
4233 4242 data = {
4234 4243 'pull_request_id': pull_request.pull_request_id,
4235 4244 'url': PullRequestModel().get_url(pull_request),
4236 4245 'title': pull_request.title,
4237 4246 'description': pull_request.description,
4238 4247 'status': pull_request.status,
4239 4248 'state': pull_request.pull_request_state,
4240 4249 'created_on': pull_request.created_on,
4241 4250 'updated_on': pull_request.updated_on,
4242 4251 'commit_ids': pull_request.revisions,
4243 4252 'review_status': pull_request.calculated_review_status(),
4244 4253 'mergeable': merge_state,
4245 4254 'source': {
4246 4255 'clone_url': pull_request.source_repo.clone_url(),
4247 4256 'repository': pull_request.source_repo.repo_name,
4248 4257 'reference': {
4249 4258 'name': pull_request.source_ref_parts.name,
4250 4259 'type': pull_request.source_ref_parts.type,
4251 4260 'commit_id': pull_request.source_ref_parts.commit_id,
4252 4261 },
4253 4262 },
4254 4263 'target': {
4255 4264 'clone_url': pull_request.target_repo.clone_url(),
4256 4265 'repository': pull_request.target_repo.repo_name,
4257 4266 'reference': {
4258 4267 'name': pull_request.target_ref_parts.name,
4259 4268 'type': pull_request.target_ref_parts.type,
4260 4269 'commit_id': pull_request.target_ref_parts.commit_id,
4261 4270 },
4262 4271 },
4263 4272 'merge': merge_data,
4264 4273 'author': pull_request.author.get_api_data(include_secrets=False,
4265 4274 details='basic'),
4266 4275 'reviewers': [
4267 4276 {
4268 4277 'user': reviewer.get_api_data(include_secrets=False,
4269 4278 details='basic'),
4270 4279 'reasons': reasons,
4271 4280 'review_status': st[0][1].status if st else 'not_reviewed',
4272 4281 }
4273 4282 for obj, reviewer, reasons, mandatory, st in
4274 4283 pull_request.reviewers_statuses()
4275 4284 ]
4276 4285 }
4277 4286
4278 4287 return data
4279 4288
4280 4289 def set_state(self, pull_request_state, final_state=None):
4281 4290 """
4282 4291 # goes from initial state to updating to initial state.
4283 4292 # initial state can be changed by specifying back_state=
4284 4293 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4285 4294 pull_request.merge()
4286 4295
4287 4296 :param pull_request_state:
4288 4297 :param final_state:
4289 4298
4290 4299 """
4291 4300
4292 4301 return _SetState(self, pull_request_state, back_state=final_state)
4293 4302
4294 4303
4295 4304 class PullRequest(Base, _PullRequestBase):
4296 4305 __tablename__ = 'pull_requests'
4297 4306 __table_args__ = (
4298 4307 base_table_args,
4299 4308 )
4300 4309
4301 4310 pull_request_id = Column(
4302 4311 'pull_request_id', Integer(), nullable=False, primary_key=True)
4303 4312
4304 4313 def __repr__(self):
4305 4314 if self.pull_request_id:
4306 4315 return '<DB:PullRequest #%s>' % self.pull_request_id
4307 4316 else:
4308 4317 return '<DB:PullRequest at %#x>' % id(self)
4309 4318
4310 4319 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4311 4320 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4312 4321 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4313 4322 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4314 4323 lazy='dynamic')
4315 4324
4316 4325 @classmethod
4317 4326 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4318 4327 internal_methods=None):
4319 4328
4320 4329 class PullRequestDisplay(object):
4321 4330 """
4322 4331 Special object wrapper for showing PullRequest data via Versions
4323 4332 It mimics PR object as close as possible. This is read only object
4324 4333 just for display
4325 4334 """
4326 4335
4327 4336 def __init__(self, attrs, internal=None):
4328 4337 self.attrs = attrs
4329 4338 # internal have priority over the given ones via attrs
4330 4339 self.internal = internal or ['versions']
4331 4340
4332 4341 def __getattr__(self, item):
4333 4342 if item in self.internal:
4334 4343 return getattr(self, item)
4335 4344 try:
4336 4345 return self.attrs[item]
4337 4346 except KeyError:
4338 4347 raise AttributeError(
4339 4348 '%s object has no attribute %s' % (self, item))
4340 4349
4341 4350 def __repr__(self):
4342 4351 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4343 4352
4344 4353 def versions(self):
4345 4354 return pull_request_obj.versions.order_by(
4346 4355 PullRequestVersion.pull_request_version_id).all()
4347 4356
4348 4357 def is_closed(self):
4349 4358 return pull_request_obj.is_closed()
4350 4359
4351 4360 def is_state_changing(self):
4352 4361 return pull_request_obj.is_state_changing()
4353 4362
4354 4363 @property
4355 4364 def pull_request_version_id(self):
4356 4365 return getattr(pull_request_obj, 'pull_request_version_id', None)
4357 4366
4358 4367 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4359 4368
4360 4369 attrs.author = StrictAttributeDict(
4361 4370 pull_request_obj.author.get_api_data())
4362 4371 if pull_request_obj.target_repo:
4363 4372 attrs.target_repo = StrictAttributeDict(
4364 4373 pull_request_obj.target_repo.get_api_data())
4365 4374 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4366 4375
4367 4376 if pull_request_obj.source_repo:
4368 4377 attrs.source_repo = StrictAttributeDict(
4369 4378 pull_request_obj.source_repo.get_api_data())
4370 4379 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4371 4380
4372 4381 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4373 4382 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4374 4383 attrs.revisions = pull_request_obj.revisions
4375 4384 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4376 4385 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4377 4386 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4378 4387 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4379 4388
4380 4389 return PullRequestDisplay(attrs, internal=internal_methods)
4381 4390
4382 4391 def is_closed(self):
4383 4392 return self.status == self.STATUS_CLOSED
4384 4393
4385 4394 def is_state_changing(self):
4386 4395 return self.pull_request_state != PullRequest.STATE_CREATED
4387 4396
4388 4397 def __json__(self):
4389 4398 return {
4390 4399 'revisions': self.revisions,
4391 4400 'versions': self.versions_count
4392 4401 }
4393 4402
4394 4403 def calculated_review_status(self):
4395 4404 from rhodecode.model.changeset_status import ChangesetStatusModel
4396 4405 return ChangesetStatusModel().calculated_review_status(self)
4397 4406
4398 4407 def reviewers_statuses(self):
4399 4408 from rhodecode.model.changeset_status import ChangesetStatusModel
4400 4409 return ChangesetStatusModel().reviewers_statuses(self)
4401 4410
4402 4411 @property
4403 4412 def workspace_id(self):
4404 4413 from rhodecode.model.pull_request import PullRequestModel
4405 4414 return PullRequestModel()._workspace_id(self)
4406 4415
4407 4416 def get_shadow_repo(self):
4408 4417 workspace_id = self.workspace_id
4409 4418 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4410 4419 if os.path.isdir(shadow_repository_path):
4411 4420 vcs_obj = self.target_repo.scm_instance()
4412 4421 return vcs_obj.get_shadow_instance(shadow_repository_path)
4413 4422
4414 4423 @property
4415 4424 def versions_count(self):
4416 4425 """
4417 4426 return number of versions this PR have, e.g a PR that once been
4418 4427 updated will have 2 versions
4419 4428 """
4420 4429 return self.versions.count() + 1
4421 4430
4422 4431
4423 4432 class PullRequestVersion(Base, _PullRequestBase):
4424 4433 __tablename__ = 'pull_request_versions'
4425 4434 __table_args__ = (
4426 4435 base_table_args,
4427 4436 )
4428 4437
4429 4438 pull_request_version_id = Column(
4430 4439 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4431 4440 pull_request_id = Column(
4432 4441 'pull_request_id', Integer(),
4433 4442 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4434 4443 pull_request = relationship('PullRequest')
4435 4444
4436 4445 def __repr__(self):
4437 4446 if self.pull_request_version_id:
4438 4447 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4439 4448 else:
4440 4449 return '<DB:PullRequestVersion at %#x>' % id(self)
4441 4450
4442 4451 @property
4443 4452 def reviewers(self):
4444 4453 return self.pull_request.reviewers
4445 4454
4446 4455 @property
4447 4456 def versions(self):
4448 4457 return self.pull_request.versions
4449 4458
4450 4459 def is_closed(self):
4451 4460 # calculate from original
4452 4461 return self.pull_request.status == self.STATUS_CLOSED
4453 4462
4454 4463 def is_state_changing(self):
4455 4464 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4456 4465
4457 4466 def calculated_review_status(self):
4458 4467 return self.pull_request.calculated_review_status()
4459 4468
4460 4469 def reviewers_statuses(self):
4461 4470 return self.pull_request.reviewers_statuses()
4462 4471
4463 4472
4464 4473 class PullRequestReviewers(Base, BaseModel):
4465 4474 __tablename__ = 'pull_request_reviewers'
4466 4475 __table_args__ = (
4467 4476 base_table_args,
4468 4477 )
4469 4478
4470 4479 @hybrid_property
4471 4480 def reasons(self):
4472 4481 if not self._reasons:
4473 4482 return []
4474 4483 return self._reasons
4475 4484
4476 4485 @reasons.setter
4477 4486 def reasons(self, val):
4478 4487 val = val or []
4479 4488 if any(not isinstance(x, compat.string_types) for x in val):
4480 4489 raise Exception('invalid reasons type, must be list of strings')
4481 4490 self._reasons = val
4482 4491
4483 4492 pull_requests_reviewers_id = Column(
4484 4493 'pull_requests_reviewers_id', Integer(), nullable=False,
4485 4494 primary_key=True)
4486 4495 pull_request_id = Column(
4487 4496 "pull_request_id", Integer(),
4488 4497 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4489 4498 user_id = Column(
4490 4499 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4491 4500 _reasons = Column(
4492 4501 'reason', MutationList.as_mutable(
4493 4502 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4494 4503
4495 4504 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4496 4505 user = relationship('User')
4497 4506 pull_request = relationship('PullRequest')
4498 4507
4499 4508 rule_data = Column(
4500 4509 'rule_data_json',
4501 4510 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4502 4511
4503 4512 def rule_user_group_data(self):
4504 4513 """
4505 4514 Returns the voting user group rule data for this reviewer
4506 4515 """
4507 4516
4508 4517 if self.rule_data and 'vote_rule' in self.rule_data:
4509 4518 user_group_data = {}
4510 4519 if 'rule_user_group_entry_id' in self.rule_data:
4511 4520 # means a group with voting rules !
4512 4521 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4513 4522 user_group_data['name'] = self.rule_data['rule_name']
4514 4523 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4515 4524
4516 4525 return user_group_data
4517 4526
4518 4527 def __unicode__(self):
4519 4528 return u"<%s('id:%s')>" % (self.__class__.__name__,
4520 4529 self.pull_requests_reviewers_id)
4521 4530
4522 4531
4523 4532 class Notification(Base, BaseModel):
4524 4533 __tablename__ = 'notifications'
4525 4534 __table_args__ = (
4526 4535 Index('notification_type_idx', 'type'),
4527 4536 base_table_args,
4528 4537 )
4529 4538
4530 4539 TYPE_CHANGESET_COMMENT = u'cs_comment'
4531 4540 TYPE_MESSAGE = u'message'
4532 4541 TYPE_MENTION = u'mention'
4533 4542 TYPE_REGISTRATION = u'registration'
4534 4543 TYPE_PULL_REQUEST = u'pull_request'
4535 4544 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4536 4545 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4537 4546
4538 4547 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4539 4548 subject = Column('subject', Unicode(512), nullable=True)
4540 4549 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4541 4550 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4542 4551 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4543 4552 type_ = Column('type', Unicode(255))
4544 4553
4545 4554 created_by_user = relationship('User')
4546 4555 notifications_to_users = relationship('UserNotification', lazy='joined',
4547 4556 cascade="all, delete-orphan")
4548 4557
4549 4558 @property
4550 4559 def recipients(self):
4551 4560 return [x.user for x in UserNotification.query()\
4552 4561 .filter(UserNotification.notification == self)\
4553 4562 .order_by(UserNotification.user_id.asc()).all()]
4554 4563
4555 4564 @classmethod
4556 4565 def create(cls, created_by, subject, body, recipients, type_=None):
4557 4566 if type_ is None:
4558 4567 type_ = Notification.TYPE_MESSAGE
4559 4568
4560 4569 notification = cls()
4561 4570 notification.created_by_user = created_by
4562 4571 notification.subject = subject
4563 4572 notification.body = body
4564 4573 notification.type_ = type_
4565 4574 notification.created_on = datetime.datetime.now()
4566 4575
4567 4576 # For each recipient link the created notification to his account
4568 4577 for u in recipients:
4569 4578 assoc = UserNotification()
4570 4579 assoc.user_id = u.user_id
4571 4580 assoc.notification = notification
4572 4581
4573 4582 # if created_by is inside recipients mark his notification
4574 4583 # as read
4575 4584 if u.user_id == created_by.user_id:
4576 4585 assoc.read = True
4577 4586 Session().add(assoc)
4578 4587
4579 4588 Session().add(notification)
4580 4589
4581 4590 return notification
4582 4591
4583 4592
4584 4593 class UserNotification(Base, BaseModel):
4585 4594 __tablename__ = 'user_to_notification'
4586 4595 __table_args__ = (
4587 4596 UniqueConstraint('user_id', 'notification_id'),
4588 4597 base_table_args
4589 4598 )
4590 4599
4591 4600 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4592 4601 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4593 4602 read = Column('read', Boolean, default=False)
4594 4603 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4595 4604
4596 4605 user = relationship('User', lazy="joined")
4597 4606 notification = relationship('Notification', lazy="joined",
4598 4607 order_by=lambda: Notification.created_on.desc(),)
4599 4608
4600 4609 def mark_as_read(self):
4601 4610 self.read = True
4602 4611 Session().add(self)
4603 4612
4604 4613
4605 4614 class UserNotice(Base, BaseModel):
4606 4615 __tablename__ = 'user_notices'
4607 4616 __table_args__ = (
4608 4617 base_table_args
4609 4618 )
4610 4619
4611 4620 NOTIFICATION_TYPE_MESSAGE = 'message'
4612 4621 NOTIFICATION_TYPE_NOTICE = 'notice'
4613 4622
4614 4623 NOTIFICATION_LEVEL_INFO = 'info'
4615 4624 NOTIFICATION_LEVEL_WARNING = 'warning'
4616 4625 NOTIFICATION_LEVEL_ERROR = 'error'
4617 4626
4618 4627 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4619 4628
4620 4629 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4621 4630 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4622 4631
4623 4632 notice_read = Column('notice_read', Boolean, default=False)
4624 4633
4625 4634 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4626 4635 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4627 4636
4628 4637 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4629 4638 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4630 4639
4631 4640 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4632 4641 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4633 4642
4634 4643 @classmethod
4635 4644 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4636 4645
4637 4646 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4638 4647 cls.NOTIFICATION_LEVEL_WARNING,
4639 4648 cls.NOTIFICATION_LEVEL_INFO]:
4640 4649 return
4641 4650
4642 4651 from rhodecode.model.user import UserModel
4643 4652 user = UserModel().get_user(user)
4644 4653
4645 4654 new_notice = UserNotice()
4646 4655 if not allow_duplicate:
4647 4656 existing_msg = UserNotice().query() \
4648 4657 .filter(UserNotice.user == user) \
4649 4658 .filter(UserNotice.notice_body == body) \
4650 4659 .filter(UserNotice.notice_read == false()) \
4651 4660 .scalar()
4652 4661 if existing_msg:
4653 4662 log.warning('Ignoring duplicate notice for user %s', user)
4654 4663 return
4655 4664
4656 4665 new_notice.user = user
4657 4666 new_notice.notice_subject = subject
4658 4667 new_notice.notice_body = body
4659 4668 new_notice.notification_level = notice_level
4660 4669 Session().add(new_notice)
4661 4670 Session().commit()
4662 4671
4663 4672
4664 4673 class Gist(Base, BaseModel):
4665 4674 __tablename__ = 'gists'
4666 4675 __table_args__ = (
4667 4676 Index('g_gist_access_id_idx', 'gist_access_id'),
4668 4677 Index('g_created_on_idx', 'created_on'),
4669 4678 base_table_args
4670 4679 )
4671 4680
4672 4681 GIST_PUBLIC = u'public'
4673 4682 GIST_PRIVATE = u'private'
4674 4683 DEFAULT_FILENAME = u'gistfile1.txt'
4675 4684
4676 4685 ACL_LEVEL_PUBLIC = u'acl_public'
4677 4686 ACL_LEVEL_PRIVATE = u'acl_private'
4678 4687
4679 4688 gist_id = Column('gist_id', Integer(), primary_key=True)
4680 4689 gist_access_id = Column('gist_access_id', Unicode(250))
4681 4690 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4682 4691 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4683 4692 gist_expires = Column('gist_expires', Float(53), nullable=False)
4684 4693 gist_type = Column('gist_type', Unicode(128), nullable=False)
4685 4694 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4686 4695 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4687 4696 acl_level = Column('acl_level', Unicode(128), nullable=True)
4688 4697
4689 4698 owner = relationship('User')
4690 4699
4691 4700 def __repr__(self):
4692 4701 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4693 4702
4694 4703 @hybrid_property
4695 4704 def description_safe(self):
4696 4705 from rhodecode.lib import helpers as h
4697 4706 return h.escape(self.gist_description)
4698 4707
4699 4708 @classmethod
4700 4709 def get_or_404(cls, id_):
4701 4710 from pyramid.httpexceptions import HTTPNotFound
4702 4711
4703 4712 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4704 4713 if not res:
4705 4714 raise HTTPNotFound()
4706 4715 return res
4707 4716
4708 4717 @classmethod
4709 4718 def get_by_access_id(cls, gist_access_id):
4710 4719 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4711 4720
4712 4721 def gist_url(self):
4713 4722 from rhodecode.model.gist import GistModel
4714 4723 return GistModel().get_url(self)
4715 4724
4716 4725 @classmethod
4717 4726 def base_path(cls):
4718 4727 """
4719 4728 Returns base path when all gists are stored
4720 4729
4721 4730 :param cls:
4722 4731 """
4723 4732 from rhodecode.model.gist import GIST_STORE_LOC
4724 4733 q = Session().query(RhodeCodeUi)\
4725 4734 .filter(RhodeCodeUi.ui_key == URL_SEP)
4726 4735 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4727 4736 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4728 4737
4729 4738 def get_api_data(self):
4730 4739 """
4731 4740 Common function for generating gist related data for API
4732 4741 """
4733 4742 gist = self
4734 4743 data = {
4735 4744 'gist_id': gist.gist_id,
4736 4745 'type': gist.gist_type,
4737 4746 'access_id': gist.gist_access_id,
4738 4747 'description': gist.gist_description,
4739 4748 'url': gist.gist_url(),
4740 4749 'expires': gist.gist_expires,
4741 4750 'created_on': gist.created_on,
4742 4751 'modified_at': gist.modified_at,
4743 4752 'content': None,
4744 4753 'acl_level': gist.acl_level,
4745 4754 }
4746 4755 return data
4747 4756
4748 4757 def __json__(self):
4749 4758 data = dict(
4750 4759 )
4751 4760 data.update(self.get_api_data())
4752 4761 return data
4753 4762 # SCM functions
4754 4763
4755 4764 def scm_instance(self, **kwargs):
4756 4765 """
4757 4766 Get an instance of VCS Repository
4758 4767
4759 4768 :param kwargs:
4760 4769 """
4761 4770 from rhodecode.model.gist import GistModel
4762 4771 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4763 4772 return get_vcs_instance(
4764 4773 repo_path=safe_str(full_repo_path), create=False,
4765 4774 _vcs_alias=GistModel.vcs_backend)
4766 4775
4767 4776
4768 4777 class ExternalIdentity(Base, BaseModel):
4769 4778 __tablename__ = 'external_identities'
4770 4779 __table_args__ = (
4771 4780 Index('local_user_id_idx', 'local_user_id'),
4772 4781 Index('external_id_idx', 'external_id'),
4773 4782 base_table_args
4774 4783 )
4775 4784
4776 4785 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4777 4786 external_username = Column('external_username', Unicode(1024), default=u'')
4778 4787 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4779 4788 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4780 4789 access_token = Column('access_token', String(1024), default=u'')
4781 4790 alt_token = Column('alt_token', String(1024), default=u'')
4782 4791 token_secret = Column('token_secret', String(1024), default=u'')
4783 4792
4784 4793 @classmethod
4785 4794 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4786 4795 """
4787 4796 Returns ExternalIdentity instance based on search params
4788 4797
4789 4798 :param external_id:
4790 4799 :param provider_name:
4791 4800 :return: ExternalIdentity
4792 4801 """
4793 4802 query = cls.query()
4794 4803 query = query.filter(cls.external_id == external_id)
4795 4804 query = query.filter(cls.provider_name == provider_name)
4796 4805 if local_user_id:
4797 4806 query = query.filter(cls.local_user_id == local_user_id)
4798 4807 return query.first()
4799 4808
4800 4809 @classmethod
4801 4810 def user_by_external_id_and_provider(cls, external_id, provider_name):
4802 4811 """
4803 4812 Returns User instance based on search params
4804 4813
4805 4814 :param external_id:
4806 4815 :param provider_name:
4807 4816 :return: User
4808 4817 """
4809 4818 query = User.query()
4810 4819 query = query.filter(cls.external_id == external_id)
4811 4820 query = query.filter(cls.provider_name == provider_name)
4812 4821 query = query.filter(User.user_id == cls.local_user_id)
4813 4822 return query.first()
4814 4823
4815 4824 @classmethod
4816 4825 def by_local_user_id(cls, local_user_id):
4817 4826 """
4818 4827 Returns all tokens for user
4819 4828
4820 4829 :param local_user_id:
4821 4830 :return: ExternalIdentity
4822 4831 """
4823 4832 query = cls.query()
4824 4833 query = query.filter(cls.local_user_id == local_user_id)
4825 4834 return query
4826 4835
4827 4836 @classmethod
4828 4837 def load_provider_plugin(cls, plugin_id):
4829 4838 from rhodecode.authentication.base import loadplugin
4830 4839 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4831 4840 auth_plugin = loadplugin(_plugin_id)
4832 4841 return auth_plugin
4833 4842
4834 4843
4835 4844 class Integration(Base, BaseModel):
4836 4845 __tablename__ = 'integrations'
4837 4846 __table_args__ = (
4838 4847 base_table_args
4839 4848 )
4840 4849
4841 4850 integration_id = Column('integration_id', Integer(), primary_key=True)
4842 4851 integration_type = Column('integration_type', String(255))
4843 4852 enabled = Column('enabled', Boolean(), nullable=False)
4844 4853 name = Column('name', String(255), nullable=False)
4845 4854 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4846 4855 default=False)
4847 4856
4848 4857 settings = Column(
4849 4858 'settings_json', MutationObj.as_mutable(
4850 4859 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4851 4860 repo_id = Column(
4852 4861 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4853 4862 nullable=True, unique=None, default=None)
4854 4863 repo = relationship('Repository', lazy='joined')
4855 4864
4856 4865 repo_group_id = Column(
4857 4866 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4858 4867 nullable=True, unique=None, default=None)
4859 4868 repo_group = relationship('RepoGroup', lazy='joined')
4860 4869
4861 4870 @property
4862 4871 def scope(self):
4863 4872 if self.repo:
4864 4873 return repr(self.repo)
4865 4874 if self.repo_group:
4866 4875 if self.child_repos_only:
4867 4876 return repr(self.repo_group) + ' (child repos only)'
4868 4877 else:
4869 4878 return repr(self.repo_group) + ' (recursive)'
4870 4879 if self.child_repos_only:
4871 4880 return 'root_repos'
4872 4881 return 'global'
4873 4882
4874 4883 def __repr__(self):
4875 4884 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4876 4885
4877 4886
4878 4887 class RepoReviewRuleUser(Base, BaseModel):
4879 4888 __tablename__ = 'repo_review_rules_users'
4880 4889 __table_args__ = (
4881 4890 base_table_args
4882 4891 )
4883 4892
4884 4893 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4885 4894 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4886 4895 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4887 4896 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4888 4897 user = relationship('User')
4889 4898
4890 4899 def rule_data(self):
4891 4900 return {
4892 4901 'mandatory': self.mandatory
4893 4902 }
4894 4903
4895 4904
4896 4905 class RepoReviewRuleUserGroup(Base, BaseModel):
4897 4906 __tablename__ = 'repo_review_rules_users_groups'
4898 4907 __table_args__ = (
4899 4908 base_table_args
4900 4909 )
4901 4910
4902 4911 VOTE_RULE_ALL = -1
4903 4912
4904 4913 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4905 4914 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4906 4915 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4907 4916 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4908 4917 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4909 4918 users_group = relationship('UserGroup')
4910 4919
4911 4920 def rule_data(self):
4912 4921 return {
4913 4922 'mandatory': self.mandatory,
4914 4923 'vote_rule': self.vote_rule
4915 4924 }
4916 4925
4917 4926 @property
4918 4927 def vote_rule_label(self):
4919 4928 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4920 4929 return 'all must vote'
4921 4930 else:
4922 4931 return 'min. vote {}'.format(self.vote_rule)
4923 4932
4924 4933
4925 4934 class RepoReviewRule(Base, BaseModel):
4926 4935 __tablename__ = 'repo_review_rules'
4927 4936 __table_args__ = (
4928 4937 base_table_args
4929 4938 )
4930 4939
4931 4940 repo_review_rule_id = Column(
4932 4941 'repo_review_rule_id', Integer(), primary_key=True)
4933 4942 repo_id = Column(
4934 4943 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4935 4944 repo = relationship('Repository', backref='review_rules')
4936 4945
4937 4946 review_rule_name = Column('review_rule_name', String(255))
4938 4947 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4939 4948 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4940 4949 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4941 4950
4942 4951 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4943 4952 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4944 4953 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4945 4954 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4946 4955
4947 4956 rule_users = relationship('RepoReviewRuleUser')
4948 4957 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4949 4958
4950 4959 def _validate_pattern(self, value):
4951 4960 re.compile('^' + glob2re(value) + '$')
4952 4961
4953 4962 @hybrid_property
4954 4963 def source_branch_pattern(self):
4955 4964 return self._branch_pattern or '*'
4956 4965
4957 4966 @source_branch_pattern.setter
4958 4967 def source_branch_pattern(self, value):
4959 4968 self._validate_pattern(value)
4960 4969 self._branch_pattern = value or '*'
4961 4970
4962 4971 @hybrid_property
4963 4972 def target_branch_pattern(self):
4964 4973 return self._target_branch_pattern or '*'
4965 4974
4966 4975 @target_branch_pattern.setter
4967 4976 def target_branch_pattern(self, value):
4968 4977 self._validate_pattern(value)
4969 4978 self._target_branch_pattern = value or '*'
4970 4979
4971 4980 @hybrid_property
4972 4981 def file_pattern(self):
4973 4982 return self._file_pattern or '*'
4974 4983
4975 4984 @file_pattern.setter
4976 4985 def file_pattern(self, value):
4977 4986 self._validate_pattern(value)
4978 4987 self._file_pattern = value or '*'
4979 4988
4980 4989 def matches(self, source_branch, target_branch, files_changed):
4981 4990 """
4982 4991 Check if this review rule matches a branch/files in a pull request
4983 4992
4984 4993 :param source_branch: source branch name for the commit
4985 4994 :param target_branch: target branch name for the commit
4986 4995 :param files_changed: list of file paths changed in the pull request
4987 4996 """
4988 4997
4989 4998 source_branch = source_branch or ''
4990 4999 target_branch = target_branch or ''
4991 5000 files_changed = files_changed or []
4992 5001
4993 5002 branch_matches = True
4994 5003 if source_branch or target_branch:
4995 5004 if self.source_branch_pattern == '*':
4996 5005 source_branch_match = True
4997 5006 else:
4998 5007 if self.source_branch_pattern.startswith('re:'):
4999 5008 source_pattern = self.source_branch_pattern[3:]
5000 5009 else:
5001 5010 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5002 5011 source_branch_regex = re.compile(source_pattern)
5003 5012 source_branch_match = bool(source_branch_regex.search(source_branch))
5004 5013 if self.target_branch_pattern == '*':
5005 5014 target_branch_match = True
5006 5015 else:
5007 5016 if self.target_branch_pattern.startswith('re:'):
5008 5017 target_pattern = self.target_branch_pattern[3:]
5009 5018 else:
5010 5019 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5011 5020 target_branch_regex = re.compile(target_pattern)
5012 5021 target_branch_match = bool(target_branch_regex.search(target_branch))
5013 5022
5014 5023 branch_matches = source_branch_match and target_branch_match
5015 5024
5016 5025 files_matches = True
5017 5026 if self.file_pattern != '*':
5018 5027 files_matches = False
5019 5028 if self.file_pattern.startswith('re:'):
5020 5029 file_pattern = self.file_pattern[3:]
5021 5030 else:
5022 5031 file_pattern = glob2re(self.file_pattern)
5023 5032 file_regex = re.compile(file_pattern)
5024 5033 for file_data in files_changed:
5025 5034 filename = file_data.get('filename')
5026 5035
5027 5036 if file_regex.search(filename):
5028 5037 files_matches = True
5029 5038 break
5030 5039
5031 5040 return branch_matches and files_matches
5032 5041
5033 5042 @property
5034 5043 def review_users(self):
5035 5044 """ Returns the users which this rule applies to """
5036 5045
5037 5046 users = collections.OrderedDict()
5038 5047
5039 5048 for rule_user in self.rule_users:
5040 5049 if rule_user.user.active:
5041 5050 if rule_user.user not in users:
5042 5051 users[rule_user.user.username] = {
5043 5052 'user': rule_user.user,
5044 5053 'source': 'user',
5045 5054 'source_data': {},
5046 5055 'data': rule_user.rule_data()
5047 5056 }
5048 5057
5049 5058 for rule_user_group in self.rule_user_groups:
5050 5059 source_data = {
5051 5060 'user_group_id': rule_user_group.users_group.users_group_id,
5052 5061 'name': rule_user_group.users_group.users_group_name,
5053 5062 'members': len(rule_user_group.users_group.members)
5054 5063 }
5055 5064 for member in rule_user_group.users_group.members:
5056 5065 if member.user.active:
5057 5066 key = member.user.username
5058 5067 if key in users:
5059 5068 # skip this member as we have him already
5060 5069 # this prevents from override the "first" matched
5061 5070 # users with duplicates in multiple groups
5062 5071 continue
5063 5072
5064 5073 users[key] = {
5065 5074 'user': member.user,
5066 5075 'source': 'user_group',
5067 5076 'source_data': source_data,
5068 5077 'data': rule_user_group.rule_data()
5069 5078 }
5070 5079
5071 5080 return users
5072 5081
5073 5082 def user_group_vote_rule(self, user_id):
5074 5083
5075 5084 rules = []
5076 5085 if not self.rule_user_groups:
5077 5086 return rules
5078 5087
5079 5088 for user_group in self.rule_user_groups:
5080 5089 user_group_members = [x.user_id for x in user_group.users_group.members]
5081 5090 if user_id in user_group_members:
5082 5091 rules.append(user_group)
5083 5092 return rules
5084 5093
5085 5094 def __repr__(self):
5086 5095 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5087 5096 self.repo_review_rule_id, self.repo)
5088 5097
5089 5098
5090 5099 class ScheduleEntry(Base, BaseModel):
5091 5100 __tablename__ = 'schedule_entries'
5092 5101 __table_args__ = (
5093 5102 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5094 5103 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5095 5104 base_table_args,
5096 5105 )
5097 5106
5098 5107 schedule_types = ['crontab', 'timedelta', 'integer']
5099 5108 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5100 5109
5101 5110 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5102 5111 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5103 5112 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5104 5113
5105 5114 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5106 5115 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5107 5116
5108 5117 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5109 5118 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5110 5119
5111 5120 # task
5112 5121 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5113 5122 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5114 5123 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5115 5124 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5116 5125
5117 5126 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5118 5127 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5119 5128
5120 5129 @hybrid_property
5121 5130 def schedule_type(self):
5122 5131 return self._schedule_type
5123 5132
5124 5133 @schedule_type.setter
5125 5134 def schedule_type(self, val):
5126 5135 if val not in self.schedule_types:
5127 5136 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5128 5137 val, self.schedule_type))
5129 5138
5130 5139 self._schedule_type = val
5131 5140
5132 5141 @classmethod
5133 5142 def get_uid(cls, obj):
5134 5143 args = obj.task_args
5135 5144 kwargs = obj.task_kwargs
5136 5145 if isinstance(args, JsonRaw):
5137 5146 try:
5138 5147 args = json.loads(args)
5139 5148 except ValueError:
5140 5149 args = tuple()
5141 5150
5142 5151 if isinstance(kwargs, JsonRaw):
5143 5152 try:
5144 5153 kwargs = json.loads(kwargs)
5145 5154 except ValueError:
5146 5155 kwargs = dict()
5147 5156
5148 5157 dot_notation = obj.task_dot_notation
5149 5158 val = '.'.join(map(safe_str, [
5150 5159 sorted(dot_notation), args, sorted(kwargs.items())]))
5151 5160 return hashlib.sha1(val).hexdigest()
5152 5161
5153 5162 @classmethod
5154 5163 def get_by_schedule_name(cls, schedule_name):
5155 5164 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5156 5165
5157 5166 @classmethod
5158 5167 def get_by_schedule_id(cls, schedule_id):
5159 5168 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5160 5169
5161 5170 @property
5162 5171 def task(self):
5163 5172 return self.task_dot_notation
5164 5173
5165 5174 @property
5166 5175 def schedule(self):
5167 5176 from rhodecode.lib.celerylib.utils import raw_2_schedule
5168 5177 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5169 5178 return schedule
5170 5179
5171 5180 @property
5172 5181 def args(self):
5173 5182 try:
5174 5183 return list(self.task_args or [])
5175 5184 except ValueError:
5176 5185 return list()
5177 5186
5178 5187 @property
5179 5188 def kwargs(self):
5180 5189 try:
5181 5190 return dict(self.task_kwargs or {})
5182 5191 except ValueError:
5183 5192 return dict()
5184 5193
5185 5194 def _as_raw(self, val):
5186 5195 if hasattr(val, 'de_coerce'):
5187 5196 val = val.de_coerce()
5188 5197 if val:
5189 5198 val = json.dumps(val)
5190 5199
5191 5200 return val
5192 5201
5193 5202 @property
5194 5203 def schedule_definition_raw(self):
5195 5204 return self._as_raw(self.schedule_definition)
5196 5205
5197 5206 @property
5198 5207 def args_raw(self):
5199 5208 return self._as_raw(self.task_args)
5200 5209
5201 5210 @property
5202 5211 def kwargs_raw(self):
5203 5212 return self._as_raw(self.task_kwargs)
5204 5213
5205 5214 def __repr__(self):
5206 5215 return '<DB:ScheduleEntry({}:{})>'.format(
5207 5216 self.schedule_entry_id, self.schedule_name)
5208 5217
5209 5218
5210 5219 @event.listens_for(ScheduleEntry, 'before_update')
5211 5220 def update_task_uid(mapper, connection, target):
5212 5221 target.task_uid = ScheduleEntry.get_uid(target)
5213 5222
5214 5223
5215 5224 @event.listens_for(ScheduleEntry, 'before_insert')
5216 5225 def set_task_uid(mapper, connection, target):
5217 5226 target.task_uid = ScheduleEntry.get_uid(target)
5218 5227
5219 5228
5220 5229 class _BaseBranchPerms(BaseModel):
5221 5230 @classmethod
5222 5231 def compute_hash(cls, value):
5223 5232 return sha1_safe(value)
5224 5233
5225 5234 @hybrid_property
5226 5235 def branch_pattern(self):
5227 5236 return self._branch_pattern or '*'
5228 5237
5229 5238 @hybrid_property
5230 5239 def branch_hash(self):
5231 5240 return self._branch_hash
5232 5241
5233 5242 def _validate_glob(self, value):
5234 5243 re.compile('^' + glob2re(value) + '$')
5235 5244
5236 5245 @branch_pattern.setter
5237 5246 def branch_pattern(self, value):
5238 5247 self._validate_glob(value)
5239 5248 self._branch_pattern = value or '*'
5240 5249 # set the Hash when setting the branch pattern
5241 5250 self._branch_hash = self.compute_hash(self._branch_pattern)
5242 5251
5243 5252 def matches(self, branch):
5244 5253 """
5245 5254 Check if this the branch matches entry
5246 5255
5247 5256 :param branch: branch name for the commit
5248 5257 """
5249 5258
5250 5259 branch = branch or ''
5251 5260
5252 5261 branch_matches = True
5253 5262 if branch:
5254 5263 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5255 5264 branch_matches = bool(branch_regex.search(branch))
5256 5265
5257 5266 return branch_matches
5258 5267
5259 5268
5260 5269 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5261 5270 __tablename__ = 'user_to_repo_branch_permissions'
5262 5271 __table_args__ = (
5263 5272 base_table_args
5264 5273 )
5265 5274
5266 5275 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5267 5276
5268 5277 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5269 5278 repo = relationship('Repository', backref='user_branch_perms')
5270 5279
5271 5280 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5272 5281 permission = relationship('Permission')
5273 5282
5274 5283 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5275 5284 user_repo_to_perm = relationship('UserRepoToPerm')
5276 5285
5277 5286 rule_order = Column('rule_order', Integer(), nullable=False)
5278 5287 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5279 5288 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5280 5289
5281 5290 def __unicode__(self):
5282 5291 return u'<UserBranchPermission(%s => %r)>' % (
5283 5292 self.user_repo_to_perm, self.branch_pattern)
5284 5293
5285 5294
5286 5295 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5287 5296 __tablename__ = 'user_group_to_repo_branch_permissions'
5288 5297 __table_args__ = (
5289 5298 base_table_args
5290 5299 )
5291 5300
5292 5301 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5293 5302
5294 5303 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5295 5304 repo = relationship('Repository', backref='user_group_branch_perms')
5296 5305
5297 5306 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5298 5307 permission = relationship('Permission')
5299 5308
5300 5309 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5301 5310 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5302 5311
5303 5312 rule_order = Column('rule_order', Integer(), nullable=False)
5304 5313 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5305 5314 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5306 5315
5307 5316 def __unicode__(self):
5308 5317 return u'<UserBranchPermission(%s => %r)>' % (
5309 5318 self.user_group_repo_to_perm, self.branch_pattern)
5310 5319
5311 5320
5312 5321 class UserBookmark(Base, BaseModel):
5313 5322 __tablename__ = 'user_bookmarks'
5314 5323 __table_args__ = (
5315 5324 UniqueConstraint('user_id', 'bookmark_repo_id'),
5316 5325 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5317 5326 UniqueConstraint('user_id', 'bookmark_position'),
5318 5327 base_table_args
5319 5328 )
5320 5329
5321 5330 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5322 5331 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5323 5332 position = Column("bookmark_position", Integer(), nullable=False)
5324 5333 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5325 5334 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5326 5335 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5327 5336
5328 5337 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5329 5338 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5330 5339
5331 5340 user = relationship("User")
5332 5341
5333 5342 repository = relationship("Repository")
5334 5343 repository_group = relationship("RepoGroup")
5335 5344
5336 5345 @classmethod
5337 5346 def get_by_position_for_user(cls, position, user_id):
5338 5347 return cls.query() \
5339 5348 .filter(UserBookmark.user_id == user_id) \
5340 5349 .filter(UserBookmark.position == position).scalar()
5341 5350
5342 5351 @classmethod
5343 5352 def get_bookmarks_for_user(cls, user_id, cache=True):
5344 5353 bookmarks = cls.query() \
5345 5354 .filter(UserBookmark.user_id == user_id) \
5346 5355 .options(joinedload(UserBookmark.repository)) \
5347 5356 .options(joinedload(UserBookmark.repository_group)) \
5348 5357 .order_by(UserBookmark.position.asc())
5349 5358
5350 5359 if cache:
5351 5360 bookmarks = bookmarks.options(
5352 5361 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5353 5362 )
5354 5363
5355 5364 return bookmarks.all()
5356 5365
5357 5366 def __unicode__(self):
5358 5367 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5359 5368
5360 5369
5361 5370 class FileStore(Base, BaseModel):
5362 5371 __tablename__ = 'file_store'
5363 5372 __table_args__ = (
5364 5373 base_table_args
5365 5374 )
5366 5375
5367 5376 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5368 5377 file_uid = Column('file_uid', String(1024), nullable=False)
5369 5378 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5370 5379 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5371 5380 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5372 5381
5373 5382 # sha256 hash
5374 5383 file_hash = Column('file_hash', String(512), nullable=False)
5375 5384 file_size = Column('file_size', BigInteger(), nullable=False)
5376 5385
5377 5386 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5378 5387 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5379 5388 accessed_count = Column('accessed_count', Integer(), default=0)
5380 5389
5381 5390 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5382 5391
5383 5392 # if repo/repo_group reference is set, check for permissions
5384 5393 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5385 5394
5386 5395 # hidden defines an attachment that should be hidden from showing in artifact listing
5387 5396 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5388 5397
5389 5398 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5390 5399 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5391 5400
5392 5401 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5393 5402
5394 5403 # scope limited to user, which requester have access to
5395 5404 scope_user_id = Column(
5396 5405 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5397 5406 nullable=True, unique=None, default=None)
5398 5407 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5399 5408
5400 5409 # scope limited to user group, which requester have access to
5401 5410 scope_user_group_id = Column(
5402 5411 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5403 5412 nullable=True, unique=None, default=None)
5404 5413 user_group = relationship('UserGroup', lazy='joined')
5405 5414
5406 5415 # scope limited to repo, which requester have access to
5407 5416 scope_repo_id = Column(
5408 5417 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5409 5418 nullable=True, unique=None, default=None)
5410 5419 repo = relationship('Repository', lazy='joined')
5411 5420
5412 5421 # scope limited to repo group, which requester have access to
5413 5422 scope_repo_group_id = Column(
5414 5423 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5415 5424 nullable=True, unique=None, default=None)
5416 5425 repo_group = relationship('RepoGroup', lazy='joined')
5417 5426
5418 5427 @classmethod
5419 5428 def get_by_store_uid(cls, file_store_uid):
5420 5429 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5421 5430
5422 5431 @classmethod
5423 5432 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5424 5433 file_description='', enabled=True, hidden=False, check_acl=True,
5425 5434 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5426 5435
5427 5436 store_entry = FileStore()
5428 5437 store_entry.file_uid = file_uid
5429 5438 store_entry.file_display_name = file_display_name
5430 5439 store_entry.file_org_name = filename
5431 5440 store_entry.file_size = file_size
5432 5441 store_entry.file_hash = file_hash
5433 5442 store_entry.file_description = file_description
5434 5443
5435 5444 store_entry.check_acl = check_acl
5436 5445 store_entry.enabled = enabled
5437 5446 store_entry.hidden = hidden
5438 5447
5439 5448 store_entry.user_id = user_id
5440 5449 store_entry.scope_user_id = scope_user_id
5441 5450 store_entry.scope_repo_id = scope_repo_id
5442 5451 store_entry.scope_repo_group_id = scope_repo_group_id
5443 5452
5444 5453 return store_entry
5445 5454
5446 5455 @classmethod
5447 5456 def store_metadata(cls, file_store_id, args, commit=True):
5448 5457 file_store = FileStore.get(file_store_id)
5449 5458 if file_store is None:
5450 5459 return
5451 5460
5452 5461 for section, key, value, value_type in args:
5453 5462 has_key = FileStoreMetadata().query() \
5454 5463 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5455 5464 .filter(FileStoreMetadata.file_store_meta_section == section) \
5456 5465 .filter(FileStoreMetadata.file_store_meta_key == key) \
5457 5466 .scalar()
5458 5467 if has_key:
5459 5468 msg = 'key `{}` already defined under section `{}` for this file.'\
5460 5469 .format(key, section)
5461 5470 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5462 5471
5463 5472 # NOTE(marcink): raises ArtifactMetadataBadValueType
5464 5473 FileStoreMetadata.valid_value_type(value_type)
5465 5474
5466 5475 meta_entry = FileStoreMetadata()
5467 5476 meta_entry.file_store = file_store
5468 5477 meta_entry.file_store_meta_section = section
5469 5478 meta_entry.file_store_meta_key = key
5470 5479 meta_entry.file_store_meta_value_type = value_type
5471 5480 meta_entry.file_store_meta_value = value
5472 5481
5473 5482 Session().add(meta_entry)
5474 5483
5475 5484 try:
5476 5485 if commit:
5477 5486 Session().commit()
5478 5487 except IntegrityError:
5479 5488 Session().rollback()
5480 5489 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5481 5490
5482 5491 @classmethod
5483 5492 def bump_access_counter(cls, file_uid, commit=True):
5484 5493 FileStore().query()\
5485 5494 .filter(FileStore.file_uid == file_uid)\
5486 5495 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5487 5496 FileStore.accessed_on: datetime.datetime.now()})
5488 5497 if commit:
5489 5498 Session().commit()
5490 5499
5491 5500 def __json__(self):
5492 5501 data = {
5493 5502 'filename': self.file_display_name,
5494 5503 'filename_org': self.file_org_name,
5495 5504 'file_uid': self.file_uid,
5496 5505 'description': self.file_description,
5497 5506 'hidden': self.hidden,
5498 5507 'size': self.file_size,
5499 5508 'created_on': self.created_on,
5500 5509 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5501 5510 'downloaded_times': self.accessed_count,
5502 5511 'sha256': self.file_hash,
5503 5512 'metadata': self.file_metadata,
5504 5513 }
5505 5514
5506 5515 return data
5507 5516
5508 5517 def __repr__(self):
5509 5518 return '<FileStore({})>'.format(self.file_store_id)
5510 5519
5511 5520
5512 5521 class FileStoreMetadata(Base, BaseModel):
5513 5522 __tablename__ = 'file_store_metadata'
5514 5523 __table_args__ = (
5515 5524 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5516 5525 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5517 5526 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5518 5527 base_table_args
5519 5528 )
5520 5529 SETTINGS_TYPES = {
5521 5530 'str': safe_str,
5522 5531 'int': safe_int,
5523 5532 'unicode': safe_unicode,
5524 5533 'bool': str2bool,
5525 5534 'list': functools.partial(aslist, sep=',')
5526 5535 }
5527 5536
5528 5537 file_store_meta_id = Column(
5529 5538 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5530 5539 primary_key=True)
5531 5540 _file_store_meta_section = Column(
5532 5541 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5533 5542 nullable=True, unique=None, default=None)
5534 5543 _file_store_meta_section_hash = Column(
5535 5544 "file_store_meta_section_hash", String(255),
5536 5545 nullable=True, unique=None, default=None)
5537 5546 _file_store_meta_key = Column(
5538 5547 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5539 5548 nullable=True, unique=None, default=None)
5540 5549 _file_store_meta_key_hash = Column(
5541 5550 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5542 5551 _file_store_meta_value = Column(
5543 5552 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5544 5553 nullable=True, unique=None, default=None)
5545 5554 _file_store_meta_value_type = Column(
5546 5555 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5547 5556 default='unicode')
5548 5557
5549 5558 file_store_id = Column(
5550 5559 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5551 5560 nullable=True, unique=None, default=None)
5552 5561
5553 5562 file_store = relationship('FileStore', lazy='joined')
5554 5563
5555 5564 @classmethod
5556 5565 def valid_value_type(cls, value):
5557 5566 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5558 5567 raise ArtifactMetadataBadValueType(
5559 5568 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5560 5569
5561 5570 @hybrid_property
5562 5571 def file_store_meta_section(self):
5563 5572 return self._file_store_meta_section
5564 5573
5565 5574 @file_store_meta_section.setter
5566 5575 def file_store_meta_section(self, value):
5567 5576 self._file_store_meta_section = value
5568 5577 self._file_store_meta_section_hash = _hash_key(value)
5569 5578
5570 5579 @hybrid_property
5571 5580 def file_store_meta_key(self):
5572 5581 return self._file_store_meta_key
5573 5582
5574 5583 @file_store_meta_key.setter
5575 5584 def file_store_meta_key(self, value):
5576 5585 self._file_store_meta_key = value
5577 5586 self._file_store_meta_key_hash = _hash_key(value)
5578 5587
5579 5588 @hybrid_property
5580 5589 def file_store_meta_value(self):
5581 5590 val = self._file_store_meta_value
5582 5591
5583 5592 if self._file_store_meta_value_type:
5584 5593 # e.g unicode.encrypted == unicode
5585 5594 _type = self._file_store_meta_value_type.split('.')[0]
5586 5595 # decode the encrypted value if it's encrypted field type
5587 5596 if '.encrypted' in self._file_store_meta_value_type:
5588 5597 cipher = EncryptedTextValue()
5589 5598 val = safe_unicode(cipher.process_result_value(val, None))
5590 5599 # do final type conversion
5591 5600 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5592 5601 val = converter(val)
5593 5602
5594 5603 return val
5595 5604
5596 5605 @file_store_meta_value.setter
5597 5606 def file_store_meta_value(self, val):
5598 5607 val = safe_unicode(val)
5599 5608 # encode the encrypted value
5600 5609 if '.encrypted' in self.file_store_meta_value_type:
5601 5610 cipher = EncryptedTextValue()
5602 5611 val = safe_unicode(cipher.process_bind_param(val, None))
5603 5612 self._file_store_meta_value = val
5604 5613
5605 5614 @hybrid_property
5606 5615 def file_store_meta_value_type(self):
5607 5616 return self._file_store_meta_value_type
5608 5617
5609 5618 @file_store_meta_value_type.setter
5610 5619 def file_store_meta_value_type(self, val):
5611 5620 # e.g unicode.encrypted
5612 5621 self.valid_value_type(val)
5613 5622 self._file_store_meta_value_type = val
5614 5623
5615 5624 def __json__(self):
5616 5625 data = {
5617 5626 'artifact': self.file_store.file_uid,
5618 5627 'section': self.file_store_meta_section,
5619 5628 'key': self.file_store_meta_key,
5620 5629 'value': self.file_store_meta_value,
5621 5630 }
5622 5631
5623 5632 return data
5624 5633
5625 5634 def __repr__(self):
5626 5635 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5627 5636 self.file_store_meta_key, self.file_store_meta_value)
5628 5637
5629 5638
5630 5639 class DbMigrateVersion(Base, BaseModel):
5631 5640 __tablename__ = 'db_migrate_version'
5632 5641 __table_args__ = (
5633 5642 base_table_args,
5634 5643 )
5635 5644
5636 5645 repository_id = Column('repository_id', String(250), primary_key=True)
5637 5646 repository_path = Column('repository_path', Text)
5638 5647 version = Column('version', Integer)
5639 5648
5640 5649 @classmethod
5641 5650 def set_version(cls, version):
5642 5651 """
5643 5652 Helper for forcing a different version, usually for debugging purposes via ishell.
5644 5653 """
5645 5654 ver = DbMigrateVersion.query().first()
5646 5655 ver.version = version
5647 5656 Session().commit()
5648 5657
5649 5658
5650 5659 class DbSession(Base, BaseModel):
5651 5660 __tablename__ = 'db_session'
5652 5661 __table_args__ = (
5653 5662 base_table_args,
5654 5663 )
5655 5664
5656 5665 def __repr__(self):
5657 5666 return '<DB:DbSession({})>'.format(self.id)
5658 5667
5659 5668 id = Column('id', Integer())
5660 5669 namespace = Column('namespace', String(255), primary_key=True)
5661 5670 accessed = Column('accessed', DateTime, nullable=False)
5662 5671 created = Column('created', DateTime, nullable=False)
5663 5672 data = Column('data', PickleType, nullable=False)
General Comments 0
You need to be logged in to leave comments. Login now