##// 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

The requested changes are too big and content was truncated. Show full diff

@@ -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 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now