##// END OF EJS Templates
api: refactor auth helpers to reflect the action they do....
marcink -
r1150:081f50ab default
parent child Browse files
Show More
@@ -1,268 +1,268 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23 from mock import Mock, patch
23 from mock import Mock, patch
24
24
25 from rhodecode.api import utils
25 from rhodecode.api import utils
26 from rhodecode.api import JSONRPCError
26 from rhodecode.api import JSONRPCError
27 from rhodecode.lib.vcs.exceptions import RepositoryError
27 from rhodecode.lib.vcs.exceptions import RepositoryError
28
28
29
29
30 class TestGetCommitOrError(object):
30 class TestGetCommitOrError(object):
31 def setup(self):
31 def setup(self):
32 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
32 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
33
33
34 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d', 'branch:name'])
34 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d', 'branch:name'])
35 def test_ref_cannot_be_parsed(self, ref):
35 def test_ref_cannot_be_parsed(self, ref):
36 repo = Mock()
36 repo = Mock()
37 with pytest.raises(JSONRPCError) as excinfo:
37 with pytest.raises(JSONRPCError) as excinfo:
38 utils.get_commit_or_error(ref, repo)
38 utils.get_commit_or_error(ref, repo)
39 expected_message = (
39 expected_message = (
40 'Ref `{ref}` given in a wrong format. Please check the API'
40 'Ref `{ref}` given in a wrong format. Please check the API'
41 ' documentation for more details'.format(ref=ref)
41 ' documentation for more details'.format(ref=ref)
42 )
42 )
43 assert excinfo.value.message == expected_message
43 assert excinfo.value.message == expected_message
44
44
45 def test_success_with_hash_specified(self):
45 def test_success_with_hash_specified(self):
46 repo = Mock()
46 repo = Mock()
47 ref_type = 'branch'
47 ref_type = 'branch'
48 ref = '{}:master:{}'.format(ref_type, self.commit_hash)
48 ref = '{}:master:{}'.format(ref_type, self.commit_hash)
49
49
50 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
50 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
51 result = utils.get_commit_or_error(ref, repo)
51 result = utils.get_commit_or_error(ref, repo)
52 get_commit.assert_called_once_with(
52 get_commit.assert_called_once_with(
53 repo, self.commit_hash)
53 repo, self.commit_hash)
54 assert result == get_commit()
54 assert result == get_commit()
55
55
56 def test_raises_an_error_when_commit_not_found(self):
56 def test_raises_an_error_when_commit_not_found(self):
57 repo = Mock()
57 repo = Mock()
58 ref = 'branch:master:{}'.format(self.commit_hash)
58 ref = 'branch:master:{}'.format(self.commit_hash)
59
59
60 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
60 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
61 get_commit.side_effect = RepositoryError('Commit not found')
61 get_commit.side_effect = RepositoryError('Commit not found')
62 with pytest.raises(JSONRPCError) as excinfo:
62 with pytest.raises(JSONRPCError) as excinfo:
63 utils.get_commit_or_error(ref, repo)
63 utils.get_commit_or_error(ref, repo)
64 expected_message = 'Ref `{}` does not exist'.format(ref)
64 expected_message = 'Ref `{}` does not exist'.format(ref)
65 assert excinfo.value.message == expected_message
65 assert excinfo.value.message == expected_message
66
66
67
67
68 class TestResolveRefOrError(object):
68 class TestResolveRefOrError(object):
69 def setup(self):
69 def setup(self):
70 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
70 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
71
71
72 def test_success_with_no_hash_specified(self):
72 def test_success_with_no_hash_specified(self):
73 repo = Mock()
73 repo = Mock()
74 ref_type = 'branch'
74 ref_type = 'branch'
75 ref_name = 'master'
75 ref_name = 'master'
76 ref = '{}:{}'.format(ref_type, ref_name)
76 ref = '{}:{}'.format(ref_type, ref_name)
77
77
78 with patch('rhodecode.api.utils._get_ref_hash') \
78 with patch('rhodecode.api.utils._get_ref_hash') \
79 as _get_ref_hash:
79 as _get_ref_hash:
80 _get_ref_hash.return_value = self.commit_hash
80 _get_ref_hash.return_value = self.commit_hash
81 result = utils.resolve_ref_or_error(ref, repo)
81 result = utils.resolve_ref_or_error(ref, repo)
82 _get_ref_hash.assert_called_once_with(repo, ref_type, ref_name)
82 _get_ref_hash.assert_called_once_with(repo, ref_type, ref_name)
83 assert result == '{}:{}'.format(ref, self.commit_hash)
83 assert result == '{}:{}'.format(ref, self.commit_hash)
84
84
85 def test_non_supported_refs(self):
85 def test_non_supported_refs(self):
86 repo = Mock()
86 repo = Mock()
87 ref = 'ancestor:ref'
87 ref = 'ancestor:ref'
88 with pytest.raises(JSONRPCError) as excinfo:
88 with pytest.raises(JSONRPCError) as excinfo:
89 utils.resolve_ref_or_error(ref, repo)
89 utils.resolve_ref_or_error(ref, repo)
90 expected_message = 'The specified ancestor `ref` does not exist'
90 expected_message = 'The specified ancestor `ref` does not exist'
91 assert excinfo.value.message == expected_message
91 assert excinfo.value.message == expected_message
92
92
93 def test_branch_is_not_found(self):
93 def test_branch_is_not_found(self):
94 repo = Mock()
94 repo = Mock()
95 ref = 'branch:non-existing-one'
95 ref = 'branch:non-existing-one'
96 with patch('rhodecode.api.utils._get_ref_hash')\
96 with patch('rhodecode.api.utils._get_ref_hash')\
97 as _get_ref_hash:
97 as _get_ref_hash:
98 _get_ref_hash.side_effect = KeyError()
98 _get_ref_hash.side_effect = KeyError()
99 with pytest.raises(JSONRPCError) as excinfo:
99 with pytest.raises(JSONRPCError) as excinfo:
100 utils.resolve_ref_or_error(ref, repo)
100 utils.resolve_ref_or_error(ref, repo)
101 expected_message = (
101 expected_message = (
102 'The specified branch `non-existing-one` does not exist')
102 'The specified branch `non-existing-one` does not exist')
103 assert excinfo.value.message == expected_message
103 assert excinfo.value.message == expected_message
104
104
105 def test_bookmark_is_not_found(self):
105 def test_bookmark_is_not_found(self):
106 repo = Mock()
106 repo = Mock()
107 ref = 'bookmark:non-existing-one'
107 ref = 'bookmark:non-existing-one'
108 with patch('rhodecode.api.utils._get_ref_hash')\
108 with patch('rhodecode.api.utils._get_ref_hash')\
109 as _get_ref_hash:
109 as _get_ref_hash:
110 _get_ref_hash.side_effect = KeyError()
110 _get_ref_hash.side_effect = KeyError()
111 with pytest.raises(JSONRPCError) as excinfo:
111 with pytest.raises(JSONRPCError) as excinfo:
112 utils.resolve_ref_or_error(ref, repo)
112 utils.resolve_ref_or_error(ref, repo)
113 expected_message = (
113 expected_message = (
114 'The specified bookmark `non-existing-one` does not exist')
114 'The specified bookmark `non-existing-one` does not exist')
115 assert excinfo.value.message == expected_message
115 assert excinfo.value.message == expected_message
116
116
117 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d'])
117 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d'])
118 def test_ref_cannot_be_parsed(self, ref):
118 def test_ref_cannot_be_parsed(self, ref):
119 repo = Mock()
119 repo = Mock()
120 with pytest.raises(JSONRPCError) as excinfo:
120 with pytest.raises(JSONRPCError) as excinfo:
121 utils.resolve_ref_or_error(ref, repo)
121 utils.resolve_ref_or_error(ref, repo)
122 expected_message = (
122 expected_message = (
123 'Ref `{ref}` given in a wrong format. Please check the API'
123 'Ref `{ref}` given in a wrong format. Please check the API'
124 ' documentation for more details'.format(ref=ref)
124 ' documentation for more details'.format(ref=ref)
125 )
125 )
126 assert excinfo.value.message == expected_message
126 assert excinfo.value.message == expected_message
127
127
128
128
129 class TestGetRefHash(object):
129 class TestGetRefHash(object):
130 def setup(self):
130 def setup(self):
131 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
131 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
132 self.bookmark_name = 'test-bookmark'
132 self.bookmark_name = 'test-bookmark'
133
133
134 @pytest.mark.parametrize("alias, branch_name", [
134 @pytest.mark.parametrize("alias, branch_name", [
135 ("git", "master"),
135 ("git", "master"),
136 ("hg", "default")
136 ("hg", "default")
137 ])
137 ])
138 def test_returns_hash_by_branch_name(self, alias, branch_name):
138 def test_returns_hash_by_branch_name(self, alias, branch_name):
139 with patch('rhodecode.model.db.Repository') as repo:
139 with patch('rhodecode.model.db.Repository') as repo:
140 repo.scm_instance().alias = alias
140 repo.scm_instance().alias = alias
141 repo.scm_instance().branches = {branch_name: self.commit_hash}
141 repo.scm_instance().branches = {branch_name: self.commit_hash}
142 result_hash = utils._get_ref_hash(repo, 'branch', branch_name)
142 result_hash = utils._get_ref_hash(repo, 'branch', branch_name)
143 assert result_hash == self.commit_hash
143 assert result_hash == self.commit_hash
144
144
145 @pytest.mark.parametrize("alias, branch_name", [
145 @pytest.mark.parametrize("alias, branch_name", [
146 ("git", "master"),
146 ("git", "master"),
147 ("hg", "default")
147 ("hg", "default")
148 ])
148 ])
149 def test_raises_error_when_branch_is_not_found(self, alias, branch_name):
149 def test_raises_error_when_branch_is_not_found(self, alias, branch_name):
150 with patch('rhodecode.model.db.Repository') as repo:
150 with patch('rhodecode.model.db.Repository') as repo:
151 repo.scm_instance().alias = alias
151 repo.scm_instance().alias = alias
152 repo.scm_instance().branches = {}
152 repo.scm_instance().branches = {}
153 with pytest.raises(KeyError):
153 with pytest.raises(KeyError):
154 utils._get_ref_hash(repo, 'branch', branch_name)
154 utils._get_ref_hash(repo, 'branch', branch_name)
155
155
156 def test_returns_hash_when_bookmark_is_specified_for_hg(self):
156 def test_returns_hash_when_bookmark_is_specified_for_hg(self):
157 with patch('rhodecode.model.db.Repository') as repo:
157 with patch('rhodecode.model.db.Repository') as repo:
158 repo.scm_instance().alias = 'hg'
158 repo.scm_instance().alias = 'hg'
159 repo.scm_instance().bookmarks = {
159 repo.scm_instance().bookmarks = {
160 self.bookmark_name: self.commit_hash}
160 self.bookmark_name: self.commit_hash}
161 result_hash = utils._get_ref_hash(
161 result_hash = utils._get_ref_hash(
162 repo, 'bookmark', self.bookmark_name)
162 repo, 'bookmark', self.bookmark_name)
163 assert result_hash == self.commit_hash
163 assert result_hash == self.commit_hash
164
164
165 def test_raises_error_when_bookmark_is_not_found_in_hg_repo(self):
165 def test_raises_error_when_bookmark_is_not_found_in_hg_repo(self):
166 with patch('rhodecode.model.db.Repository') as repo:
166 with patch('rhodecode.model.db.Repository') as repo:
167 repo.scm_instance().alias = 'hg'
167 repo.scm_instance().alias = 'hg'
168 repo.scm_instance().bookmarks = {}
168 repo.scm_instance().bookmarks = {}
169 with pytest.raises(KeyError):
169 with pytest.raises(KeyError):
170 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
170 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
171
171
172 def test_raises_error_when_bookmark_is_specified_for_git(self):
172 def test_raises_error_when_bookmark_is_specified_for_git(self):
173 with patch('rhodecode.model.db.Repository') as repo:
173 with patch('rhodecode.model.db.Repository') as repo:
174 repo.scm_instance().alias = 'git'
174 repo.scm_instance().alias = 'git'
175 repo.scm_instance().bookmarks = {
175 repo.scm_instance().bookmarks = {
176 self.bookmark_name: self.commit_hash}
176 self.bookmark_name: self.commit_hash}
177 with pytest.raises(ValueError):
177 with pytest.raises(ValueError):
178 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
178 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
179
179
180
180
181 class TestUserByNameOrError(object):
181 class TestUserByNameOrError(object):
182 def test_user_found_by_id(self):
182 def test_user_found_by_id(self):
183 fake_user = Mock(id=123)
183 fake_user = Mock(id=123)
184 patcher = patch('rhodecode.model.user.UserModel.get_user')
184 patcher = patch('rhodecode.model.user.UserModel.get_user')
185 with patcher as get_user:
185 with patcher as get_user:
186 get_user.return_value = fake_user
186 get_user.return_value = fake_user
187 result = utils.get_user_or_error('123')
187 result = utils.get_user_or_error('123')
188 assert result == fake_user
188 assert result == fake_user
189
189
190 def test_user_found_by_name(self):
190 def test_user_found_by_name(self):
191 fake_user = Mock(id=123)
191 fake_user = Mock(id=123)
192 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
192 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
193 with patcher as get_by_username:
193 with patcher as get_by_username:
194 get_by_username.return_value = fake_user
194 get_by_username.return_value = fake_user
195 result = utils.get_user_or_error('test')
195 result = utils.get_user_or_error('test')
196 assert result == fake_user
196 assert result == fake_user
197
197
198 def test_user_not_found_by_id(self):
198 def test_user_not_found_by_id(self):
199 patcher = patch('rhodecode.model.user.UserModel.get_user')
199 patcher = patch('rhodecode.model.user.UserModel.get_user')
200 with patcher as get_user:
200 with patcher as get_user:
201 get_user.return_value = None
201 get_user.return_value = None
202 with pytest.raises(JSONRPCError) as excinfo:
202 with pytest.raises(JSONRPCError) as excinfo:
203 utils.get_user_or_error('123')
203 utils.get_user_or_error('123')
204
204
205 expected_message = 'user `123` does not exist'
205 expected_message = 'user `123` does not exist'
206 assert excinfo.value.message == expected_message
206 assert excinfo.value.message == expected_message
207
207
208 def test_user_not_found_by_name(self):
208 def test_user_not_found_by_name(self):
209 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
209 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
210 with patcher as get_by_username:
210 with patcher as get_by_username:
211 get_by_username.return_value = None
211 get_by_username.return_value = None
212 with pytest.raises(JSONRPCError) as excinfo:
212 with pytest.raises(JSONRPCError) as excinfo:
213 utils.get_user_or_error('test')
213 utils.get_user_or_error('test')
214
214
215 expected_message = 'user `test` does not exist'
215 expected_message = 'user `test` does not exist'
216 assert excinfo.value.message == expected_message
216 assert excinfo.value.message == expected_message
217
217
218
218
219 class TestGetCommitDict:
219 class TestGetCommitDict:
220
220
221 @pytest.mark.parametrize('filename, expected', [
221 @pytest.mark.parametrize('filename, expected', [
222 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
222 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
223 (b'sp\xa4cial', u'sp\ufffdcial'),
223 (b'sp\xa4cial', u'sp\ufffdcial'),
224 ])
224 ])
225 def test_decodes_filenames_to_unicode(self, filename, expected):
225 def test_decodes_filenames_to_unicode(self, filename, expected):
226 result = utils._get_commit_dict(filename=filename, op='A')
226 result = utils._get_commit_dict(filename=filename, op='A')
227 assert result['filename'] == expected
227 assert result['filename'] == expected
228
228
229
229
230 class TestRepoAccess(object):
230 class TestRepoAccess(object):
231 def setup_method(self, method):
231 def setup_method(self, method):
232
232
233 self.admin_perm_patch = patch(
233 self.admin_perm_patch = patch(
234 'rhodecode.api.utils.HasPermissionAnyApi')
234 'rhodecode.api.utils.HasPermissionAnyApi')
235 self.repo_perm_patch = patch(
235 self.repo_perm_patch = patch(
236 'rhodecode.api.utils.HasRepoPermissionAnyApi')
236 'rhodecode.api.utils.HasRepoPermissionAnyApi')
237
237
238 def test_has_superadmin_permission_checks_for_admin(self):
238 def test_has_superadmin_permission_checks_for_admin(self):
239 admin_mock = Mock()
239 admin_mock = Mock()
240 with self.admin_perm_patch as amock:
240 with self.admin_perm_patch as amock:
241 amock.return_value = admin_mock
241 amock.return_value = admin_mock
242 assert utils.has_superadmin_permission('fake_user')
242 assert utils.has_superadmin_permission('fake_user')
243 amock.assert_called_once_with('hg.admin')
243 amock.assert_called_once_with('hg.admin')
244
244
245 admin_mock.assert_called_once_with(user='fake_user')
245 admin_mock.assert_called_once_with(user='fake_user')
246
246
247 def test_has_repo_permissions_checks_for_repo_access(self):
247 def test_has_repo_permissions_checks_for_repo_access(self):
248 repo_mock = Mock()
248 repo_mock = Mock()
249 fake_repo = Mock()
249 fake_repo = Mock()
250 with self.repo_perm_patch as rmock:
250 with self.repo_perm_patch as rmock:
251 rmock.return_value = repo_mock
251 rmock.return_value = repo_mock
252 assert utils.has_repo_permissions(
252 assert utils.validate_repo_permissions(
253 'fake_user', 'fake_repo_id', fake_repo,
253 'fake_user', 'fake_repo_id', fake_repo,
254 ['perm1', 'perm2'])
254 ['perm1', 'perm2'])
255 rmock.assert_called_once_with(*['perm1', 'perm2'])
255 rmock.assert_called_once_with(*['perm1', 'perm2'])
256
256
257 repo_mock.assert_called_once_with(
257 repo_mock.assert_called_once_with(
258 user='fake_user', repo_name=fake_repo.repo_name)
258 user='fake_user', repo_name=fake_repo.repo_name)
259
259
260 def test_has_repo_permissions_raises_not_found(self):
260 def test_has_repo_permissions_raises_not_found(self):
261 repo_mock = Mock(return_value=False)
261 repo_mock = Mock(return_value=False)
262 fake_repo = Mock()
262 fake_repo = Mock()
263 with self.repo_perm_patch as rmock:
263 with self.repo_perm_patch as rmock:
264 rmock.return_value = repo_mock
264 rmock.return_value = repo_mock
265 with pytest.raises(JSONRPCError) as excinfo:
265 with pytest.raises(JSONRPCError) as excinfo:
266 utils.has_repo_permissions(
266 utils.validate_repo_permissions(
267 'fake_user', 'fake_repo_id', fake_repo, 'perms')
267 'fake_user', 'fake_repo_id', fake_repo, 'perms')
268 assert 'fake_repo_id' in excinfo
268 assert 'fake_repo_id' in excinfo
@@ -1,407 +1,407 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 JSON RPC utils
22 JSON RPC utils
23 """
23 """
24
24
25 import collections
25 import collections
26 import logging
26 import logging
27
27
28 from rhodecode.api.exc import JSONRPCError
28 from rhodecode.api.exc import JSONRPCError
29 from rhodecode.lib.auth import HasPermissionAnyApi, HasRepoPermissionAnyApi, \
29 from rhodecode.lib.auth import HasPermissionAnyApi, HasRepoPermissionAnyApi, \
30 HasRepoGroupPermissionAnyApi
30 HasRepoGroupPermissionAnyApi
31 from rhodecode.lib.utils import safe_unicode
31 from rhodecode.lib.utils import safe_unicode
32 from rhodecode.controllers.utils import get_commit_from_ref_name
32 from rhodecode.controllers.utils import get_commit_from_ref_name
33 from rhodecode.lib.vcs.exceptions import RepositoryError
33 from rhodecode.lib.vcs.exceptions import RepositoryError
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class OAttr(object):
38 class OAttr(object):
39 """
39 """
40 Special Option that defines other attribute, and can default to them
40 Special Option that defines other attribute, and can default to them
41
41
42 Example::
42 Example::
43
43
44 def test(apiuser, userid=Optional(OAttr('apiuser')):
44 def test(apiuser, userid=Optional(OAttr('apiuser')):
45 user = Optional.extract(userid, evaluate_locals=local())
45 user = Optional.extract(userid, evaluate_locals=local())
46 #if we pass in userid, we get it, else it will default to apiuser
46 #if we pass in userid, we get it, else it will default to apiuser
47 #attribute
47 #attribute
48 """
48 """
49
49
50 def __init__(self, attr_name):
50 def __init__(self, attr_name):
51 self.attr_name = attr_name
51 self.attr_name = attr_name
52
52
53 def __repr__(self):
53 def __repr__(self):
54 return '<OptionalAttr:%s>' % self.attr_name
54 return '<OptionalAttr:%s>' % self.attr_name
55
55
56 def __call__(self):
56 def __call__(self):
57 return self
57 return self
58
58
59
59
60 class Optional(object):
60 class Optional(object):
61 """
61 """
62 Defines an optional parameter::
62 Defines an optional parameter::
63
63
64 param = param.getval() if isinstance(param, Optional) else param
64 param = param.getval() if isinstance(param, Optional) else param
65 param = param() if isinstance(param, Optional) else param
65 param = param() if isinstance(param, Optional) else param
66
66
67 is equivalent of::
67 is equivalent of::
68
68
69 param = Optional.extract(param)
69 param = Optional.extract(param)
70
70
71 """
71 """
72
72
73 def __init__(self, type_):
73 def __init__(self, type_):
74 self.type_ = type_
74 self.type_ = type_
75
75
76 def __repr__(self):
76 def __repr__(self):
77 return '<Optional:%s>' % self.type_.__repr__()
77 return '<Optional:%s>' % self.type_.__repr__()
78
78
79 def __call__(self):
79 def __call__(self):
80 return self.getval()
80 return self.getval()
81
81
82 def getval(self, evaluate_locals=None):
82 def getval(self, evaluate_locals=None):
83 """
83 """
84 returns value from this Optional instance
84 returns value from this Optional instance
85 """
85 """
86 if isinstance(self.type_, OAttr):
86 if isinstance(self.type_, OAttr):
87 param_name = self.type_.attr_name
87 param_name = self.type_.attr_name
88 if evaluate_locals:
88 if evaluate_locals:
89 return evaluate_locals[param_name]
89 return evaluate_locals[param_name]
90 # use params name
90 # use params name
91 return param_name
91 return param_name
92 return self.type_
92 return self.type_
93
93
94 @classmethod
94 @classmethod
95 def extract(cls, val, evaluate_locals=None):
95 def extract(cls, val, evaluate_locals=None):
96 """
96 """
97 Extracts value from Optional() instance
97 Extracts value from Optional() instance
98
98
99 :param val:
99 :param val:
100 :return: original value if it's not Optional instance else
100 :return: original value if it's not Optional instance else
101 value of instance
101 value of instance
102 """
102 """
103 if isinstance(val, cls):
103 if isinstance(val, cls):
104 return val.getval(evaluate_locals)
104 return val.getval(evaluate_locals)
105 return val
105 return val
106
106
107
107
108 def parse_args(cli_args, key_prefix=''):
108 def parse_args(cli_args, key_prefix=''):
109 from rhodecode.lib.utils2 import (escape_split)
109 from rhodecode.lib.utils2 import (escape_split)
110 kwargs = collections.defaultdict(dict)
110 kwargs = collections.defaultdict(dict)
111 for el in escape_split(cli_args, ','):
111 for el in escape_split(cli_args, ','):
112 kv = escape_split(el, '=', 1)
112 kv = escape_split(el, '=', 1)
113 if len(kv) == 2:
113 if len(kv) == 2:
114 k, v = kv
114 k, v = kv
115 kwargs[key_prefix + k] = v
115 kwargs[key_prefix + k] = v
116 return kwargs
116 return kwargs
117
117
118
118
119 def get_origin(obj):
119 def get_origin(obj):
120 """
120 """
121 Get origin of permission from object.
121 Get origin of permission from object.
122
122
123 :param obj:
123 :param obj:
124 """
124 """
125 origin = 'permission'
125 origin = 'permission'
126
126
127 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
127 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
128 # admin and owner case, maybe we should use dual string ?
128 # admin and owner case, maybe we should use dual string ?
129 origin = 'owner'
129 origin = 'owner'
130 elif getattr(obj, 'owner_row', ''):
130 elif getattr(obj, 'owner_row', ''):
131 origin = 'owner'
131 origin = 'owner'
132 elif getattr(obj, 'admin_row', ''):
132 elif getattr(obj, 'admin_row', ''):
133 origin = 'super-admin'
133 origin = 'super-admin'
134 return origin
134 return origin
135
135
136
136
137 def store_update(updates, attr, name):
137 def store_update(updates, attr, name):
138 """
138 """
139 Stores param in updates dict if it's not instance of Optional
139 Stores param in updates dict if it's not instance of Optional
140 allows easy updates of passed in params
140 allows easy updates of passed in params
141 """
141 """
142 if not isinstance(attr, Optional):
142 if not isinstance(attr, Optional):
143 updates[name] = attr
143 updates[name] = attr
144
144
145
145
146 def has_superadmin_permission(apiuser):
146 def has_superadmin_permission(apiuser):
147 """
147 """
148 Return True if apiuser is admin or return False
148 Return True if apiuser is admin or return False
149
149
150 :param apiuser:
150 :param apiuser:
151 """
151 """
152 if HasPermissionAnyApi('hg.admin')(user=apiuser):
152 if HasPermissionAnyApi('hg.admin')(user=apiuser):
153 return True
153 return True
154 return False
154 return False
155
155
156
156
157 def has_repo_permissions(apiuser, repoid, repo, perms):
157 def validate_repo_permissions(apiuser, repoid, repo, perms):
158 """
158 """
159 Raise JsonRPCError if apiuser is not authorized or return True
159 Raise JsonRPCError if apiuser is not authorized or return True
160
160
161 :param apiuser:
161 :param apiuser:
162 :param repoid:
162 :param repoid:
163 :param repo:
163 :param repo:
164 :param perms:
164 :param perms:
165 """
165 """
166 if not HasRepoPermissionAnyApi(*perms)(
166 if not HasRepoPermissionAnyApi(*perms)(
167 user=apiuser, repo_name=repo.repo_name):
167 user=apiuser, repo_name=repo.repo_name):
168 raise JSONRPCError(
168 raise JSONRPCError(
169 'repository `%s` does not exist' % repoid)
169 'repository `%s` does not exist' % repoid)
170
170
171 return True
171 return True
172
172
173
173
174 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
174 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
175 """
175 """
176 Raise JsonRPCError if apiuser is not authorized or return True
176 Raise JsonRPCError if apiuser is not authorized or return True
177
177
178 :param apiuser:
178 :param apiuser:
179 :param repogroupid: just the id of repository group
179 :param repogroupid: just the id of repository group
180 :param repo_group: instance of repo_group
180 :param repo_group: instance of repo_group
181 :param perms:
181 :param perms:
182 """
182 """
183 if not HasRepoGroupPermissionAnyApi(*perms)(
183 if not HasRepoGroupPermissionAnyApi(*perms)(
184 user=apiuser, group_name=repo_group.group_name):
184 user=apiuser, group_name=repo_group.group_name):
185 raise JSONRPCError(
185 raise JSONRPCError(
186 'repository group `%s` does not exist' % repogroupid)
186 'repository group `%s` does not exist' % repogroupid)
187
187
188 return True
188 return True
189
189
190
190
191 def has_set_owner_permissions(apiuser, owner):
191 def validate_set_owner_permissions(apiuser, owner):
192 if isinstance(owner, Optional):
192 if isinstance(owner, Optional):
193 owner = get_user_or_error(apiuser.user_id)
193 owner = get_user_or_error(apiuser.user_id)
194 else:
194 else:
195 if has_superadmin_permission(apiuser):
195 if has_superadmin_permission(apiuser):
196 owner = get_user_or_error(owner)
196 owner = get_user_or_error(owner)
197 else:
197 else:
198 # forbid setting owner for non-admins
198 # forbid setting owner for non-admins
199 raise JSONRPCError(
199 raise JSONRPCError(
200 'Only RhodeCode super-admin can specify `owner` param')
200 'Only RhodeCode super-admin can specify `owner` param')
201 return owner
201 return owner
202
202
203
203
204 def get_user_or_error(userid):
204 def get_user_or_error(userid):
205 """
205 """
206 Get user by id or name or return JsonRPCError if not found
206 Get user by id or name or return JsonRPCError if not found
207
207
208 :param userid:
208 :param userid:
209 """
209 """
210 from rhodecode.model.user import UserModel
210 from rhodecode.model.user import UserModel
211
211
212 user_model = UserModel()
212 user_model = UserModel()
213 try:
213 try:
214 user = user_model.get_user(int(userid))
214 user = user_model.get_user(int(userid))
215 except ValueError:
215 except ValueError:
216 user = user_model.get_by_username(userid)
216 user = user_model.get_by_username(userid)
217
217
218 if user is None:
218 if user is None:
219 raise JSONRPCError("user `%s` does not exist" % (userid,))
219 raise JSONRPCError("user `%s` does not exist" % (userid,))
220 return user
220 return user
221
221
222
222
223 def get_repo_or_error(repoid):
223 def get_repo_or_error(repoid):
224 """
224 """
225 Get repo by id or name or return JsonRPCError if not found
225 Get repo by id or name or return JsonRPCError if not found
226
226
227 :param repoid:
227 :param repoid:
228 """
228 """
229 from rhodecode.model.repo import RepoModel
229 from rhodecode.model.repo import RepoModel
230
230
231 repo = RepoModel().get_repo(repoid)
231 repo = RepoModel().get_repo(repoid)
232 if repo is None:
232 if repo is None:
233 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
233 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
234 return repo
234 return repo
235
235
236
236
237 def get_repo_group_or_error(repogroupid):
237 def get_repo_group_or_error(repogroupid):
238 """
238 """
239 Get repo group by id or name or return JsonRPCError if not found
239 Get repo group by id or name or return JsonRPCError if not found
240
240
241 :param repogroupid:
241 :param repogroupid:
242 """
242 """
243 from rhodecode.model.repo_group import RepoGroupModel
243 from rhodecode.model.repo_group import RepoGroupModel
244
244
245 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
245 repo_group = RepoGroupModel()._get_repo_group(repogroupid)
246 if repo_group is None:
246 if repo_group is None:
247 raise JSONRPCError(
247 raise JSONRPCError(
248 'repository group `%s` does not exist' % (repogroupid,))
248 'repository group `%s` does not exist' % (repogroupid,))
249 return repo_group
249 return repo_group
250
250
251
251
252 def get_user_group_or_error(usergroupid):
252 def get_user_group_or_error(usergroupid):
253 """
253 """
254 Get user group by id or name or return JsonRPCError if not found
254 Get user group by id or name or return JsonRPCError if not found
255
255
256 :param usergroupid:
256 :param usergroupid:
257 """
257 """
258 from rhodecode.model.user_group import UserGroupModel
258 from rhodecode.model.user_group import UserGroupModel
259
259
260 user_group = UserGroupModel().get_group(usergroupid)
260 user_group = UserGroupModel().get_group(usergroupid)
261 if user_group is None:
261 if user_group is None:
262 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
262 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
263 return user_group
263 return user_group
264
264
265
265
266 def get_perm_or_error(permid, prefix=None):
266 def get_perm_or_error(permid, prefix=None):
267 """
267 """
268 Get permission by id or name or return JsonRPCError if not found
268 Get permission by id or name or return JsonRPCError if not found
269
269
270 :param permid:
270 :param permid:
271 """
271 """
272 from rhodecode.model.permission import PermissionModel
272 from rhodecode.model.permission import PermissionModel
273
273
274 perm = PermissionModel.cls.get_by_key(permid)
274 perm = PermissionModel.cls.get_by_key(permid)
275 if perm is None:
275 if perm is None:
276 raise JSONRPCError('permission `%s` does not exist' % (permid,))
276 raise JSONRPCError('permission `%s` does not exist' % (permid,))
277 if prefix:
277 if prefix:
278 if not perm.permission_name.startswith(prefix):
278 if not perm.permission_name.startswith(prefix):
279 raise JSONRPCError('permission `%s` is invalid, '
279 raise JSONRPCError('permission `%s` is invalid, '
280 'should start with %s' % (permid, prefix))
280 'should start with %s' % (permid, prefix))
281 return perm
281 return perm
282
282
283
283
284 def get_gist_or_error(gistid):
284 def get_gist_or_error(gistid):
285 """
285 """
286 Get gist by id or gist_access_id or return JsonRPCError if not found
286 Get gist by id or gist_access_id or return JsonRPCError if not found
287
287
288 :param gistid:
288 :param gistid:
289 """
289 """
290 from rhodecode.model.gist import GistModel
290 from rhodecode.model.gist import GistModel
291
291
292 gist = GistModel.cls.get_by_access_id(gistid)
292 gist = GistModel.cls.get_by_access_id(gistid)
293 if gist is None:
293 if gist is None:
294 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
294 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
295 return gist
295 return gist
296
296
297
297
298 def get_pull_request_or_error(pullrequestid):
298 def get_pull_request_or_error(pullrequestid):
299 """
299 """
300 Get pull request by id or return JsonRPCError if not found
300 Get pull request by id or return JsonRPCError if not found
301
301
302 :param pullrequestid:
302 :param pullrequestid:
303 """
303 """
304 from rhodecode.model.pull_request import PullRequestModel
304 from rhodecode.model.pull_request import PullRequestModel
305
305
306 try:
306 try:
307 pull_request = PullRequestModel().get(int(pullrequestid))
307 pull_request = PullRequestModel().get(int(pullrequestid))
308 except ValueError:
308 except ValueError:
309 raise JSONRPCError('pullrequestid must be an integer')
309 raise JSONRPCError('pullrequestid must be an integer')
310 if not pull_request:
310 if not pull_request:
311 raise JSONRPCError('pull request `%s` does not exist' % (
311 raise JSONRPCError('pull request `%s` does not exist' % (
312 pullrequestid,))
312 pullrequestid,))
313 return pull_request
313 return pull_request
314
314
315
315
316 def build_commit_data(commit, detail_level):
316 def build_commit_data(commit, detail_level):
317 parsed_diff = []
317 parsed_diff = []
318 if detail_level == 'extended':
318 if detail_level == 'extended':
319 for f in commit.added:
319 for f in commit.added:
320 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
320 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
321 for f in commit.changed:
321 for f in commit.changed:
322 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
322 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
323 for f in commit.removed:
323 for f in commit.removed:
324 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
324 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
325
325
326 elif detail_level == 'full':
326 elif detail_level == 'full':
327 from rhodecode.lib.diffs import DiffProcessor
327 from rhodecode.lib.diffs import DiffProcessor
328 diff_processor = DiffProcessor(commit.diff())
328 diff_processor = DiffProcessor(commit.diff())
329 for dp in diff_processor.prepare():
329 for dp in diff_processor.prepare():
330 del dp['stats']['ops']
330 del dp['stats']['ops']
331 _stats = dp['stats']
331 _stats = dp['stats']
332 parsed_diff.append(_get_commit_dict(
332 parsed_diff.append(_get_commit_dict(
333 filename=dp['filename'], op=dp['operation'],
333 filename=dp['filename'], op=dp['operation'],
334 new_revision=dp['new_revision'],
334 new_revision=dp['new_revision'],
335 old_revision=dp['old_revision'],
335 old_revision=dp['old_revision'],
336 raw_diff=dp['raw_diff'], stats=_stats))
336 raw_diff=dp['raw_diff'], stats=_stats))
337
337
338 return parsed_diff
338 return parsed_diff
339
339
340
340
341 def get_commit_or_error(ref, repo):
341 def get_commit_or_error(ref, repo):
342 try:
342 try:
343 ref_type, _, ref_hash = ref.split(':')
343 ref_type, _, ref_hash = ref.split(':')
344 except ValueError:
344 except ValueError:
345 raise JSONRPCError(
345 raise JSONRPCError(
346 'Ref `{ref}` given in a wrong format. Please check the API'
346 'Ref `{ref}` given in a wrong format. Please check the API'
347 ' documentation for more details'.format(ref=ref))
347 ' documentation for more details'.format(ref=ref))
348 try:
348 try:
349 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
349 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
350 # once get_commit supports ref_types
350 # once get_commit supports ref_types
351 return get_commit_from_ref_name(repo, ref_hash)
351 return get_commit_from_ref_name(repo, ref_hash)
352 except RepositoryError:
352 except RepositoryError:
353 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
353 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
354
354
355
355
356 def resolve_ref_or_error(ref, repo):
356 def resolve_ref_or_error(ref, repo):
357 def _parse_ref(type_, name, hash_=None):
357 def _parse_ref(type_, name, hash_=None):
358 return type_, name, hash_
358 return type_, name, hash_
359
359
360 try:
360 try:
361 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
361 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
362 except TypeError:
362 except TypeError:
363 raise JSONRPCError(
363 raise JSONRPCError(
364 'Ref `{ref}` given in a wrong format. Please check the API'
364 'Ref `{ref}` given in a wrong format. Please check the API'
365 ' documentation for more details'.format(ref=ref))
365 ' documentation for more details'.format(ref=ref))
366
366
367 try:
367 try:
368 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
368 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
369 except (KeyError, ValueError):
369 except (KeyError, ValueError):
370 raise JSONRPCError(
370 raise JSONRPCError(
371 'The specified {type} `{name}` does not exist'.format(
371 'The specified {type} `{name}` does not exist'.format(
372 type=ref_type, name=ref_name))
372 type=ref_type, name=ref_name))
373
373
374 return ':'.join([ref_type, ref_name, ref_hash])
374 return ':'.join([ref_type, ref_name, ref_hash])
375
375
376
376
377 def _get_commit_dict(
377 def _get_commit_dict(
378 filename, op, new_revision=None, old_revision=None,
378 filename, op, new_revision=None, old_revision=None,
379 raw_diff=None, stats=None):
379 raw_diff=None, stats=None):
380 if stats is None:
380 if stats is None:
381 stats = {
381 stats = {
382 "added": None,
382 "added": None,
383 "binary": None,
383 "binary": None,
384 "deleted": None
384 "deleted": None
385 }
385 }
386 return {
386 return {
387 "filename": safe_unicode(filename),
387 "filename": safe_unicode(filename),
388 "op": op,
388 "op": op,
389
389
390 # extra details
390 # extra details
391 "new_revision": new_revision,
391 "new_revision": new_revision,
392 "old_revision": old_revision,
392 "old_revision": old_revision,
393
393
394 "raw_diff": raw_diff,
394 "raw_diff": raw_diff,
395 "stats": stats
395 "stats": stats
396 }
396 }
397
397
398
398
399 # TODO: mikhail: Think about moving this function to some library
399 # TODO: mikhail: Think about moving this function to some library
400 def _get_ref_hash(repo, type_, name):
400 def _get_ref_hash(repo, type_, name):
401 vcs_repo = repo.scm_instance()
401 vcs_repo = repo.scm_instance()
402 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
402 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
403 return vcs_repo.branches[name]
403 return vcs_repo.branches[name]
404 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
404 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
405 return vcs_repo.bookmarks[name]
405 return vcs_repo.bookmarks[name]
406 else:
406 else:
407 raise ValueError()
407 raise ValueError()
@@ -1,691 +1,691 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2016 RhodeCode GmbH
3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError
24 from rhodecode.api import jsonrpc_method, JSONRPCError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 has_repo_permissions, resolve_ref_or_error)
28 validate_repo_permissions, resolve_ref_or_error)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.base import vcs_operation_context
30 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.changeset_status import ChangesetStatusModel
32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.comment import ChangesetCommentsModel
33 from rhodecode.model.comment import ChangesetCommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus
34 from rhodecode.model.db import Session, ChangesetStatus
35 from rhodecode.model.pull_request import PullRequestModel
35 from rhodecode.model.pull_request import PullRequestModel
36 from rhodecode.model.settings import SettingsModel
36 from rhodecode.model.settings import SettingsModel
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 @jsonrpc_method()
41 @jsonrpc_method()
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
42 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 """
43 """
44 Get a pull request based on the given ID.
44 Get a pull request based on the given ID.
45
45
46 :param apiuser: This is filled automatically from the |authtoken|.
46 :param apiuser: This is filled automatically from the |authtoken|.
47 :type apiuser: AuthUser
47 :type apiuser: AuthUser
48 :param repoid: Repository name or repository ID from where the pull
48 :param repoid: Repository name or repository ID from where the pull
49 request was opened.
49 request was opened.
50 :type repoid: str or int
50 :type repoid: str or int
51 :param pullrequestid: ID of the requested pull request.
51 :param pullrequestid: ID of the requested pull request.
52 :type pullrequestid: int
52 :type pullrequestid: int
53
53
54 Example output:
54 Example output:
55
55
56 .. code-block:: bash
56 .. code-block:: bash
57
57
58 "id": <id_given_in_input>,
58 "id": <id_given_in_input>,
59 "result":
59 "result":
60 {
60 {
61 "pull_request_id": "<pull_request_id>",
61 "pull_request_id": "<pull_request_id>",
62 "url": "<url>",
62 "url": "<url>",
63 "title": "<title>",
63 "title": "<title>",
64 "description": "<description>",
64 "description": "<description>",
65 "status" : "<status>",
65 "status" : "<status>",
66 "created_on": "<date_time_created>",
66 "created_on": "<date_time_created>",
67 "updated_on": "<date_time_updated>",
67 "updated_on": "<date_time_updated>",
68 "commit_ids": [
68 "commit_ids": [
69 ...
69 ...
70 "<commit_id>",
70 "<commit_id>",
71 "<commit_id>",
71 "<commit_id>",
72 ...
72 ...
73 ],
73 ],
74 "review_status": "<review_status>",
74 "review_status": "<review_status>",
75 "mergeable": {
75 "mergeable": {
76 "status": "<bool>",
76 "status": "<bool>",
77 "message": "<message>",
77 "message": "<message>",
78 },
78 },
79 "source": {
79 "source": {
80 "clone_url": "<clone_url>",
80 "clone_url": "<clone_url>",
81 "repository": "<repository_name>",
81 "repository": "<repository_name>",
82 "reference":
82 "reference":
83 {
83 {
84 "name": "<name>",
84 "name": "<name>",
85 "type": "<type>",
85 "type": "<type>",
86 "commit_id": "<commit_id>",
86 "commit_id": "<commit_id>",
87 }
87 }
88 },
88 },
89 "target": {
89 "target": {
90 "clone_url": "<clone_url>",
90 "clone_url": "<clone_url>",
91 "repository": "<repository_name>",
91 "repository": "<repository_name>",
92 "reference":
92 "reference":
93 {
93 {
94 "name": "<name>",
94 "name": "<name>",
95 "type": "<type>",
95 "type": "<type>",
96 "commit_id": "<commit_id>",
96 "commit_id": "<commit_id>",
97 }
97 }
98 },
98 },
99 "merge": {
99 "merge": {
100 "clone_url": "<clone_url>",
100 "clone_url": "<clone_url>",
101 "reference":
101 "reference":
102 {
102 {
103 "name": "<name>",
103 "name": "<name>",
104 "type": "<type>",
104 "type": "<type>",
105 "commit_id": "<commit_id>",
105 "commit_id": "<commit_id>",
106 }
106 }
107 },
107 },
108 "author": <user_obj>,
108 "author": <user_obj>,
109 "reviewers": [
109 "reviewers": [
110 ...
110 ...
111 {
111 {
112 "user": "<user_obj>",
112 "user": "<user_obj>",
113 "review_status": "<review_status>",
113 "review_status": "<review_status>",
114 }
114 }
115 ...
115 ...
116 ]
116 ]
117 },
117 },
118 "error": null
118 "error": null
119 """
119 """
120 get_repo_or_error(repoid)
120 get_repo_or_error(repoid)
121 pull_request = get_pull_request_or_error(pullrequestid)
121 pull_request = get_pull_request_or_error(pullrequestid)
122 if not PullRequestModel().check_user_read(
122 if not PullRequestModel().check_user_read(
123 pull_request, apiuser, api=True):
123 pull_request, apiuser, api=True):
124 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
124 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
125 data = pull_request.get_api_data()
125 data = pull_request.get_api_data()
126 return data
126 return data
127
127
128
128
129 @jsonrpc_method()
129 @jsonrpc_method()
130 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
130 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
131 """
131 """
132 Get all pull requests from the repository specified in `repoid`.
132 Get all pull requests from the repository specified in `repoid`.
133
133
134 :param apiuser: This is filled automatically from the |authtoken|.
134 :param apiuser: This is filled automatically from the |authtoken|.
135 :type apiuser: AuthUser
135 :type apiuser: AuthUser
136 :param repoid: Repository name or repository ID.
136 :param repoid: Repository name or repository ID.
137 :type repoid: str or int
137 :type repoid: str or int
138 :param status: Only return pull requests with the specified status.
138 :param status: Only return pull requests with the specified status.
139 Valid options are.
139 Valid options are.
140 * ``new`` (default)
140 * ``new`` (default)
141 * ``open``
141 * ``open``
142 * ``closed``
142 * ``closed``
143 :type status: str
143 :type status: str
144
144
145 Example output:
145 Example output:
146
146
147 .. code-block:: bash
147 .. code-block:: bash
148
148
149 "id": <id_given_in_input>,
149 "id": <id_given_in_input>,
150 "result":
150 "result":
151 [
151 [
152 ...
152 ...
153 {
153 {
154 "pull_request_id": "<pull_request_id>",
154 "pull_request_id": "<pull_request_id>",
155 "url": "<url>",
155 "url": "<url>",
156 "title" : "<title>",
156 "title" : "<title>",
157 "description": "<description>",
157 "description": "<description>",
158 "status": "<status>",
158 "status": "<status>",
159 "created_on": "<date_time_created>",
159 "created_on": "<date_time_created>",
160 "updated_on": "<date_time_updated>",
160 "updated_on": "<date_time_updated>",
161 "commit_ids": [
161 "commit_ids": [
162 ...
162 ...
163 "<commit_id>",
163 "<commit_id>",
164 "<commit_id>",
164 "<commit_id>",
165 ...
165 ...
166 ],
166 ],
167 "review_status": "<review_status>",
167 "review_status": "<review_status>",
168 "mergeable": {
168 "mergeable": {
169 "status": "<bool>",
169 "status": "<bool>",
170 "message: "<message>",
170 "message: "<message>",
171 },
171 },
172 "source": {
172 "source": {
173 "clone_url": "<clone_url>",
173 "clone_url": "<clone_url>",
174 "reference":
174 "reference":
175 {
175 {
176 "name": "<name>",
176 "name": "<name>",
177 "type": "<type>",
177 "type": "<type>",
178 "commit_id": "<commit_id>",
178 "commit_id": "<commit_id>",
179 }
179 }
180 },
180 },
181 "target": {
181 "target": {
182 "clone_url": "<clone_url>",
182 "clone_url": "<clone_url>",
183 "reference":
183 "reference":
184 {
184 {
185 "name": "<name>",
185 "name": "<name>",
186 "type": "<type>",
186 "type": "<type>",
187 "commit_id": "<commit_id>",
187 "commit_id": "<commit_id>",
188 }
188 }
189 },
189 },
190 "merge": {
190 "merge": {
191 "clone_url": "<clone_url>",
191 "clone_url": "<clone_url>",
192 "reference":
192 "reference":
193 {
193 {
194 "name": "<name>",
194 "name": "<name>",
195 "type": "<type>",
195 "type": "<type>",
196 "commit_id": "<commit_id>",
196 "commit_id": "<commit_id>",
197 }
197 }
198 },
198 },
199 "author": <user_obj>,
199 "author": <user_obj>,
200 "reviewers": [
200 "reviewers": [
201 ...
201 ...
202 {
202 {
203 "user": "<user_obj>",
203 "user": "<user_obj>",
204 "review_status": "<review_status>",
204 "review_status": "<review_status>",
205 }
205 }
206 ...
206 ...
207 ]
207 ]
208 }
208 }
209 ...
209 ...
210 ],
210 ],
211 "error": null
211 "error": null
212
212
213 """
213 """
214 repo = get_repo_or_error(repoid)
214 repo = get_repo_or_error(repoid)
215 if not has_superadmin_permission(apiuser):
215 if not has_superadmin_permission(apiuser):
216 _perms = (
216 _perms = (
217 'repository.admin', 'repository.write', 'repository.read',)
217 'repository.admin', 'repository.write', 'repository.read',)
218 has_repo_permissions(apiuser, repoid, repo, _perms)
218 validate_repo_permissions(apiuser, repoid, repo, _perms)
219
219
220 status = Optional.extract(status)
220 status = Optional.extract(status)
221 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
221 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
222 data = [pr.get_api_data() for pr in pull_requests]
222 data = [pr.get_api_data() for pr in pull_requests]
223 return data
223 return data
224
224
225
225
226 @jsonrpc_method()
226 @jsonrpc_method()
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
228 userid=Optional(OAttr('apiuser'))):
228 userid=Optional(OAttr('apiuser'))):
229 """
229 """
230 Merge the pull request specified by `pullrequestid` into its target
230 Merge the pull request specified by `pullrequestid` into its target
231 repository.
231 repository.
232
232
233 :param apiuser: This is filled automatically from the |authtoken|.
233 :param apiuser: This is filled automatically from the |authtoken|.
234 :type apiuser: AuthUser
234 :type apiuser: AuthUser
235 :param repoid: The Repository name or repository ID of the
235 :param repoid: The Repository name or repository ID of the
236 target repository to which the |pr| is to be merged.
236 target repository to which the |pr| is to be merged.
237 :type repoid: str or int
237 :type repoid: str or int
238 :param pullrequestid: ID of the pull request which shall be merged.
238 :param pullrequestid: ID of the pull request which shall be merged.
239 :type pullrequestid: int
239 :type pullrequestid: int
240 :param userid: Merge the pull request as this user.
240 :param userid: Merge the pull request as this user.
241 :type userid: Optional(str or int)
241 :type userid: Optional(str or int)
242
242
243 Example output:
243 Example output:
244
244
245 .. code-block:: bash
245 .. code-block:: bash
246
246
247 "id": <id_given_in_input>,
247 "id": <id_given_in_input>,
248 "result":
248 "result":
249 {
249 {
250 "executed": "<bool>",
250 "executed": "<bool>",
251 "failure_reason": "<int>",
251 "failure_reason": "<int>",
252 "merge_commit_id": "<merge_commit_id>",
252 "merge_commit_id": "<merge_commit_id>",
253 "possible": "<bool>",
253 "possible": "<bool>",
254 "merge_ref": {
254 "merge_ref": {
255 "commit_id": "<commit_id>",
255 "commit_id": "<commit_id>",
256 "type": "<type>",
256 "type": "<type>",
257 "name": "<name>"
257 "name": "<name>"
258 }
258 }
259 },
259 },
260 "error": null
260 "error": null
261
261
262 """
262 """
263 repo = get_repo_or_error(repoid)
263 repo = get_repo_or_error(repoid)
264 if not isinstance(userid, Optional):
264 if not isinstance(userid, Optional):
265 if (has_superadmin_permission(apiuser) or
265 if (has_superadmin_permission(apiuser) or
266 HasRepoPermissionAnyApi('repository.admin')(
266 HasRepoPermissionAnyApi('repository.admin')(
267 user=apiuser, repo_name=repo.repo_name)):
267 user=apiuser, repo_name=repo.repo_name)):
268 apiuser = get_user_or_error(userid)
268 apiuser = get_user_or_error(userid)
269 else:
269 else:
270 raise JSONRPCError('userid is not the same as your user')
270 raise JSONRPCError('userid is not the same as your user')
271
271
272 pull_request = get_pull_request_or_error(pullrequestid)
272 pull_request = get_pull_request_or_error(pullrequestid)
273 if not PullRequestModel().check_user_merge(
273 if not PullRequestModel().check_user_merge(
274 pull_request, apiuser, api=True):
274 pull_request, apiuser, api=True):
275 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
275 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
276 if pull_request.is_closed():
276 if pull_request.is_closed():
277 raise JSONRPCError(
277 raise JSONRPCError(
278 'pull request `%s` merge failed, pull request is closed' % (
278 'pull request `%s` merge failed, pull request is closed' % (
279 pullrequestid,))
279 pullrequestid,))
280
280
281 target_repo = pull_request.target_repo
281 target_repo = pull_request.target_repo
282 extras = vcs_operation_context(
282 extras = vcs_operation_context(
283 request.environ, repo_name=target_repo.repo_name,
283 request.environ, repo_name=target_repo.repo_name,
284 username=apiuser.username, action='push',
284 username=apiuser.username, action='push',
285 scm=target_repo.repo_type)
285 scm=target_repo.repo_type)
286 merge_response = PullRequestModel().merge(
286 merge_response = PullRequestModel().merge(
287 pull_request, apiuser, extras=extras)
287 pull_request, apiuser, extras=extras)
288 if merge_response.executed:
288 if merge_response.executed:
289 PullRequestModel().close_pull_request(
289 PullRequestModel().close_pull_request(
290 pull_request.pull_request_id, apiuser)
290 pull_request.pull_request_id, apiuser)
291
291
292 Session().commit()
292 Session().commit()
293
293
294 # In previous versions the merge response directly contained the merge
294 # In previous versions the merge response directly contained the merge
295 # commit id. It is now contained in the merge reference object. To be
295 # commit id. It is now contained in the merge reference object. To be
296 # backwards compatible we have to extract it again.
296 # backwards compatible we have to extract it again.
297 merge_response = merge_response._asdict()
297 merge_response = merge_response._asdict()
298 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
298 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
299
299
300 return merge_response
300 return merge_response
301
301
302
302
303 @jsonrpc_method()
303 @jsonrpc_method()
304 def close_pull_request(request, apiuser, repoid, pullrequestid,
304 def close_pull_request(request, apiuser, repoid, pullrequestid,
305 userid=Optional(OAttr('apiuser'))):
305 userid=Optional(OAttr('apiuser'))):
306 """
306 """
307 Close the pull request specified by `pullrequestid`.
307 Close the pull request specified by `pullrequestid`.
308
308
309 :param apiuser: This is filled automatically from the |authtoken|.
309 :param apiuser: This is filled automatically from the |authtoken|.
310 :type apiuser: AuthUser
310 :type apiuser: AuthUser
311 :param repoid: Repository name or repository ID to which the pull
311 :param repoid: Repository name or repository ID to which the pull
312 request belongs.
312 request belongs.
313 :type repoid: str or int
313 :type repoid: str or int
314 :param pullrequestid: ID of the pull request to be closed.
314 :param pullrequestid: ID of the pull request to be closed.
315 :type pullrequestid: int
315 :type pullrequestid: int
316 :param userid: Close the pull request as this user.
316 :param userid: Close the pull request as this user.
317 :type userid: Optional(str or int)
317 :type userid: Optional(str or int)
318
318
319 Example output:
319 Example output:
320
320
321 .. code-block:: bash
321 .. code-block:: bash
322
322
323 "id": <id_given_in_input>,
323 "id": <id_given_in_input>,
324 "result":
324 "result":
325 {
325 {
326 "pull_request_id": "<int>",
326 "pull_request_id": "<int>",
327 "closed": "<bool>"
327 "closed": "<bool>"
328 },
328 },
329 "error": null
329 "error": null
330
330
331 """
331 """
332 repo = get_repo_or_error(repoid)
332 repo = get_repo_or_error(repoid)
333 if not isinstance(userid, Optional):
333 if not isinstance(userid, Optional):
334 if (has_superadmin_permission(apiuser) or
334 if (has_superadmin_permission(apiuser) or
335 HasRepoPermissionAnyApi('repository.admin')(
335 HasRepoPermissionAnyApi('repository.admin')(
336 user=apiuser, repo_name=repo.repo_name)):
336 user=apiuser, repo_name=repo.repo_name)):
337 apiuser = get_user_or_error(userid)
337 apiuser = get_user_or_error(userid)
338 else:
338 else:
339 raise JSONRPCError('userid is not the same as your user')
339 raise JSONRPCError('userid is not the same as your user')
340
340
341 pull_request = get_pull_request_or_error(pullrequestid)
341 pull_request = get_pull_request_or_error(pullrequestid)
342 if not PullRequestModel().check_user_update(
342 if not PullRequestModel().check_user_update(
343 pull_request, apiuser, api=True):
343 pull_request, apiuser, api=True):
344 raise JSONRPCError(
344 raise JSONRPCError(
345 'pull request `%s` close failed, no permission to close.' % (
345 'pull request `%s` close failed, no permission to close.' % (
346 pullrequestid,))
346 pullrequestid,))
347 if pull_request.is_closed():
347 if pull_request.is_closed():
348 raise JSONRPCError(
348 raise JSONRPCError(
349 'pull request `%s` is already closed' % (pullrequestid,))
349 'pull request `%s` is already closed' % (pullrequestid,))
350
350
351 PullRequestModel().close_pull_request(
351 PullRequestModel().close_pull_request(
352 pull_request.pull_request_id, apiuser)
352 pull_request.pull_request_id, apiuser)
353 Session().commit()
353 Session().commit()
354 data = {
354 data = {
355 'pull_request_id': pull_request.pull_request_id,
355 'pull_request_id': pull_request.pull_request_id,
356 'closed': True,
356 'closed': True,
357 }
357 }
358 return data
358 return data
359
359
360
360
361 @jsonrpc_method()
361 @jsonrpc_method()
362 def comment_pull_request(request, apiuser, repoid, pullrequestid,
362 def comment_pull_request(request, apiuser, repoid, pullrequestid,
363 message=Optional(None), status=Optional(None),
363 message=Optional(None), status=Optional(None),
364 userid=Optional(OAttr('apiuser'))):
364 userid=Optional(OAttr('apiuser'))):
365 """
365 """
366 Comment on the pull request specified with the `pullrequestid`,
366 Comment on the pull request specified with the `pullrequestid`,
367 in the |repo| specified by the `repoid`, and optionally change the
367 in the |repo| specified by the `repoid`, and optionally change the
368 review status.
368 review status.
369
369
370 :param apiuser: This is filled automatically from the |authtoken|.
370 :param apiuser: This is filled automatically from the |authtoken|.
371 :type apiuser: AuthUser
371 :type apiuser: AuthUser
372 :param repoid: The repository name or repository ID.
372 :param repoid: The repository name or repository ID.
373 :type repoid: str or int
373 :type repoid: str or int
374 :param pullrequestid: The pull request ID.
374 :param pullrequestid: The pull request ID.
375 :type pullrequestid: int
375 :type pullrequestid: int
376 :param message: The text content of the comment.
376 :param message: The text content of the comment.
377 :type message: str
377 :type message: str
378 :param status: (**Optional**) Set the approval status of the pull
378 :param status: (**Optional**) Set the approval status of the pull
379 request. Valid options are:
379 request. Valid options are:
380 * not_reviewed
380 * not_reviewed
381 * approved
381 * approved
382 * rejected
382 * rejected
383 * under_review
383 * under_review
384 :type status: str
384 :type status: str
385 :param userid: Comment on the pull request as this user
385 :param userid: Comment on the pull request as this user
386 :type userid: Optional(str or int)
386 :type userid: Optional(str or int)
387
387
388 Example output:
388 Example output:
389
389
390 .. code-block:: bash
390 .. code-block:: bash
391
391
392 id : <id_given_in_input>
392 id : <id_given_in_input>
393 result :
393 result :
394 {
394 {
395 "pull_request_id": "<Integer>",
395 "pull_request_id": "<Integer>",
396 "comment_id": "<Integer>"
396 "comment_id": "<Integer>"
397 }
397 }
398 error : null
398 error : null
399 """
399 """
400 repo = get_repo_or_error(repoid)
400 repo = get_repo_or_error(repoid)
401 if not isinstance(userid, Optional):
401 if not isinstance(userid, Optional):
402 if (has_superadmin_permission(apiuser) or
402 if (has_superadmin_permission(apiuser) or
403 HasRepoPermissionAnyApi('repository.admin')(
403 HasRepoPermissionAnyApi('repository.admin')(
404 user=apiuser, repo_name=repo.repo_name)):
404 user=apiuser, repo_name=repo.repo_name)):
405 apiuser = get_user_or_error(userid)
405 apiuser = get_user_or_error(userid)
406 else:
406 else:
407 raise JSONRPCError('userid is not the same as your user')
407 raise JSONRPCError('userid is not the same as your user')
408
408
409 pull_request = get_pull_request_or_error(pullrequestid)
409 pull_request = get_pull_request_or_error(pullrequestid)
410 if not PullRequestModel().check_user_read(
410 if not PullRequestModel().check_user_read(
411 pull_request, apiuser, api=True):
411 pull_request, apiuser, api=True):
412 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
412 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
413 message = Optional.extract(message)
413 message = Optional.extract(message)
414 status = Optional.extract(status)
414 status = Optional.extract(status)
415 if not message and not status:
415 if not message and not status:
416 raise JSONRPCError('message and status parameter missing')
416 raise JSONRPCError('message and status parameter missing')
417
417
418 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
418 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
419 status is not None):
419 status is not None):
420 raise JSONRPCError('unknown comment status`%s`' % status)
420 raise JSONRPCError('unknown comment status`%s`' % status)
421
421
422 allowed_to_change_status = PullRequestModel().check_user_change_status(
422 allowed_to_change_status = PullRequestModel().check_user_change_status(
423 pull_request, apiuser)
423 pull_request, apiuser)
424 text = message
424 text = message
425 if status and allowed_to_change_status:
425 if status and allowed_to_change_status:
426 st_message = (('Status change %(transition_icon)s %(status)s')
426 st_message = (('Status change %(transition_icon)s %(status)s')
427 % {'transition_icon': '>',
427 % {'transition_icon': '>',
428 'status': ChangesetStatus.get_status_lbl(status)})
428 'status': ChangesetStatus.get_status_lbl(status)})
429 text = message or st_message
429 text = message or st_message
430
430
431 rc_config = SettingsModel().get_all_settings()
431 rc_config = SettingsModel().get_all_settings()
432 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
432 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
433 comment = ChangesetCommentsModel().create(
433 comment = ChangesetCommentsModel().create(
434 text=text,
434 text=text,
435 repo=pull_request.target_repo.repo_id,
435 repo=pull_request.target_repo.repo_id,
436 user=apiuser.user_id,
436 user=apiuser.user_id,
437 pull_request=pull_request.pull_request_id,
437 pull_request=pull_request.pull_request_id,
438 f_path=None,
438 f_path=None,
439 line_no=None,
439 line_no=None,
440 status_change=(ChangesetStatus.get_status_lbl(status)
440 status_change=(ChangesetStatus.get_status_lbl(status)
441 if status and allowed_to_change_status else None),
441 if status and allowed_to_change_status else None),
442 status_change_type=(status
442 status_change_type=(status
443 if status and allowed_to_change_status else None),
443 if status and allowed_to_change_status else None),
444 closing_pr=False,
444 closing_pr=False,
445 renderer=renderer
445 renderer=renderer
446 )
446 )
447
447
448 if allowed_to_change_status and status:
448 if allowed_to_change_status and status:
449 ChangesetStatusModel().set_status(
449 ChangesetStatusModel().set_status(
450 pull_request.target_repo.repo_id,
450 pull_request.target_repo.repo_id,
451 status,
451 status,
452 apiuser.user_id,
452 apiuser.user_id,
453 comment,
453 comment,
454 pull_request=pull_request.pull_request_id
454 pull_request=pull_request.pull_request_id
455 )
455 )
456 Session().flush()
456 Session().flush()
457
457
458 Session().commit()
458 Session().commit()
459 data = {
459 data = {
460 'pull_request_id': pull_request.pull_request_id,
460 'pull_request_id': pull_request.pull_request_id,
461 'comment_id': comment.comment_id,
461 'comment_id': comment.comment_id,
462 'status': status
462 'status': status
463 }
463 }
464 return data
464 return data
465
465
466
466
467 @jsonrpc_method()
467 @jsonrpc_method()
468 def create_pull_request(
468 def create_pull_request(
469 request, apiuser, source_repo, target_repo, source_ref, target_ref,
469 request, apiuser, source_repo, target_repo, source_ref, target_ref,
470 title, description=Optional(''), reviewers=Optional(None)):
470 title, description=Optional(''), reviewers=Optional(None)):
471 """
471 """
472 Creates a new pull request.
472 Creates a new pull request.
473
473
474 Accepts refs in the following formats:
474 Accepts refs in the following formats:
475
475
476 * branch:<branch_name>:<sha>
476 * branch:<branch_name>:<sha>
477 * branch:<branch_name>
477 * branch:<branch_name>
478 * bookmark:<bookmark_name>:<sha> (Mercurial only)
478 * bookmark:<bookmark_name>:<sha> (Mercurial only)
479 * bookmark:<bookmark_name> (Mercurial only)
479 * bookmark:<bookmark_name> (Mercurial only)
480
480
481 :param apiuser: This is filled automatically from the |authtoken|.
481 :param apiuser: This is filled automatically from the |authtoken|.
482 :type apiuser: AuthUser
482 :type apiuser: AuthUser
483 :param source_repo: Set the source repository name.
483 :param source_repo: Set the source repository name.
484 :type source_repo: str
484 :type source_repo: str
485 :param target_repo: Set the target repository name.
485 :param target_repo: Set the target repository name.
486 :type target_repo: str
486 :type target_repo: str
487 :param source_ref: Set the source ref name.
487 :param source_ref: Set the source ref name.
488 :type source_ref: str
488 :type source_ref: str
489 :param target_ref: Set the target ref name.
489 :param target_ref: Set the target ref name.
490 :type target_ref: str
490 :type target_ref: str
491 :param title: Set the pull request title.
491 :param title: Set the pull request title.
492 :type title: str
492 :type title: str
493 :param description: Set the pull request description.
493 :param description: Set the pull request description.
494 :type description: Optional(str)
494 :type description: Optional(str)
495 :param reviewers: Set the new pull request reviewers list.
495 :param reviewers: Set the new pull request reviewers list.
496 :type reviewers: Optional(list)
496 :type reviewers: Optional(list)
497 Accepts username strings or objects of the format:
497 Accepts username strings or objects of the format:
498 {
498 {
499 'username': 'nick', 'reasons': ['original author']
499 'username': 'nick', 'reasons': ['original author']
500 }
500 }
501 """
501 """
502
502
503 source = get_repo_or_error(source_repo)
503 source = get_repo_or_error(source_repo)
504 target = get_repo_or_error(target_repo)
504 target = get_repo_or_error(target_repo)
505 if not has_superadmin_permission(apiuser):
505 if not has_superadmin_permission(apiuser):
506 _perms = ('repository.admin', 'repository.write', 'repository.read',)
506 _perms = ('repository.admin', 'repository.write', 'repository.read',)
507 has_repo_permissions(apiuser, source_repo, source, _perms)
507 validate_repo_permissions(apiuser, source_repo, source, _perms)
508
508
509 full_source_ref = resolve_ref_or_error(source_ref, source)
509 full_source_ref = resolve_ref_or_error(source_ref, source)
510 full_target_ref = resolve_ref_or_error(target_ref, target)
510 full_target_ref = resolve_ref_or_error(target_ref, target)
511 source_commit = get_commit_or_error(full_source_ref, source)
511 source_commit = get_commit_or_error(full_source_ref, source)
512 target_commit = get_commit_or_error(full_target_ref, target)
512 target_commit = get_commit_or_error(full_target_ref, target)
513 source_scm = source.scm_instance()
513 source_scm = source.scm_instance()
514 target_scm = target.scm_instance()
514 target_scm = target.scm_instance()
515
515
516 commit_ranges = target_scm.compare(
516 commit_ranges = target_scm.compare(
517 target_commit.raw_id, source_commit.raw_id, source_scm,
517 target_commit.raw_id, source_commit.raw_id, source_scm,
518 merge=True, pre_load=[])
518 merge=True, pre_load=[])
519
519
520 ancestor = target_scm.get_common_ancestor(
520 ancestor = target_scm.get_common_ancestor(
521 target_commit.raw_id, source_commit.raw_id, source_scm)
521 target_commit.raw_id, source_commit.raw_id, source_scm)
522
522
523 if not commit_ranges:
523 if not commit_ranges:
524 raise JSONRPCError('no commits found')
524 raise JSONRPCError('no commits found')
525
525
526 if not ancestor:
526 if not ancestor:
527 raise JSONRPCError('no common ancestor found')
527 raise JSONRPCError('no common ancestor found')
528
528
529 reviewer_objects = Optional.extract(reviewers) or []
529 reviewer_objects = Optional.extract(reviewers) or []
530 if not isinstance(reviewer_objects, list):
530 if not isinstance(reviewer_objects, list):
531 raise JSONRPCError('reviewers should be specified as a list')
531 raise JSONRPCError('reviewers should be specified as a list')
532
532
533 reviewers_reasons = []
533 reviewers_reasons = []
534 for reviewer_object in reviewer_objects:
534 for reviewer_object in reviewer_objects:
535 reviewer_reasons = []
535 reviewer_reasons = []
536 if isinstance(reviewer_object, (basestring, int)):
536 if isinstance(reviewer_object, (basestring, int)):
537 reviewer_username = reviewer_object
537 reviewer_username = reviewer_object
538 else:
538 else:
539 reviewer_username = reviewer_object['username']
539 reviewer_username = reviewer_object['username']
540 reviewer_reasons = reviewer_object.get('reasons', [])
540 reviewer_reasons = reviewer_object.get('reasons', [])
541
541
542 user = get_user_or_error(reviewer_username)
542 user = get_user_or_error(reviewer_username)
543 reviewers_reasons.append((user.user_id, reviewer_reasons))
543 reviewers_reasons.append((user.user_id, reviewer_reasons))
544
544
545 pull_request_model = PullRequestModel()
545 pull_request_model = PullRequestModel()
546 pull_request = pull_request_model.create(
546 pull_request = pull_request_model.create(
547 created_by=apiuser.user_id,
547 created_by=apiuser.user_id,
548 source_repo=source_repo,
548 source_repo=source_repo,
549 source_ref=full_source_ref,
549 source_ref=full_source_ref,
550 target_repo=target_repo,
550 target_repo=target_repo,
551 target_ref=full_target_ref,
551 target_ref=full_target_ref,
552 revisions=reversed(
552 revisions=reversed(
553 [commit.raw_id for commit in reversed(commit_ranges)]),
553 [commit.raw_id for commit in reversed(commit_ranges)]),
554 reviewers=reviewers_reasons,
554 reviewers=reviewers_reasons,
555 title=title,
555 title=title,
556 description=Optional.extract(description)
556 description=Optional.extract(description)
557 )
557 )
558
558
559 Session().commit()
559 Session().commit()
560 data = {
560 data = {
561 'msg': 'Created new pull request `{}`'.format(title),
561 'msg': 'Created new pull request `{}`'.format(title),
562 'pull_request_id': pull_request.pull_request_id,
562 'pull_request_id': pull_request.pull_request_id,
563 }
563 }
564 return data
564 return data
565
565
566
566
567 @jsonrpc_method()
567 @jsonrpc_method()
568 def update_pull_request(
568 def update_pull_request(
569 request, apiuser, repoid, pullrequestid, title=Optional(''),
569 request, apiuser, repoid, pullrequestid, title=Optional(''),
570 description=Optional(''), reviewers=Optional(None),
570 description=Optional(''), reviewers=Optional(None),
571 update_commits=Optional(None), close_pull_request=Optional(None)):
571 update_commits=Optional(None), close_pull_request=Optional(None)):
572 """
572 """
573 Updates a pull request.
573 Updates a pull request.
574
574
575 :param apiuser: This is filled automatically from the |authtoken|.
575 :param apiuser: This is filled automatically from the |authtoken|.
576 :type apiuser: AuthUser
576 :type apiuser: AuthUser
577 :param repoid: The repository name or repository ID.
577 :param repoid: The repository name or repository ID.
578 :type repoid: str or int
578 :type repoid: str or int
579 :param pullrequestid: The pull request ID.
579 :param pullrequestid: The pull request ID.
580 :type pullrequestid: int
580 :type pullrequestid: int
581 :param title: Set the pull request title.
581 :param title: Set the pull request title.
582 :type title: str
582 :type title: str
583 :param description: Update pull request description.
583 :param description: Update pull request description.
584 :type description: Optional(str)
584 :type description: Optional(str)
585 :param reviewers: Update pull request reviewers list with new value.
585 :param reviewers: Update pull request reviewers list with new value.
586 :type reviewers: Optional(list)
586 :type reviewers: Optional(list)
587 :param update_commits: Trigger update of commits for this pull request
587 :param update_commits: Trigger update of commits for this pull request
588 :type: update_commits: Optional(bool)
588 :type: update_commits: Optional(bool)
589 :param close_pull_request: Close this pull request with rejected state
589 :param close_pull_request: Close this pull request with rejected state
590 :type: close_pull_request: Optional(bool)
590 :type: close_pull_request: Optional(bool)
591
591
592 Example output:
592 Example output:
593
593
594 .. code-block:: bash
594 .. code-block:: bash
595
595
596 id : <id_given_in_input>
596 id : <id_given_in_input>
597 result :
597 result :
598 {
598 {
599 "msg": "Updated pull request `63`",
599 "msg": "Updated pull request `63`",
600 "pull_request": <pull_request_object>,
600 "pull_request": <pull_request_object>,
601 "updated_reviewers": {
601 "updated_reviewers": {
602 "added": [
602 "added": [
603 "username"
603 "username"
604 ],
604 ],
605 "removed": []
605 "removed": []
606 },
606 },
607 "updated_commits": {
607 "updated_commits": {
608 "added": [
608 "added": [
609 "<sha1_hash>"
609 "<sha1_hash>"
610 ],
610 ],
611 "common": [
611 "common": [
612 "<sha1_hash>",
612 "<sha1_hash>",
613 "<sha1_hash>",
613 "<sha1_hash>",
614 ],
614 ],
615 "removed": []
615 "removed": []
616 }
616 }
617 }
617 }
618 error : null
618 error : null
619 """
619 """
620
620
621 repo = get_repo_or_error(repoid)
621 repo = get_repo_or_error(repoid)
622 pull_request = get_pull_request_or_error(pullrequestid)
622 pull_request = get_pull_request_or_error(pullrequestid)
623 if not PullRequestModel().check_user_update(
623 if not PullRequestModel().check_user_update(
624 pull_request, apiuser, api=True):
624 pull_request, apiuser, api=True):
625 raise JSONRPCError(
625 raise JSONRPCError(
626 'pull request `%s` update failed, no permission to update.' % (
626 'pull request `%s` update failed, no permission to update.' % (
627 pullrequestid,))
627 pullrequestid,))
628 if pull_request.is_closed():
628 if pull_request.is_closed():
629 raise JSONRPCError(
629 raise JSONRPCError(
630 'pull request `%s` update failed, pull request is closed' % (
630 'pull request `%s` update failed, pull request is closed' % (
631 pullrequestid,))
631 pullrequestid,))
632
632
633 reviewer_objects = Optional.extract(reviewers) or []
633 reviewer_objects = Optional.extract(reviewers) or []
634 if not isinstance(reviewer_objects, list):
634 if not isinstance(reviewer_objects, list):
635 raise JSONRPCError('reviewers should be specified as a list')
635 raise JSONRPCError('reviewers should be specified as a list')
636
636
637 reviewers_reasons = []
637 reviewers_reasons = []
638 reviewer_ids = set()
638 reviewer_ids = set()
639 for reviewer_object in reviewer_objects:
639 for reviewer_object in reviewer_objects:
640 reviewer_reasons = []
640 reviewer_reasons = []
641 if isinstance(reviewer_object, (int, basestring)):
641 if isinstance(reviewer_object, (int, basestring)):
642 reviewer_username = reviewer_object
642 reviewer_username = reviewer_object
643 else:
643 else:
644 reviewer_username = reviewer_object['username']
644 reviewer_username = reviewer_object['username']
645 reviewer_reasons = reviewer_object.get('reasons', [])
645 reviewer_reasons = reviewer_object.get('reasons', [])
646
646
647 user = get_user_or_error(reviewer_username)
647 user = get_user_or_error(reviewer_username)
648 reviewer_ids.add(user.user_id)
648 reviewer_ids.add(user.user_id)
649 reviewers_reasons.append((user.user_id, reviewer_reasons))
649 reviewers_reasons.append((user.user_id, reviewer_reasons))
650
650
651 title = Optional.extract(title)
651 title = Optional.extract(title)
652 description = Optional.extract(description)
652 description = Optional.extract(description)
653 if title or description:
653 if title or description:
654 PullRequestModel().edit(
654 PullRequestModel().edit(
655 pull_request, title or pull_request.title,
655 pull_request, title or pull_request.title,
656 description or pull_request.description)
656 description or pull_request.description)
657 Session().commit()
657 Session().commit()
658
658
659 commit_changes = {"added": [], "common": [], "removed": []}
659 commit_changes = {"added": [], "common": [], "removed": []}
660 if str2bool(Optional.extract(update_commits)):
660 if str2bool(Optional.extract(update_commits)):
661 if PullRequestModel().has_valid_update_type(pull_request):
661 if PullRequestModel().has_valid_update_type(pull_request):
662 update_response = PullRequestModel().update_commits(
662 update_response = PullRequestModel().update_commits(
663 pull_request)
663 pull_request)
664 commit_changes = update_response.changes or commit_changes
664 commit_changes = update_response.changes or commit_changes
665 Session().commit()
665 Session().commit()
666
666
667 reviewers_changes = {"added": [], "removed": []}
667 reviewers_changes = {"added": [], "removed": []}
668 if reviewer_ids:
668 if reviewer_ids:
669 added_reviewers, removed_reviewers = \
669 added_reviewers, removed_reviewers = \
670 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
670 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
671
671
672 reviewers_changes['added'] = sorted(
672 reviewers_changes['added'] = sorted(
673 [get_user_or_error(n).username for n in added_reviewers])
673 [get_user_or_error(n).username for n in added_reviewers])
674 reviewers_changes['removed'] = sorted(
674 reviewers_changes['removed'] = sorted(
675 [get_user_or_error(n).username for n in removed_reviewers])
675 [get_user_or_error(n).username for n in removed_reviewers])
676 Session().commit()
676 Session().commit()
677
677
678 if str2bool(Optional.extract(close_pull_request)):
678 if str2bool(Optional.extract(close_pull_request)):
679 PullRequestModel().close_pull_request_with_comment(
679 PullRequestModel().close_pull_request_with_comment(
680 pull_request, apiuser, repo)
680 pull_request, apiuser, repo)
681 Session().commit()
681 Session().commit()
682
682
683 data = {
683 data = {
684 'msg': 'Updated pull request `{}`'.format(
684 'msg': 'Updated pull request `{}`'.format(
685 pull_request.pull_request_id),
685 pull_request.pull_request_id),
686 'pull_request': pull_request.get_api_data(),
686 'pull_request': pull_request.get_api_data(),
687 'updated_commits': commit_changes,
687 'updated_commits': commit_changes,
688 'updated_reviewers': reviewers_changes
688 'updated_reviewers': reviewers_changes
689 }
689 }
690
690
691 return data
691 return data
General Comments 0
You need to be logged in to leave comments. Login now