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