##// END OF EJS Templates
comments: extend API data with references to commit_id or pull_request_id for audit-logs.
marcink -
r4304:a588c9e6 default
parent child Browse files
Show More

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

@@ -1,83 +1,86 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import pytest
23 23 import urlobject
24 24
25 25 from rhodecode.api.tests.utils import (
26 26 build_data, api_call, assert_error, assert_ok)
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.utils2 import safe_unicode
29 29
30 30 pytestmark = pytest.mark.backends("git", "hg")
31 31
32 32
33 33 @pytest.mark.usefixtures("testuser_api", "app")
34 34 class TestGetPullRequestComments(object):
35 35
36 36 def test_api_get_pull_request_comments(self, pr_util, http_host_only_stub):
37 37 from rhodecode.model.pull_request import PullRequestModel
38 38
39 39 pull_request = pr_util.create_pull_request(mergeable=True)
40 40 id_, params = build_data(
41 41 self.apikey, 'get_pull_request_comments',
42 42 pullrequestid=pull_request.pull_request_id)
43 43
44 44 response = api_call(self.app, params)
45 45
46 46 assert response.status == '200 OK'
47 47 resp_date = response.json['result'][0]['comment_created_on']
48 48 resp_comment_id = response.json['result'][0]['comment_id']
49 49
50 50 expected = [
51 51 {'comment_author': {'active': True,
52 52 'full_name_or_username': 'RhodeCode Admin',
53 53 'username': 'test_admin'},
54 54 'comment_created_on': resp_date,
55 55 'comment_f_path': None,
56 56 'comment_id': resp_comment_id,
57 57 'comment_lineno': None,
58 58 'comment_status': {'status': 'under_review',
59 59 'status_lbl': 'Under Review'},
60 60 'comment_text': 'Auto status change to |new_status|\n\n.. |new_status| replace:: *"Under Review"*',
61 61 'comment_type': 'note',
62 62 'comment_resolved_by': None,
63 'pull_request_version': None}
63 'pull_request_version': None,
64 'comment_commit_id': None,
65 'comment_pull_request_id': pull_request.pull_request_id
66 }
64 67 ]
65 68 assert_ok(id_, expected, response.body)
66 69
67 70 def test_api_get_pull_request_comments_repo_error(self, pr_util):
68 71 pull_request = pr_util.create_pull_request()
69 72 id_, params = build_data(
70 73 self.apikey, 'get_pull_request_comments',
71 74 repoid=666, pullrequestid=pull_request.pull_request_id)
72 75 response = api_call(self.app, params)
73 76
74 77 expected = 'repository `666` does not exist'
75 78 assert_error(id_, expected, given=response.body)
76 79
77 80 def test_api_get_pull_request_comments_pull_request_error(self):
78 81 id_, params = build_data(
79 82 self.apikey, 'get_pull_request_comments', pullrequestid=666)
80 83 response = api_call(self.app, params)
81 84
82 85 expected = 'pull request `666` does not exist'
83 86 assert_error(id_, expected, given=response.body)
@@ -1,1016 +1,1018 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 import events
25 25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 26 from rhodecode.api.utils import (
27 27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
30 30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 31 from rhodecode.lib.base import vcs_operation_context
32 32 from rhodecode.lib.utils2 import str2bool
33 33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 34 from rhodecode.model.comment import CommentsModel
35 35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 37 from rhodecode.model.settings import SettingsModel
38 38 from rhodecode.model.validation_schema import Invalid
39 39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 40 ReviewerListSchema)
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 @jsonrpc_method()
46 46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
47 47 merge_state=Optional(False)):
48 48 """
49 49 Get a pull request based on the given ID.
50 50
51 51 :param apiuser: This is filled automatically from the |authtoken|.
52 52 :type apiuser: AuthUser
53 53 :param repoid: Optional, repository name or repository ID from where
54 54 the pull request was opened.
55 55 :type repoid: str or int
56 56 :param pullrequestid: ID of the requested pull request.
57 57 :type pullrequestid: int
58 58 :param merge_state: Optional calculate merge state for each repository.
59 59 This could result in longer time to fetch the data
60 60 :type merge_state: bool
61 61
62 62 Example output:
63 63
64 64 .. code-block:: bash
65 65
66 66 "id": <id_given_in_input>,
67 67 "result":
68 68 {
69 69 "pull_request_id": "<pull_request_id>",
70 70 "url": "<url>",
71 71 "title": "<title>",
72 72 "description": "<description>",
73 73 "status" : "<status>",
74 74 "created_on": "<date_time_created>",
75 75 "updated_on": "<date_time_updated>",
76 76 "versions": "<number_or_versions_of_pr>",
77 77 "commit_ids": [
78 78 ...
79 79 "<commit_id>",
80 80 "<commit_id>",
81 81 ...
82 82 ],
83 83 "review_status": "<review_status>",
84 84 "mergeable": {
85 85 "status": "<bool>",
86 86 "message": "<message>",
87 87 },
88 88 "source": {
89 89 "clone_url": "<clone_url>",
90 90 "repository": "<repository_name>",
91 91 "reference":
92 92 {
93 93 "name": "<name>",
94 94 "type": "<type>",
95 95 "commit_id": "<commit_id>",
96 96 }
97 97 },
98 98 "target": {
99 99 "clone_url": "<clone_url>",
100 100 "repository": "<repository_name>",
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 "merge": {
109 109 "clone_url": "<clone_url>",
110 110 "reference":
111 111 {
112 112 "name": "<name>",
113 113 "type": "<type>",
114 114 "commit_id": "<commit_id>",
115 115 }
116 116 },
117 117 "author": <user_obj>,
118 118 "reviewers": [
119 119 ...
120 120 {
121 121 "user": "<user_obj>",
122 122 "review_status": "<review_status>",
123 123 }
124 124 ...
125 125 ]
126 126 },
127 127 "error": null
128 128 """
129 129
130 130 pull_request = get_pull_request_or_error(pullrequestid)
131 131 if Optional.extract(repoid):
132 132 repo = get_repo_or_error(repoid)
133 133 else:
134 134 repo = pull_request.target_repo
135 135
136 136 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
137 137 raise JSONRPCError('repository `%s` or pull request `%s` '
138 138 'does not exist' % (repoid, pullrequestid))
139 139
140 140 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
141 141 # otherwise we can lock the repo on calculation of merge state while update/merge
142 142 # is happening.
143 143 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
144 144 merge_state = Optional.extract(merge_state, binary=True) and pr_created
145 145 data = pull_request.get_api_data(with_merge_state=merge_state)
146 146 return data
147 147
148 148
149 149 @jsonrpc_method()
150 150 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
151 151 merge_state=Optional(False)):
152 152 """
153 153 Get all pull requests from the repository specified in `repoid`.
154 154
155 155 :param apiuser: This is filled automatically from the |authtoken|.
156 156 :type apiuser: AuthUser
157 157 :param repoid: Optional repository name or repository ID.
158 158 :type repoid: str or int
159 159 :param status: Only return pull requests with the specified status.
160 160 Valid options are.
161 161 * ``new`` (default)
162 162 * ``open``
163 163 * ``closed``
164 164 :type status: str
165 165 :param merge_state: Optional calculate merge state for each repository.
166 166 This could result in longer time to fetch the data
167 167 :type merge_state: bool
168 168
169 169 Example output:
170 170
171 171 .. code-block:: bash
172 172
173 173 "id": <id_given_in_input>,
174 174 "result":
175 175 [
176 176 ...
177 177 {
178 178 "pull_request_id": "<pull_request_id>",
179 179 "url": "<url>",
180 180 "title" : "<title>",
181 181 "description": "<description>",
182 182 "status": "<status>",
183 183 "created_on": "<date_time_created>",
184 184 "updated_on": "<date_time_updated>",
185 185 "commit_ids": [
186 186 ...
187 187 "<commit_id>",
188 188 "<commit_id>",
189 189 ...
190 190 ],
191 191 "review_status": "<review_status>",
192 192 "mergeable": {
193 193 "status": "<bool>",
194 194 "message: "<message>",
195 195 },
196 196 "source": {
197 197 "clone_url": "<clone_url>",
198 198 "reference":
199 199 {
200 200 "name": "<name>",
201 201 "type": "<type>",
202 202 "commit_id": "<commit_id>",
203 203 }
204 204 },
205 205 "target": {
206 206 "clone_url": "<clone_url>",
207 207 "reference":
208 208 {
209 209 "name": "<name>",
210 210 "type": "<type>",
211 211 "commit_id": "<commit_id>",
212 212 }
213 213 },
214 214 "merge": {
215 215 "clone_url": "<clone_url>",
216 216 "reference":
217 217 {
218 218 "name": "<name>",
219 219 "type": "<type>",
220 220 "commit_id": "<commit_id>",
221 221 }
222 222 },
223 223 "author": <user_obj>,
224 224 "reviewers": [
225 225 ...
226 226 {
227 227 "user": "<user_obj>",
228 228 "review_status": "<review_status>",
229 229 }
230 230 ...
231 231 ]
232 232 }
233 233 ...
234 234 ],
235 235 "error": null
236 236
237 237 """
238 238 repo = get_repo_or_error(repoid)
239 239 if not has_superadmin_permission(apiuser):
240 240 _perms = (
241 241 'repository.admin', 'repository.write', 'repository.read',)
242 242 validate_repo_permissions(apiuser, repoid, repo, _perms)
243 243
244 244 status = Optional.extract(status)
245 245 merge_state = Optional.extract(merge_state, binary=True)
246 246 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
247 247 order_by='id', order_dir='desc')
248 248 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
249 249 return data
250 250
251 251
252 252 @jsonrpc_method()
253 253 def merge_pull_request(
254 254 request, apiuser, pullrequestid, repoid=Optional(None),
255 255 userid=Optional(OAttr('apiuser'))):
256 256 """
257 257 Merge the pull request specified by `pullrequestid` into its target
258 258 repository.
259 259
260 260 :param apiuser: This is filled automatically from the |authtoken|.
261 261 :type apiuser: AuthUser
262 262 :param repoid: Optional, repository name or repository ID of the
263 263 target repository to which the |pr| is to be merged.
264 264 :type repoid: str or int
265 265 :param pullrequestid: ID of the pull request which shall be merged.
266 266 :type pullrequestid: int
267 267 :param userid: Merge the pull request as this user.
268 268 :type userid: Optional(str or int)
269 269
270 270 Example output:
271 271
272 272 .. code-block:: bash
273 273
274 274 "id": <id_given_in_input>,
275 275 "result": {
276 276 "executed": "<bool>",
277 277 "failure_reason": "<int>",
278 278 "merge_status_message": "<str>",
279 279 "merge_commit_id": "<merge_commit_id>",
280 280 "possible": "<bool>",
281 281 "merge_ref": {
282 282 "commit_id": "<commit_id>",
283 283 "type": "<type>",
284 284 "name": "<name>"
285 285 }
286 286 },
287 287 "error": null
288 288 """
289 289 pull_request = get_pull_request_or_error(pullrequestid)
290 290 if Optional.extract(repoid):
291 291 repo = get_repo_or_error(repoid)
292 292 else:
293 293 repo = pull_request.target_repo
294 294 auth_user = apiuser
295 295 if not isinstance(userid, Optional):
296 296 if (has_superadmin_permission(apiuser) or
297 297 HasRepoPermissionAnyApi('repository.admin')(
298 298 user=apiuser, repo_name=repo.repo_name)):
299 299 apiuser = get_user_or_error(userid)
300 300 auth_user = apiuser.AuthUser()
301 301 else:
302 302 raise JSONRPCError('userid is not the same as your user')
303 303
304 304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
305 305 raise JSONRPCError(
306 306 'Operation forbidden because pull request is in state {}, '
307 307 'only state {} is allowed.'.format(
308 308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
309 309
310 310 with pull_request.set_state(PullRequest.STATE_UPDATING):
311 311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
312 312 translator=request.translate)
313 313 merge_possible = not check.failed
314 314
315 315 if not merge_possible:
316 316 error_messages = []
317 317 for err_type, error_msg in check.errors:
318 318 error_msg = request.translate(error_msg)
319 319 error_messages.append(error_msg)
320 320
321 321 reasons = ','.join(error_messages)
322 322 raise JSONRPCError(
323 323 'merge not possible for following reasons: {}'.format(reasons))
324 324
325 325 target_repo = pull_request.target_repo
326 326 extras = vcs_operation_context(
327 327 request.environ, repo_name=target_repo.repo_name,
328 328 username=auth_user.username, action='push',
329 329 scm=target_repo.repo_type)
330 330 with pull_request.set_state(PullRequest.STATE_UPDATING):
331 331 merge_response = PullRequestModel().merge_repo(
332 332 pull_request, apiuser, extras=extras)
333 333 if merge_response.executed:
334 334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
335 335
336 336 Session().commit()
337 337
338 338 # In previous versions the merge response directly contained the merge
339 339 # commit id. It is now contained in the merge reference object. To be
340 340 # backwards compatible we have to extract it again.
341 341 merge_response = merge_response.asdict()
342 342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
343 343
344 344 return merge_response
345 345
346 346
347 347 @jsonrpc_method()
348 348 def get_pull_request_comments(
349 349 request, apiuser, pullrequestid, repoid=Optional(None)):
350 350 """
351 351 Get all comments of pull request specified with the `pullrequestid`
352 352
353 353 :param apiuser: This is filled automatically from the |authtoken|.
354 354 :type apiuser: AuthUser
355 355 :param repoid: Optional repository name or repository ID.
356 356 :type repoid: str or int
357 357 :param pullrequestid: The pull request ID.
358 358 :type pullrequestid: int
359 359
360 360 Example output:
361 361
362 362 .. code-block:: bash
363 363
364 364 id : <id_given_in_input>
365 365 result : [
366 366 {
367 367 "comment_author": {
368 368 "active": true,
369 369 "full_name_or_username": "Tom Gore",
370 370 "username": "admin"
371 371 },
372 372 "comment_created_on": "2017-01-02T18:43:45.533",
373 373 "comment_f_path": null,
374 374 "comment_id": 25,
375 375 "comment_lineno": null,
376 376 "comment_status": {
377 377 "status": "under_review",
378 378 "status_lbl": "Under Review"
379 379 },
380 380 "comment_text": "Example text",
381 381 "comment_type": null,
382 "pull_request_version": null
382 "pull_request_version": null,
383 "comment_commit_id": None,
384 "comment_pull_request_id": <pull_request_id>
383 385 }
384 386 ],
385 387 error : null
386 388 """
387 389
388 390 pull_request = get_pull_request_or_error(pullrequestid)
389 391 if Optional.extract(repoid):
390 392 repo = get_repo_or_error(repoid)
391 393 else:
392 394 repo = pull_request.target_repo
393 395
394 396 if not PullRequestModel().check_user_read(
395 397 pull_request, apiuser, api=True):
396 398 raise JSONRPCError('repository `%s` or pull request `%s` '
397 399 'does not exist' % (repoid, pullrequestid))
398 400
399 401 (pull_request_latest,
400 402 pull_request_at_ver,
401 403 pull_request_display_obj,
402 404 at_version) = PullRequestModel().get_pr_version(
403 405 pull_request.pull_request_id, version=None)
404 406
405 407 versions = pull_request_display_obj.versions()
406 408 ver_map = {
407 409 ver.pull_request_version_id: cnt
408 410 for cnt, ver in enumerate(versions, 1)
409 411 }
410 412
411 413 # GENERAL COMMENTS with versions #
412 414 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
413 415 q = q.order_by(ChangesetComment.comment_id.asc())
414 416 general_comments = q.all()
415 417
416 418 # INLINE COMMENTS with versions #
417 419 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
418 420 q = q.order_by(ChangesetComment.comment_id.asc())
419 421 inline_comments = q.all()
420 422
421 423 data = []
422 424 for comment in inline_comments + general_comments:
423 425 full_data = comment.get_api_data()
424 426 pr_version_id = None
425 427 if comment.pull_request_version_id:
426 428 pr_version_id = 'v{}'.format(
427 429 ver_map[comment.pull_request_version_id])
428 430
429 431 # sanitize some entries
430 432
431 433 full_data['pull_request_version'] = pr_version_id
432 434 full_data['comment_author'] = {
433 435 'username': full_data['comment_author'].username,
434 436 'full_name_or_username': full_data['comment_author'].full_name_or_username,
435 437 'active': full_data['comment_author'].active,
436 438 }
437 439
438 440 if full_data['comment_status']:
439 441 full_data['comment_status'] = {
440 442 'status': full_data['comment_status'][0].status,
441 443 'status_lbl': full_data['comment_status'][0].status_lbl,
442 444 }
443 445 else:
444 446 full_data['comment_status'] = {}
445 447
446 448 data.append(full_data)
447 449 return data
448 450
449 451
450 452 @jsonrpc_method()
451 453 def comment_pull_request(
452 454 request, apiuser, pullrequestid, repoid=Optional(None),
453 455 message=Optional(None), commit_id=Optional(None), status=Optional(None),
454 456 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
455 457 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
456 458 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
457 459 """
458 460 Comment on the pull request specified with the `pullrequestid`,
459 461 in the |repo| specified by the `repoid`, and optionally change the
460 462 review status.
461 463
462 464 :param apiuser: This is filled automatically from the |authtoken|.
463 465 :type apiuser: AuthUser
464 466 :param repoid: Optional repository name or repository ID.
465 467 :type repoid: str or int
466 468 :param pullrequestid: The pull request ID.
467 469 :type pullrequestid: int
468 470 :param commit_id: Specify the commit_id for which to set a comment. If
469 471 given commit_id is different than latest in the PR status
470 472 change won't be performed.
471 473 :type commit_id: str
472 474 :param message: The text content of the comment.
473 475 :type message: str
474 476 :param status: (**Optional**) Set the approval status of the pull
475 477 request. One of: 'not_reviewed', 'approved', 'rejected',
476 478 'under_review'
477 479 :type status: str
478 480 :param comment_type: Comment type, one of: 'note', 'todo'
479 481 :type comment_type: Optional(str), default: 'note'
480 482 :param resolves_comment_id: id of comment which this one will resolve
481 483 :type resolves_comment_id: Optional(int)
482 484 :param extra_recipients: list of user ids or usernames to add
483 485 notifications for this comment. Acts like a CC for notification
484 486 :type extra_recipients: Optional(list)
485 487 :param userid: Comment on the pull request as this user
486 488 :type userid: Optional(str or int)
487 489 :param send_email: Define if this comment should also send email notification
488 490 :type send_email: Optional(bool)
489 491
490 492 Example output:
491 493
492 494 .. code-block:: bash
493 495
494 496 id : <id_given_in_input>
495 497 result : {
496 498 "pull_request_id": "<Integer>",
497 499 "comment_id": "<Integer>",
498 500 "status": {"given": <given_status>,
499 501 "was_changed": <bool status_was_actually_changed> },
500 502 },
501 503 error : null
502 504 """
503 505 pull_request = get_pull_request_or_error(pullrequestid)
504 506 if Optional.extract(repoid):
505 507 repo = get_repo_or_error(repoid)
506 508 else:
507 509 repo = pull_request.target_repo
508 510
509 511 auth_user = apiuser
510 512 if not isinstance(userid, Optional):
511 513 if (has_superadmin_permission(apiuser) or
512 514 HasRepoPermissionAnyApi('repository.admin')(
513 515 user=apiuser, repo_name=repo.repo_name)):
514 516 apiuser = get_user_or_error(userid)
515 517 auth_user = apiuser.AuthUser()
516 518 else:
517 519 raise JSONRPCError('userid is not the same as your user')
518 520
519 521 if pull_request.is_closed():
520 522 raise JSONRPCError(
521 523 'pull request `%s` comment failed, pull request is closed' % (
522 524 pullrequestid,))
523 525
524 526 if not PullRequestModel().check_user_read(
525 527 pull_request, apiuser, api=True):
526 528 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
527 529 message = Optional.extract(message)
528 530 status = Optional.extract(status)
529 531 commit_id = Optional.extract(commit_id)
530 532 comment_type = Optional.extract(comment_type)
531 533 resolves_comment_id = Optional.extract(resolves_comment_id)
532 534 extra_recipients = Optional.extract(extra_recipients)
533 535 send_email = Optional.extract(send_email, binary=True)
534 536
535 537 if not message and not status:
536 538 raise JSONRPCError(
537 539 'Both message and status parameters are missing. '
538 540 'At least one is required.')
539 541
540 542 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
541 543 status is not None):
542 544 raise JSONRPCError('Unknown comment status: `%s`' % status)
543 545
544 546 if commit_id and commit_id not in pull_request.revisions:
545 547 raise JSONRPCError(
546 548 'Invalid commit_id `%s` for this pull request.' % commit_id)
547 549
548 550 allowed_to_change_status = PullRequestModel().check_user_change_status(
549 551 pull_request, apiuser)
550 552
551 553 # if commit_id is passed re-validated if user is allowed to change status
552 554 # based on latest commit_id from the PR
553 555 if commit_id:
554 556 commit_idx = pull_request.revisions.index(commit_id)
555 557 if commit_idx != 0:
556 558 allowed_to_change_status = False
557 559
558 560 if resolves_comment_id:
559 561 comment = ChangesetComment.get(resolves_comment_id)
560 562 if not comment:
561 563 raise JSONRPCError(
562 564 'Invalid resolves_comment_id `%s` for this pull request.'
563 565 % resolves_comment_id)
564 566 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
565 567 raise JSONRPCError(
566 568 'Comment `%s` is wrong type for setting status to resolved.'
567 569 % resolves_comment_id)
568 570
569 571 text = message
570 572 status_label = ChangesetStatus.get_status_lbl(status)
571 573 if status and allowed_to_change_status:
572 574 st_message = ('Status change %(transition_icon)s %(status)s'
573 575 % {'transition_icon': '>', 'status': status_label})
574 576 text = message or st_message
575 577
576 578 rc_config = SettingsModel().get_all_settings()
577 579 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
578 580
579 581 status_change = status and allowed_to_change_status
580 582 comment = CommentsModel().create(
581 583 text=text,
582 584 repo=pull_request.target_repo.repo_id,
583 585 user=apiuser.user_id,
584 586 pull_request=pull_request.pull_request_id,
585 587 f_path=None,
586 588 line_no=None,
587 589 status_change=(status_label if status_change else None),
588 590 status_change_type=(status if status_change else None),
589 591 closing_pr=False,
590 592 renderer=renderer,
591 593 comment_type=comment_type,
592 594 resolves_comment_id=resolves_comment_id,
593 595 auth_user=auth_user,
594 596 extra_recipients=extra_recipients,
595 597 send_email=send_email
596 598 )
597 599
598 600 if allowed_to_change_status and status:
599 601 old_calculated_status = pull_request.calculated_review_status()
600 602 ChangesetStatusModel().set_status(
601 603 pull_request.target_repo.repo_id,
602 604 status,
603 605 apiuser.user_id,
604 606 comment,
605 607 pull_request=pull_request.pull_request_id
606 608 )
607 609 Session().flush()
608 610
609 611 Session().commit()
610 612
611 613 PullRequestModel().trigger_pull_request_hook(
612 614 pull_request, apiuser, 'comment',
613 615 data={'comment': comment})
614 616
615 617 if allowed_to_change_status and status:
616 618 # we now calculate the status of pull request, and based on that
617 619 # calculation we set the commits status
618 620 calculated_status = pull_request.calculated_review_status()
619 621 if old_calculated_status != calculated_status:
620 622 PullRequestModel().trigger_pull_request_hook(
621 623 pull_request, apiuser, 'review_status_change',
622 624 data={'status': calculated_status})
623 625
624 626 data = {
625 627 'pull_request_id': pull_request.pull_request_id,
626 628 'comment_id': comment.comment_id if comment else None,
627 629 'status': {'given': status, 'was_changed': status_change},
628 630 }
629 631 return data
630 632
631 633
632 634 @jsonrpc_method()
633 635 def create_pull_request(
634 636 request, apiuser, source_repo, target_repo, source_ref, target_ref,
635 637 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
636 638 description_renderer=Optional(''), reviewers=Optional(None)):
637 639 """
638 640 Creates a new pull request.
639 641
640 642 Accepts refs in the following formats:
641 643
642 644 * branch:<branch_name>:<sha>
643 645 * branch:<branch_name>
644 646 * bookmark:<bookmark_name>:<sha> (Mercurial only)
645 647 * bookmark:<bookmark_name> (Mercurial only)
646 648
647 649 :param apiuser: This is filled automatically from the |authtoken|.
648 650 :type apiuser: AuthUser
649 651 :param source_repo: Set the source repository name.
650 652 :type source_repo: str
651 653 :param target_repo: Set the target repository name.
652 654 :type target_repo: str
653 655 :param source_ref: Set the source ref name.
654 656 :type source_ref: str
655 657 :param target_ref: Set the target ref name.
656 658 :type target_ref: str
657 659 :param owner: user_id or username
658 660 :type owner: Optional(str)
659 661 :param title: Optionally Set the pull request title, it's generated otherwise
660 662 :type title: str
661 663 :param description: Set the pull request description.
662 664 :type description: Optional(str)
663 665 :type description_renderer: Optional(str)
664 666 :param description_renderer: Set pull request renderer for the description.
665 667 It should be 'rst', 'markdown' or 'plain'. If not give default
666 668 system renderer will be used
667 669 :param reviewers: Set the new pull request reviewers list.
668 670 Reviewer defined by review rules will be added automatically to the
669 671 defined list.
670 672 :type reviewers: Optional(list)
671 673 Accepts username strings or objects of the format:
672 674
673 675 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
674 676 """
675 677
676 678 source_db_repo = get_repo_or_error(source_repo)
677 679 target_db_repo = get_repo_or_error(target_repo)
678 680 if not has_superadmin_permission(apiuser):
679 681 _perms = ('repository.admin', 'repository.write', 'repository.read',)
680 682 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
681 683
682 684 owner = validate_set_owner_permissions(apiuser, owner)
683 685
684 686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
685 687 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
686 688
687 689 source_scm = source_db_repo.scm_instance()
688 690 target_scm = target_db_repo.scm_instance()
689 691
690 692 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
691 693 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
692 694
693 695 ancestor = source_scm.get_common_ancestor(
694 696 source_commit.raw_id, target_commit.raw_id, target_scm)
695 697 if not ancestor:
696 698 raise JSONRPCError('no common ancestor found')
697 699
698 700 # recalculate target ref based on ancestor
699 701 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
700 702 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
701 703
702 704 commit_ranges = target_scm.compare(
703 705 target_commit.raw_id, source_commit.raw_id, source_scm,
704 706 merge=True, pre_load=[])
705 707
706 708 if not commit_ranges:
707 709 raise JSONRPCError('no commits found')
708 710
709 711 reviewer_objects = Optional.extract(reviewers) or []
710 712
711 713 # serialize and validate passed in given reviewers
712 714 if reviewer_objects:
713 715 schema = ReviewerListSchema()
714 716 try:
715 717 reviewer_objects = schema.deserialize(reviewer_objects)
716 718 except Invalid as err:
717 719 raise JSONRPCValidationError(colander_exc=err)
718 720
719 721 # validate users
720 722 for reviewer_object in reviewer_objects:
721 723 user = get_user_or_error(reviewer_object['username'])
722 724 reviewer_object['user_id'] = user.user_id
723 725
724 726 get_default_reviewers_data, validate_default_reviewers = \
725 727 PullRequestModel().get_reviewer_functions()
726 728
727 729 # recalculate reviewers logic, to make sure we can validate this
728 730 reviewer_rules = get_default_reviewers_data(
729 731 owner, source_db_repo,
730 732 source_commit, target_db_repo, target_commit)
731 733
732 734 # now MERGE our given with the calculated
733 735 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
734 736
735 737 try:
736 738 reviewers = validate_default_reviewers(
737 739 reviewer_objects, reviewer_rules)
738 740 except ValueError as e:
739 741 raise JSONRPCError('Reviewers Validation: {}'.format(e))
740 742
741 743 title = Optional.extract(title)
742 744 if not title:
743 745 title_source_ref = source_ref.split(':', 2)[1]
744 746 title = PullRequestModel().generate_pullrequest_title(
745 747 source=source_repo,
746 748 source_ref=title_source_ref,
747 749 target=target_repo
748 750 )
749 751 # fetch renderer, if set fallback to plain in case of PR
750 752 rc_config = SettingsModel().get_all_settings()
751 753 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
752 754 description = Optional.extract(description)
753 755 description_renderer = Optional.extract(description_renderer) or default_system_renderer
754 756
755 757 pull_request = PullRequestModel().create(
756 758 created_by=owner.user_id,
757 759 source_repo=source_repo,
758 760 source_ref=full_source_ref,
759 761 target_repo=target_repo,
760 762 target_ref=full_target_ref,
761 763 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
762 764 reviewers=reviewers,
763 765 title=title,
764 766 description=description,
765 767 description_renderer=description_renderer,
766 768 reviewer_data=reviewer_rules,
767 769 auth_user=apiuser
768 770 )
769 771
770 772 Session().commit()
771 773 data = {
772 774 'msg': 'Created new pull request `{}`'.format(title),
773 775 'pull_request_id': pull_request.pull_request_id,
774 776 }
775 777 return data
776 778
777 779
778 780 @jsonrpc_method()
779 781 def update_pull_request(
780 782 request, apiuser, pullrequestid, repoid=Optional(None),
781 783 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
782 784 reviewers=Optional(None), update_commits=Optional(None)):
783 785 """
784 786 Updates a pull request.
785 787
786 788 :param apiuser: This is filled automatically from the |authtoken|.
787 789 :type apiuser: AuthUser
788 790 :param repoid: Optional repository name or repository ID.
789 791 :type repoid: str or int
790 792 :param pullrequestid: The pull request ID.
791 793 :type pullrequestid: int
792 794 :param title: Set the pull request title.
793 795 :type title: str
794 796 :param description: Update pull request description.
795 797 :type description: Optional(str)
796 798 :type description_renderer: Optional(str)
797 799 :param description_renderer: Update pull request renderer for the description.
798 800 It should be 'rst', 'markdown' or 'plain'
799 801 :param reviewers: Update pull request reviewers list with new value.
800 802 :type reviewers: Optional(list)
801 803 Accepts username strings or objects of the format:
802 804
803 805 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
804 806
805 807 :param update_commits: Trigger update of commits for this pull request
806 808 :type: update_commits: Optional(bool)
807 809
808 810 Example output:
809 811
810 812 .. code-block:: bash
811 813
812 814 id : <id_given_in_input>
813 815 result : {
814 816 "msg": "Updated pull request `63`",
815 817 "pull_request": <pull_request_object>,
816 818 "updated_reviewers": {
817 819 "added": [
818 820 "username"
819 821 ],
820 822 "removed": []
821 823 },
822 824 "updated_commits": {
823 825 "added": [
824 826 "<sha1_hash>"
825 827 ],
826 828 "common": [
827 829 "<sha1_hash>",
828 830 "<sha1_hash>",
829 831 ],
830 832 "removed": []
831 833 }
832 834 }
833 835 error : null
834 836 """
835 837
836 838 pull_request = get_pull_request_or_error(pullrequestid)
837 839 if Optional.extract(repoid):
838 840 repo = get_repo_or_error(repoid)
839 841 else:
840 842 repo = pull_request.target_repo
841 843
842 844 if not PullRequestModel().check_user_update(
843 845 pull_request, apiuser, api=True):
844 846 raise JSONRPCError(
845 847 'pull request `%s` update failed, no permission to update.' % (
846 848 pullrequestid,))
847 849 if pull_request.is_closed():
848 850 raise JSONRPCError(
849 851 'pull request `%s` update failed, pull request is closed' % (
850 852 pullrequestid,))
851 853
852 854 reviewer_objects = Optional.extract(reviewers) or []
853 855
854 856 if reviewer_objects:
855 857 schema = ReviewerListSchema()
856 858 try:
857 859 reviewer_objects = schema.deserialize(reviewer_objects)
858 860 except Invalid as err:
859 861 raise JSONRPCValidationError(colander_exc=err)
860 862
861 863 # validate users
862 864 for reviewer_object in reviewer_objects:
863 865 user = get_user_or_error(reviewer_object['username'])
864 866 reviewer_object['user_id'] = user.user_id
865 867
866 868 get_default_reviewers_data, get_validated_reviewers = \
867 869 PullRequestModel().get_reviewer_functions()
868 870
869 871 # re-use stored rules
870 872 reviewer_rules = pull_request.reviewer_data
871 873 try:
872 874 reviewers = get_validated_reviewers(
873 875 reviewer_objects, reviewer_rules)
874 876 except ValueError as e:
875 877 raise JSONRPCError('Reviewers Validation: {}'.format(e))
876 878 else:
877 879 reviewers = []
878 880
879 881 title = Optional.extract(title)
880 882 description = Optional.extract(description)
881 883 description_renderer = Optional.extract(description_renderer)
882 884
883 885 if title or description:
884 886 PullRequestModel().edit(
885 887 pull_request,
886 888 title or pull_request.title,
887 889 description or pull_request.description,
888 890 description_renderer or pull_request.description_renderer,
889 891 apiuser)
890 892 Session().commit()
891 893
892 894 commit_changes = {"added": [], "common": [], "removed": []}
893 895 if str2bool(Optional.extract(update_commits)):
894 896
895 897 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
896 898 raise JSONRPCError(
897 899 'Operation forbidden because pull request is in state {}, '
898 900 'only state {} is allowed.'.format(
899 901 pull_request.pull_request_state, PullRequest.STATE_CREATED))
900 902
901 903 with pull_request.set_state(PullRequest.STATE_UPDATING):
902 904 if PullRequestModel().has_valid_update_type(pull_request):
903 905 db_user = apiuser.get_instance()
904 906 update_response = PullRequestModel().update_commits(
905 907 pull_request, db_user)
906 908 commit_changes = update_response.changes or commit_changes
907 909 Session().commit()
908 910
909 911 reviewers_changes = {"added": [], "removed": []}
910 912 if reviewers:
911 913 old_calculated_status = pull_request.calculated_review_status()
912 914 added_reviewers, removed_reviewers = \
913 915 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
914 916
915 917 reviewers_changes['added'] = sorted(
916 918 [get_user_or_error(n).username for n in added_reviewers])
917 919 reviewers_changes['removed'] = sorted(
918 920 [get_user_or_error(n).username for n in removed_reviewers])
919 921 Session().commit()
920 922
921 923 # trigger status changed if change in reviewers changes the status
922 924 calculated_status = pull_request.calculated_review_status()
923 925 if old_calculated_status != calculated_status:
924 926 PullRequestModel().trigger_pull_request_hook(
925 927 pull_request, apiuser, 'review_status_change',
926 928 data={'status': calculated_status})
927 929
928 930 data = {
929 931 'msg': 'Updated pull request `{}`'.format(
930 932 pull_request.pull_request_id),
931 933 'pull_request': pull_request.get_api_data(),
932 934 'updated_commits': commit_changes,
933 935 'updated_reviewers': reviewers_changes
934 936 }
935 937
936 938 return data
937 939
938 940
939 941 @jsonrpc_method()
940 942 def close_pull_request(
941 943 request, apiuser, pullrequestid, repoid=Optional(None),
942 944 userid=Optional(OAttr('apiuser')), message=Optional('')):
943 945 """
944 946 Close the pull request specified by `pullrequestid`.
945 947
946 948 :param apiuser: This is filled automatically from the |authtoken|.
947 949 :type apiuser: AuthUser
948 950 :param repoid: Repository name or repository ID to which the pull
949 951 request belongs.
950 952 :type repoid: str or int
951 953 :param pullrequestid: ID of the pull request to be closed.
952 954 :type pullrequestid: int
953 955 :param userid: Close the pull request as this user.
954 956 :type userid: Optional(str or int)
955 957 :param message: Optional message to close the Pull Request with. If not
956 958 specified it will be generated automatically.
957 959 :type message: Optional(str)
958 960
959 961 Example output:
960 962
961 963 .. code-block:: bash
962 964
963 965 "id": <id_given_in_input>,
964 966 "result": {
965 967 "pull_request_id": "<int>",
966 968 "close_status": "<str:status_lbl>,
967 969 "closed": "<bool>"
968 970 },
969 971 "error": null
970 972
971 973 """
972 974 _ = request.translate
973 975
974 976 pull_request = get_pull_request_or_error(pullrequestid)
975 977 if Optional.extract(repoid):
976 978 repo = get_repo_or_error(repoid)
977 979 else:
978 980 repo = pull_request.target_repo
979 981
980 982 if not isinstance(userid, Optional):
981 983 if (has_superadmin_permission(apiuser) or
982 984 HasRepoPermissionAnyApi('repository.admin')(
983 985 user=apiuser, repo_name=repo.repo_name)):
984 986 apiuser = get_user_or_error(userid)
985 987 else:
986 988 raise JSONRPCError('userid is not the same as your user')
987 989
988 990 if pull_request.is_closed():
989 991 raise JSONRPCError(
990 992 'pull request `%s` is already closed' % (pullrequestid,))
991 993
992 994 # only owner or admin or person with write permissions
993 995 allowed_to_close = PullRequestModel().check_user_update(
994 996 pull_request, apiuser, api=True)
995 997
996 998 if not allowed_to_close:
997 999 raise JSONRPCError(
998 1000 'pull request `%s` close failed, no permission to close.' % (
999 1001 pullrequestid,))
1000 1002
1001 1003 # message we're using to close the PR, else it's automatically generated
1002 1004 message = Optional.extract(message)
1003 1005
1004 1006 # finally close the PR, with proper message comment
1005 1007 comment, status = PullRequestModel().close_pull_request_with_comment(
1006 1008 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1007 1009 status_lbl = ChangesetStatus.get_status_lbl(status)
1008 1010
1009 1011 Session().commit()
1010 1012
1011 1013 data = {
1012 1014 'pull_request_id': pull_request.pull_request_id,
1013 1015 'close_status': status_lbl,
1014 1016 'closed': True,
1015 1017 }
1016 1018 return data
@@ -1,2343 +1,2343 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import time
23 23
24 24 import rhodecode
25 25 from rhodecode.api import (
26 26 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
27 27 from rhodecode.api.utils import (
28 28 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger, rc_cache
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
35 35 from rhodecode.lib.celerylib.utils import get_task_id
36 36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 39 from rhodecode.lib.vcs import RepositoryError
40 40 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
41 41 from rhodecode.model.changeset_status import ChangesetStatusModel
42 42 from rhodecode.model.comment import CommentsModel
43 43 from rhodecode.model.db import (
44 44 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
45 45 ChangesetComment)
46 46 from rhodecode.model.permission import PermissionModel
47 47 from rhodecode.model.repo import RepoModel
48 48 from rhodecode.model.scm import ScmModel, RepoList
49 49 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
50 50 from rhodecode.model import validation_schema
51 51 from rhodecode.model.validation_schema.schemas import repo_schema
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 @jsonrpc_method()
57 57 def get_repo(request, apiuser, repoid, cache=Optional(True)):
58 58 """
59 59 Gets an existing repository by its name or repository_id.
60 60
61 61 The members section so the output returns users groups or users
62 62 associated with that repository.
63 63
64 64 This command can only be run using an |authtoken| with admin rights,
65 65 or users with at least read rights to the |repo|.
66 66
67 67 :param apiuser: This is filled automatically from the |authtoken|.
68 68 :type apiuser: AuthUser
69 69 :param repoid: The repository name or repository id.
70 70 :type repoid: str or int
71 71 :param cache: use the cached value for last changeset
72 72 :type: cache: Optional(bool)
73 73
74 74 Example output:
75 75
76 76 .. code-block:: bash
77 77
78 78 {
79 79 "error": null,
80 80 "id": <repo_id>,
81 81 "result": {
82 82 "clone_uri": null,
83 83 "created_on": "timestamp",
84 84 "description": "repo description",
85 85 "enable_downloads": false,
86 86 "enable_locking": false,
87 87 "enable_statistics": false,
88 88 "followers": [
89 89 {
90 90 "active": true,
91 91 "admin": false,
92 92 "api_key": "****************************************",
93 93 "api_keys": [
94 94 "****************************************"
95 95 ],
96 96 "email": "user@example.com",
97 97 "emails": [
98 98 "user@example.com"
99 99 ],
100 100 "extern_name": "rhodecode",
101 101 "extern_type": "rhodecode",
102 102 "firstname": "username",
103 103 "ip_addresses": [],
104 104 "language": null,
105 105 "last_login": "2015-09-16T17:16:35.854",
106 106 "lastname": "surname",
107 107 "user_id": <user_id>,
108 108 "username": "name"
109 109 }
110 110 ],
111 111 "fork_of": "parent-repo",
112 112 "landing_rev": [
113 113 "rev",
114 114 "tip"
115 115 ],
116 116 "last_changeset": {
117 117 "author": "User <user@example.com>",
118 118 "branch": "default",
119 119 "date": "timestamp",
120 120 "message": "last commit message",
121 121 "parents": [
122 122 {
123 123 "raw_id": "commit-id"
124 124 }
125 125 ],
126 126 "raw_id": "commit-id",
127 127 "revision": <revision number>,
128 128 "short_id": "short id"
129 129 },
130 130 "lock_reason": null,
131 131 "locked_by": null,
132 132 "locked_date": null,
133 133 "owner": "owner-name",
134 134 "permissions": [
135 135 {
136 136 "name": "super-admin-name",
137 137 "origin": "super-admin",
138 138 "permission": "repository.admin",
139 139 "type": "user"
140 140 },
141 141 {
142 142 "name": "owner-name",
143 143 "origin": "owner",
144 144 "permission": "repository.admin",
145 145 "type": "user"
146 146 },
147 147 {
148 148 "name": "user-group-name",
149 149 "origin": "permission",
150 150 "permission": "repository.write",
151 151 "type": "user_group"
152 152 }
153 153 ],
154 154 "private": true,
155 155 "repo_id": 676,
156 156 "repo_name": "user-group/repo-name",
157 157 "repo_type": "hg"
158 158 }
159 159 }
160 160 """
161 161
162 162 repo = get_repo_or_error(repoid)
163 163 cache = Optional.extract(cache)
164 164
165 165 include_secrets = False
166 166 if has_superadmin_permission(apiuser):
167 167 include_secrets = True
168 168 else:
169 169 # check if we have at least read permission for this repo !
170 170 _perms = (
171 171 'repository.admin', 'repository.write', 'repository.read',)
172 172 validate_repo_permissions(apiuser, repoid, repo, _perms)
173 173
174 174 permissions = []
175 175 for _user in repo.permissions():
176 176 user_data = {
177 177 'name': _user.username,
178 178 'permission': _user.permission,
179 179 'origin': get_origin(_user),
180 180 'type': "user",
181 181 }
182 182 permissions.append(user_data)
183 183
184 184 for _user_group in repo.permission_user_groups():
185 185 user_group_data = {
186 186 'name': _user_group.users_group_name,
187 187 'permission': _user_group.permission,
188 188 'origin': get_origin(_user_group),
189 189 'type': "user_group",
190 190 }
191 191 permissions.append(user_group_data)
192 192
193 193 following_users = [
194 194 user.user.get_api_data(include_secrets=include_secrets)
195 195 for user in repo.followers]
196 196
197 197 if not cache:
198 198 repo.update_commit_cache()
199 199 data = repo.get_api_data(include_secrets=include_secrets)
200 200 data['permissions'] = permissions
201 201 data['followers'] = following_users
202 202 return data
203 203
204 204
205 205 @jsonrpc_method()
206 206 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
207 207 """
208 208 Lists all existing repositories.
209 209
210 210 This command can only be run using an |authtoken| with admin rights,
211 211 or users with at least read rights to |repos|.
212 212
213 213 :param apiuser: This is filled automatically from the |authtoken|.
214 214 :type apiuser: AuthUser
215 215 :param root: specify root repository group to fetch repositories.
216 216 filters the returned repositories to be members of given root group.
217 217 :type root: Optional(None)
218 218 :param traverse: traverse given root into subrepositories. With this flag
219 219 set to False, it will only return top-level repositories from `root`.
220 220 if root is empty it will return just top-level repositories.
221 221 :type traverse: Optional(True)
222 222
223 223
224 224 Example output:
225 225
226 226 .. code-block:: bash
227 227
228 228 id : <id_given_in_input>
229 229 result: [
230 230 {
231 231 "repo_id" : "<repo_id>",
232 232 "repo_name" : "<reponame>"
233 233 "repo_type" : "<repo_type>",
234 234 "clone_uri" : "<clone_uri>",
235 235 "private": : "<bool>",
236 236 "created_on" : "<datetimecreated>",
237 237 "description" : "<description>",
238 238 "landing_rev": "<landing_rev>",
239 239 "owner": "<repo_owner>",
240 240 "fork_of": "<name_of_fork_parent>",
241 241 "enable_downloads": "<bool>",
242 242 "enable_locking": "<bool>",
243 243 "enable_statistics": "<bool>",
244 244 },
245 245 ...
246 246 ]
247 247 error: null
248 248 """
249 249
250 250 include_secrets = has_superadmin_permission(apiuser)
251 251 _perms = ('repository.read', 'repository.write', 'repository.admin',)
252 252 extras = {'user': apiuser}
253 253
254 254 root = Optional.extract(root)
255 255 traverse = Optional.extract(traverse, binary=True)
256 256
257 257 if root:
258 258 # verify parent existance, if it's empty return an error
259 259 parent = RepoGroup.get_by_group_name(root)
260 260 if not parent:
261 261 raise JSONRPCError(
262 262 'Root repository group `{}` does not exist'.format(root))
263 263
264 264 if traverse:
265 265 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
266 266 else:
267 267 repos = RepoModel().get_repos_for_root(root=parent)
268 268 else:
269 269 if traverse:
270 270 repos = RepoModel().get_all()
271 271 else:
272 272 # return just top-level
273 273 repos = RepoModel().get_repos_for_root(root=None)
274 274
275 275 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
276 276 return [repo.get_api_data(include_secrets=include_secrets)
277 277 for repo in repo_list]
278 278
279 279
280 280 @jsonrpc_method()
281 281 def get_repo_changeset(request, apiuser, repoid, revision,
282 282 details=Optional('basic')):
283 283 """
284 284 Returns information about a changeset.
285 285
286 286 Additionally parameters define the amount of details returned by
287 287 this function.
288 288
289 289 This command can only be run using an |authtoken| with admin rights,
290 290 or users with at least read rights to the |repo|.
291 291
292 292 :param apiuser: This is filled automatically from the |authtoken|.
293 293 :type apiuser: AuthUser
294 294 :param repoid: The repository name or repository id
295 295 :type repoid: str or int
296 296 :param revision: revision for which listing should be done
297 297 :type revision: str
298 298 :param details: details can be 'basic|extended|full' full gives diff
299 299 info details like the diff itself, and number of changed files etc.
300 300 :type details: Optional(str)
301 301
302 302 """
303 303 repo = get_repo_or_error(repoid)
304 304 if not has_superadmin_permission(apiuser):
305 305 _perms = (
306 306 'repository.admin', 'repository.write', 'repository.read',)
307 307 validate_repo_permissions(apiuser, repoid, repo, _perms)
308 308
309 309 changes_details = Optional.extract(details)
310 310 _changes_details_types = ['basic', 'extended', 'full']
311 311 if changes_details not in _changes_details_types:
312 312 raise JSONRPCError(
313 313 'ret_type must be one of %s' % (
314 314 ','.join(_changes_details_types)))
315 315
316 316 pre_load = ['author', 'branch', 'date', 'message', 'parents',
317 317 'status', '_commit', '_file_paths']
318 318
319 319 try:
320 320 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
321 321 except TypeError as e:
322 322 raise JSONRPCError(safe_str(e))
323 323 _cs_json = cs.__json__()
324 324 _cs_json['diff'] = build_commit_data(cs, changes_details)
325 325 if changes_details == 'full':
326 326 _cs_json['refs'] = cs._get_refs()
327 327 return _cs_json
328 328
329 329
330 330 @jsonrpc_method()
331 331 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
332 332 details=Optional('basic')):
333 333 """
334 334 Returns a set of commits limited by the number starting
335 335 from the `start_rev` option.
336 336
337 337 Additional parameters define the amount of details returned by this
338 338 function.
339 339
340 340 This command can only be run using an |authtoken| with admin rights,
341 341 or users with at least read rights to |repos|.
342 342
343 343 :param apiuser: This is filled automatically from the |authtoken|.
344 344 :type apiuser: AuthUser
345 345 :param repoid: The repository name or repository ID.
346 346 :type repoid: str or int
347 347 :param start_rev: The starting revision from where to get changesets.
348 348 :type start_rev: str
349 349 :param limit: Limit the number of commits to this amount
350 350 :type limit: str or int
351 351 :param details: Set the level of detail returned. Valid option are:
352 352 ``basic``, ``extended`` and ``full``.
353 353 :type details: Optional(str)
354 354
355 355 .. note::
356 356
357 357 Setting the parameter `details` to the value ``full`` is extensive
358 358 and returns details like the diff itself, and the number
359 359 of changed files.
360 360
361 361 """
362 362 repo = get_repo_or_error(repoid)
363 363 if not has_superadmin_permission(apiuser):
364 364 _perms = (
365 365 'repository.admin', 'repository.write', 'repository.read',)
366 366 validate_repo_permissions(apiuser, repoid, repo, _perms)
367 367
368 368 changes_details = Optional.extract(details)
369 369 _changes_details_types = ['basic', 'extended', 'full']
370 370 if changes_details not in _changes_details_types:
371 371 raise JSONRPCError(
372 372 'ret_type must be one of %s' % (
373 373 ','.join(_changes_details_types)))
374 374
375 375 limit = int(limit)
376 376 pre_load = ['author', 'branch', 'date', 'message', 'parents',
377 377 'status', '_commit', '_file_paths']
378 378
379 379 vcs_repo = repo.scm_instance()
380 380 # SVN needs a special case to distinguish its index and commit id
381 381 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
382 382 start_rev = vcs_repo.commit_ids[0]
383 383
384 384 try:
385 385 commits = vcs_repo.get_commits(
386 386 start_id=start_rev, pre_load=pre_load, translate_tags=False)
387 387 except TypeError as e:
388 388 raise JSONRPCError(safe_str(e))
389 389 except Exception:
390 390 log.exception('Fetching of commits failed')
391 391 raise JSONRPCError('Error occurred during commit fetching')
392 392
393 393 ret = []
394 394 for cnt, commit in enumerate(commits):
395 395 if cnt >= limit != -1:
396 396 break
397 397 _cs_json = commit.__json__()
398 398 _cs_json['diff'] = build_commit_data(commit, changes_details)
399 399 if changes_details == 'full':
400 400 _cs_json['refs'] = {
401 401 'branches': [commit.branch],
402 402 'bookmarks': getattr(commit, 'bookmarks', []),
403 403 'tags': commit.tags
404 404 }
405 405 ret.append(_cs_json)
406 406 return ret
407 407
408 408
409 409 @jsonrpc_method()
410 410 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
411 411 ret_type=Optional('all'), details=Optional('basic'),
412 412 max_file_bytes=Optional(None)):
413 413 """
414 414 Returns a list of nodes and children in a flat list for a given
415 415 path at given revision.
416 416
417 417 It's possible to specify ret_type to show only `files` or `dirs`.
418 418
419 419 This command can only be run using an |authtoken| with admin rights,
420 420 or users with at least read rights to |repos|.
421 421
422 422 :param apiuser: This is filled automatically from the |authtoken|.
423 423 :type apiuser: AuthUser
424 424 :param repoid: The repository name or repository ID.
425 425 :type repoid: str or int
426 426 :param revision: The revision for which listing should be done.
427 427 :type revision: str
428 428 :param root_path: The path from which to start displaying.
429 429 :type root_path: str
430 430 :param ret_type: Set the return type. Valid options are
431 431 ``all`` (default), ``files`` and ``dirs``.
432 432 :type ret_type: Optional(str)
433 433 :param details: Returns extended information about nodes, such as
434 434 md5, binary, and or content.
435 435 The valid options are ``basic`` and ``full``.
436 436 :type details: Optional(str)
437 437 :param max_file_bytes: Only return file content under this file size bytes
438 438 :type details: Optional(int)
439 439
440 440 Example output:
441 441
442 442 .. code-block:: bash
443 443
444 444 id : <id_given_in_input>
445 445 result: [
446 446 {
447 447 "binary": false,
448 448 "content": "File line",
449 449 "extension": "md",
450 450 "lines": 2,
451 451 "md5": "059fa5d29b19c0657e384749480f6422",
452 452 "mimetype": "text/x-minidsrc",
453 453 "name": "file.md",
454 454 "size": 580,
455 455 "type": "file"
456 456 },
457 457 ...
458 458 ]
459 459 error: null
460 460 """
461 461
462 462 repo = get_repo_or_error(repoid)
463 463 if not has_superadmin_permission(apiuser):
464 464 _perms = ('repository.admin', 'repository.write', 'repository.read',)
465 465 validate_repo_permissions(apiuser, repoid, repo, _perms)
466 466
467 467 ret_type = Optional.extract(ret_type)
468 468 details = Optional.extract(details)
469 469 _extended_types = ['basic', 'full']
470 470 if details not in _extended_types:
471 471 raise JSONRPCError('ret_type must be one of %s' % (','.join(_extended_types)))
472 472 extended_info = False
473 473 content = False
474 474 if details == 'basic':
475 475 extended_info = True
476 476
477 477 if details == 'full':
478 478 extended_info = content = True
479 479
480 480 _map = {}
481 481 try:
482 482 # check if repo is not empty by any chance, skip quicker if it is.
483 483 _scm = repo.scm_instance()
484 484 if _scm.is_empty():
485 485 return []
486 486
487 487 _d, _f = ScmModel().get_nodes(
488 488 repo, revision, root_path, flat=False,
489 489 extended_info=extended_info, content=content,
490 490 max_file_bytes=max_file_bytes)
491 491 _map = {
492 492 'all': _d + _f,
493 493 'files': _f,
494 494 'dirs': _d,
495 495 }
496 496 return _map[ret_type]
497 497 except KeyError:
498 498 raise JSONRPCError(
499 499 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
500 500 except Exception:
501 501 log.exception("Exception occurred while trying to get repo nodes")
502 502 raise JSONRPCError(
503 503 'failed to get repo: `%s` nodes' % repo.repo_name
504 504 )
505 505
506 506
507 507 @jsonrpc_method()
508 508 def get_repo_file(request, apiuser, repoid, commit_id, file_path,
509 509 max_file_bytes=Optional(None), details=Optional('basic'),
510 510 cache=Optional(True)):
511 511 """
512 512 Returns a single file from repository at given revision.
513 513
514 514 This command can only be run using an |authtoken| with admin rights,
515 515 or users with at least read rights to |repos|.
516 516
517 517 :param apiuser: This is filled automatically from the |authtoken|.
518 518 :type apiuser: AuthUser
519 519 :param repoid: The repository name or repository ID.
520 520 :type repoid: str or int
521 521 :param commit_id: The revision for which listing should be done.
522 522 :type commit_id: str
523 523 :param file_path: The path from which to start displaying.
524 524 :type file_path: str
525 525 :param details: Returns different set of information about nodes.
526 526 The valid options are ``minimal`` ``basic`` and ``full``.
527 527 :type details: Optional(str)
528 528 :param max_file_bytes: Only return file content under this file size bytes
529 529 :type max_file_bytes: Optional(int)
530 530 :param cache: Use internal caches for fetching files. If disabled fetching
531 531 files is slower but more memory efficient
532 532 :type cache: Optional(bool)
533 533
534 534 Example output:
535 535
536 536 .. code-block:: bash
537 537
538 538 id : <id_given_in_input>
539 539 result: {
540 540 "binary": false,
541 541 "extension": "py",
542 542 "lines": 35,
543 543 "content": "....",
544 544 "md5": "76318336366b0f17ee249e11b0c99c41",
545 545 "mimetype": "text/x-python",
546 546 "name": "python.py",
547 547 "size": 817,
548 548 "type": "file",
549 549 }
550 550 error: null
551 551 """
552 552
553 553 repo = get_repo_or_error(repoid)
554 554 if not has_superadmin_permission(apiuser):
555 555 _perms = ('repository.admin', 'repository.write', 'repository.read',)
556 556 validate_repo_permissions(apiuser, repoid, repo, _perms)
557 557
558 558 cache = Optional.extract(cache, binary=True)
559 559 details = Optional.extract(details)
560 560 _extended_types = ['minimal', 'minimal+search', 'basic', 'full']
561 561 if details not in _extended_types:
562 562 raise JSONRPCError(
563 563 'ret_type must be one of %s, got %s' % (','.join(_extended_types)), details)
564 564 extended_info = False
565 565 content = False
566 566
567 567 if details == 'minimal':
568 568 extended_info = False
569 569
570 570 elif details == 'basic':
571 571 extended_info = True
572 572
573 573 elif details == 'full':
574 574 extended_info = content = True
575 575
576 576 try:
577 577 # check if repo is not empty by any chance, skip quicker if it is.
578 578 _scm = repo.scm_instance()
579 579 if _scm.is_empty():
580 580 return None
581 581
582 582 node = ScmModel().get_node(
583 583 repo, commit_id, file_path, extended_info=extended_info,
584 584 content=content, max_file_bytes=max_file_bytes, cache=cache)
585 585 except NodeDoesNotExistError:
586 586 raise JSONRPCError('There is no file in repo: `{}` at path `{}` for commit: `{}`'.format(
587 587 repo.repo_name, file_path, commit_id))
588 588 except Exception:
589 589 log.exception("Exception occurred while trying to get repo %s file",
590 590 repo.repo_name)
591 591 raise JSONRPCError('failed to get repo: `{}` file at path {}'.format(
592 592 repo.repo_name, file_path))
593 593
594 594 return node
595 595
596 596
597 597 @jsonrpc_method()
598 598 def get_repo_fts_tree(request, apiuser, repoid, commit_id, root_path):
599 599 """
600 600 Returns a list of tree nodes for path at given revision. This api is built
601 601 strictly for usage in full text search building, and shouldn't be consumed
602 602
603 603 This command can only be run using an |authtoken| with admin rights,
604 604 or users with at least read rights to |repos|.
605 605
606 606 """
607 607
608 608 repo = get_repo_or_error(repoid)
609 609 if not has_superadmin_permission(apiuser):
610 610 _perms = ('repository.admin', 'repository.write', 'repository.read',)
611 611 validate_repo_permissions(apiuser, repoid, repo, _perms)
612 612
613 613 repo_id = repo.repo_id
614 614 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
615 615 cache_on = cache_seconds > 0
616 616
617 617 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
618 618 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
619 619
620 620 def compute_fts_tree(cache_ver, repo_id, commit_id, root_path):
621 621 return ScmModel().get_fts_data(repo_id, commit_id, root_path)
622 622
623 623 try:
624 624 # check if repo is not empty by any chance, skip quicker if it is.
625 625 _scm = repo.scm_instance()
626 626 if _scm.is_empty():
627 627 return []
628 628 except RepositoryError:
629 629 log.exception("Exception occurred while trying to get repo nodes")
630 630 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
631 631
632 632 try:
633 633 # we need to resolve commit_id to a FULL sha for cache to work correctly.
634 634 # sending 'master' is a pointer that needs to be translated to current commit.
635 635 commit_id = _scm.get_commit(commit_id=commit_id).raw_id
636 636 log.debug(
637 637 'Computing FTS REPO TREE for repo_id %s commit_id `%s` '
638 638 'with caching: %s[TTL: %ss]' % (
639 639 repo_id, commit_id, cache_on, cache_seconds or 0))
640 640
641 641 tree_files = compute_fts_tree(rc_cache.FILE_TREE_CACHE_VER, repo_id, commit_id, root_path)
642 642 return tree_files
643 643
644 644 except Exception:
645 645 log.exception("Exception occurred while trying to get repo nodes")
646 646 raise JSONRPCError('failed to get repo: `%s` nodes' % repo.repo_name)
647 647
648 648
649 649 @jsonrpc_method()
650 650 def get_repo_refs(request, apiuser, repoid):
651 651 """
652 652 Returns a dictionary of current references. It returns
653 653 bookmarks, branches, closed_branches, and tags for given repository
654 654
655 655 It's possible to specify ret_type to show only `files` or `dirs`.
656 656
657 657 This command can only be run using an |authtoken| with admin rights,
658 658 or users with at least read rights to |repos|.
659 659
660 660 :param apiuser: This is filled automatically from the |authtoken|.
661 661 :type apiuser: AuthUser
662 662 :param repoid: The repository name or repository ID.
663 663 :type repoid: str or int
664 664
665 665 Example output:
666 666
667 667 .. code-block:: bash
668 668
669 669 id : <id_given_in_input>
670 670 "result": {
671 671 "bookmarks": {
672 672 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
673 673 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
674 674 },
675 675 "branches": {
676 676 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
677 677 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
678 678 },
679 679 "branches_closed": {},
680 680 "tags": {
681 681 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
682 682 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
683 683 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
684 684 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
685 685 }
686 686 }
687 687 error: null
688 688 """
689 689
690 690 repo = get_repo_or_error(repoid)
691 691 if not has_superadmin_permission(apiuser):
692 692 _perms = ('repository.admin', 'repository.write', 'repository.read',)
693 693 validate_repo_permissions(apiuser, repoid, repo, _perms)
694 694
695 695 try:
696 696 # check if repo is not empty by any chance, skip quicker if it is.
697 697 vcs_instance = repo.scm_instance()
698 698 refs = vcs_instance.refs()
699 699 return refs
700 700 except Exception:
701 701 log.exception("Exception occurred while trying to get repo refs")
702 702 raise JSONRPCError(
703 703 'failed to get repo: `%s` references' % repo.repo_name
704 704 )
705 705
706 706
707 707 @jsonrpc_method()
708 708 def create_repo(
709 709 request, apiuser, repo_name, repo_type,
710 710 owner=Optional(OAttr('apiuser')),
711 711 description=Optional(''),
712 712 private=Optional(False),
713 713 clone_uri=Optional(None),
714 714 push_uri=Optional(None),
715 715 landing_rev=Optional(None),
716 716 enable_statistics=Optional(False),
717 717 enable_locking=Optional(False),
718 718 enable_downloads=Optional(False),
719 719 copy_permissions=Optional(False)):
720 720 """
721 721 Creates a repository.
722 722
723 723 * If the repository name contains "/", repository will be created inside
724 724 a repository group or nested repository groups
725 725
726 726 For example "foo/bar/repo1" will create |repo| called "repo1" inside
727 727 group "foo/bar". You have to have permissions to access and write to
728 728 the last repository group ("bar" in this example)
729 729
730 730 This command can only be run using an |authtoken| with at least
731 731 permissions to create repositories, or write permissions to
732 732 parent repository groups.
733 733
734 734 :param apiuser: This is filled automatically from the |authtoken|.
735 735 :type apiuser: AuthUser
736 736 :param repo_name: Set the repository name.
737 737 :type repo_name: str
738 738 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
739 739 :type repo_type: str
740 740 :param owner: user_id or username
741 741 :type owner: Optional(str)
742 742 :param description: Set the repository description.
743 743 :type description: Optional(str)
744 744 :param private: set repository as private
745 745 :type private: bool
746 746 :param clone_uri: set clone_uri
747 747 :type clone_uri: str
748 748 :param push_uri: set push_uri
749 749 :type push_uri: str
750 750 :param landing_rev: <rev_type>:<rev>, e.g branch:default, book:dev, rev:abcd
751 751 :type landing_rev: str
752 752 :param enable_locking:
753 753 :type enable_locking: bool
754 754 :param enable_downloads:
755 755 :type enable_downloads: bool
756 756 :param enable_statistics:
757 757 :type enable_statistics: bool
758 758 :param copy_permissions: Copy permission from group in which the
759 759 repository is being created.
760 760 :type copy_permissions: bool
761 761
762 762
763 763 Example output:
764 764
765 765 .. code-block:: bash
766 766
767 767 id : <id_given_in_input>
768 768 result: {
769 769 "msg": "Created new repository `<reponame>`",
770 770 "success": true,
771 771 "task": "<celery task id or None if done sync>"
772 772 }
773 773 error: null
774 774
775 775
776 776 Example error output:
777 777
778 778 .. code-block:: bash
779 779
780 780 id : <id_given_in_input>
781 781 result : null
782 782 error : {
783 783 'failed to create repository `<repo_name>`'
784 784 }
785 785
786 786 """
787 787
788 788 owner = validate_set_owner_permissions(apiuser, owner)
789 789
790 790 description = Optional.extract(description)
791 791 copy_permissions = Optional.extract(copy_permissions)
792 792 clone_uri = Optional.extract(clone_uri)
793 793 push_uri = Optional.extract(push_uri)
794 794
795 795 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
796 796 if isinstance(private, Optional):
797 797 private = defs.get('repo_private') or Optional.extract(private)
798 798 if isinstance(repo_type, Optional):
799 799 repo_type = defs.get('repo_type')
800 800 if isinstance(enable_statistics, Optional):
801 801 enable_statistics = defs.get('repo_enable_statistics')
802 802 if isinstance(enable_locking, Optional):
803 803 enable_locking = defs.get('repo_enable_locking')
804 804 if isinstance(enable_downloads, Optional):
805 805 enable_downloads = defs.get('repo_enable_downloads')
806 806
807 807 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
808 808 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
809 809 ref_choices = list(set(ref_choices + [landing_ref]))
810 810
811 811 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
812 812
813 813 schema = repo_schema.RepoSchema().bind(
814 814 repo_type_options=rhodecode.BACKENDS.keys(),
815 815 repo_ref_options=ref_choices,
816 816 repo_type=repo_type,
817 817 # user caller
818 818 user=apiuser)
819 819
820 820 try:
821 821 schema_data = schema.deserialize(dict(
822 822 repo_name=repo_name,
823 823 repo_type=repo_type,
824 824 repo_owner=owner.username,
825 825 repo_description=description,
826 826 repo_landing_commit_ref=landing_commit_ref,
827 827 repo_clone_uri=clone_uri,
828 828 repo_push_uri=push_uri,
829 829 repo_private=private,
830 830 repo_copy_permissions=copy_permissions,
831 831 repo_enable_statistics=enable_statistics,
832 832 repo_enable_downloads=enable_downloads,
833 833 repo_enable_locking=enable_locking))
834 834 except validation_schema.Invalid as err:
835 835 raise JSONRPCValidationError(colander_exc=err)
836 836
837 837 try:
838 838 data = {
839 839 'owner': owner,
840 840 'repo_name': schema_data['repo_group']['repo_name_without_group'],
841 841 'repo_name_full': schema_data['repo_name'],
842 842 'repo_group': schema_data['repo_group']['repo_group_id'],
843 843 'repo_type': schema_data['repo_type'],
844 844 'repo_description': schema_data['repo_description'],
845 845 'repo_private': schema_data['repo_private'],
846 846 'clone_uri': schema_data['repo_clone_uri'],
847 847 'push_uri': schema_data['repo_push_uri'],
848 848 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
849 849 'enable_statistics': schema_data['repo_enable_statistics'],
850 850 'enable_locking': schema_data['repo_enable_locking'],
851 851 'enable_downloads': schema_data['repo_enable_downloads'],
852 852 'repo_copy_permissions': schema_data['repo_copy_permissions'],
853 853 }
854 854
855 855 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
856 856 task_id = get_task_id(task)
857 857 # no commit, it's done in RepoModel, or async via celery
858 858 return {
859 859 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
860 860 'success': True, # cannot return the repo data here since fork
861 861 # can be done async
862 862 'task': task_id
863 863 }
864 864 except Exception:
865 865 log.exception(
866 866 u"Exception while trying to create the repository %s",
867 867 schema_data['repo_name'])
868 868 raise JSONRPCError(
869 869 'failed to create repository `%s`' % (schema_data['repo_name'],))
870 870
871 871
872 872 @jsonrpc_method()
873 873 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
874 874 description=Optional('')):
875 875 """
876 876 Adds an extra field to a repository.
877 877
878 878 This command can only be run using an |authtoken| with at least
879 879 write permissions to the |repo|.
880 880
881 881 :param apiuser: This is filled automatically from the |authtoken|.
882 882 :type apiuser: AuthUser
883 883 :param repoid: Set the repository name or repository id.
884 884 :type repoid: str or int
885 885 :param key: Create a unique field key for this repository.
886 886 :type key: str
887 887 :param label:
888 888 :type label: Optional(str)
889 889 :param description:
890 890 :type description: Optional(str)
891 891 """
892 892 repo = get_repo_or_error(repoid)
893 893 if not has_superadmin_permission(apiuser):
894 894 _perms = ('repository.admin',)
895 895 validate_repo_permissions(apiuser, repoid, repo, _perms)
896 896
897 897 label = Optional.extract(label) or key
898 898 description = Optional.extract(description)
899 899
900 900 field = RepositoryField.get_by_key_name(key, repo)
901 901 if field:
902 902 raise JSONRPCError('Field with key '
903 903 '`%s` exists for repo `%s`' % (key, repoid))
904 904
905 905 try:
906 906 RepoModel().add_repo_field(repo, key, field_label=label,
907 907 field_desc=description)
908 908 Session().commit()
909 909 return {
910 910 'msg': "Added new repository field `%s`" % (key,),
911 911 'success': True,
912 912 }
913 913 except Exception:
914 914 log.exception("Exception occurred while trying to add field to repo")
915 915 raise JSONRPCError(
916 916 'failed to create new field for repository `%s`' % (repoid,))
917 917
918 918
919 919 @jsonrpc_method()
920 920 def remove_field_from_repo(request, apiuser, repoid, key):
921 921 """
922 922 Removes an extra field from a repository.
923 923
924 924 This command can only be run using an |authtoken| with at least
925 925 write permissions to the |repo|.
926 926
927 927 :param apiuser: This is filled automatically from the |authtoken|.
928 928 :type apiuser: AuthUser
929 929 :param repoid: Set the repository name or repository ID.
930 930 :type repoid: str or int
931 931 :param key: Set the unique field key for this repository.
932 932 :type key: str
933 933 """
934 934
935 935 repo = get_repo_or_error(repoid)
936 936 if not has_superadmin_permission(apiuser):
937 937 _perms = ('repository.admin',)
938 938 validate_repo_permissions(apiuser, repoid, repo, _perms)
939 939
940 940 field = RepositoryField.get_by_key_name(key, repo)
941 941 if not field:
942 942 raise JSONRPCError('Field with key `%s` does not '
943 943 'exists for repo `%s`' % (key, repoid))
944 944
945 945 try:
946 946 RepoModel().delete_repo_field(repo, field_key=key)
947 947 Session().commit()
948 948 return {
949 949 'msg': "Deleted repository field `%s`" % (key,),
950 950 'success': True,
951 951 }
952 952 except Exception:
953 953 log.exception(
954 954 "Exception occurred while trying to delete field from repo")
955 955 raise JSONRPCError(
956 956 'failed to delete field for repository `%s`' % (repoid,))
957 957
958 958
959 959 @jsonrpc_method()
960 960 def update_repo(
961 961 request, apiuser, repoid, repo_name=Optional(None),
962 962 owner=Optional(OAttr('apiuser')), description=Optional(''),
963 963 private=Optional(False),
964 964 clone_uri=Optional(None), push_uri=Optional(None),
965 965 landing_rev=Optional(None), fork_of=Optional(None),
966 966 enable_statistics=Optional(False),
967 967 enable_locking=Optional(False),
968 968 enable_downloads=Optional(False), fields=Optional('')):
969 969 """
970 970 Updates a repository with the given information.
971 971
972 972 This command can only be run using an |authtoken| with at least
973 973 admin permissions to the |repo|.
974 974
975 975 * If the repository name contains "/", repository will be updated
976 976 accordingly with a repository group or nested repository groups
977 977
978 978 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
979 979 called "repo-test" and place it inside group "foo/bar".
980 980 You have to have permissions to access and write to the last repository
981 981 group ("bar" in this example)
982 982
983 983 :param apiuser: This is filled automatically from the |authtoken|.
984 984 :type apiuser: AuthUser
985 985 :param repoid: repository name or repository ID.
986 986 :type repoid: str or int
987 987 :param repo_name: Update the |repo| name, including the
988 988 repository group it's in.
989 989 :type repo_name: str
990 990 :param owner: Set the |repo| owner.
991 991 :type owner: str
992 992 :param fork_of: Set the |repo| as fork of another |repo|.
993 993 :type fork_of: str
994 994 :param description: Update the |repo| description.
995 995 :type description: str
996 996 :param private: Set the |repo| as private. (True | False)
997 997 :type private: bool
998 998 :param clone_uri: Update the |repo| clone URI.
999 999 :type clone_uri: str
1000 1000 :param landing_rev: Set the |repo| landing revision. e.g branch:default, book:dev, rev:abcd
1001 1001 :type landing_rev: str
1002 1002 :param enable_statistics: Enable statistics on the |repo|, (True | False).
1003 1003 :type enable_statistics: bool
1004 1004 :param enable_locking: Enable |repo| locking.
1005 1005 :type enable_locking: bool
1006 1006 :param enable_downloads: Enable downloads from the |repo|, (True | False).
1007 1007 :type enable_downloads: bool
1008 1008 :param fields: Add extra fields to the |repo|. Use the following
1009 1009 example format: ``field_key=field_val,field_key2=fieldval2``.
1010 1010 Escape ', ' with \,
1011 1011 :type fields: str
1012 1012 """
1013 1013
1014 1014 repo = get_repo_or_error(repoid)
1015 1015
1016 1016 include_secrets = False
1017 1017 if not has_superadmin_permission(apiuser):
1018 1018 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
1019 1019 else:
1020 1020 include_secrets = True
1021 1021
1022 1022 updates = dict(
1023 1023 repo_name=repo_name
1024 1024 if not isinstance(repo_name, Optional) else repo.repo_name,
1025 1025
1026 1026 fork_id=fork_of
1027 1027 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
1028 1028
1029 1029 user=owner
1030 1030 if not isinstance(owner, Optional) else repo.user.username,
1031 1031
1032 1032 repo_description=description
1033 1033 if not isinstance(description, Optional) else repo.description,
1034 1034
1035 1035 repo_private=private
1036 1036 if not isinstance(private, Optional) else repo.private,
1037 1037
1038 1038 clone_uri=clone_uri
1039 1039 if not isinstance(clone_uri, Optional) else repo.clone_uri,
1040 1040
1041 1041 push_uri=push_uri
1042 1042 if not isinstance(push_uri, Optional) else repo.push_uri,
1043 1043
1044 1044 repo_landing_rev=landing_rev
1045 1045 if not isinstance(landing_rev, Optional) else repo._landing_revision,
1046 1046
1047 1047 repo_enable_statistics=enable_statistics
1048 1048 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
1049 1049
1050 1050 repo_enable_locking=enable_locking
1051 1051 if not isinstance(enable_locking, Optional) else repo.enable_locking,
1052 1052
1053 1053 repo_enable_downloads=enable_downloads
1054 1054 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
1055 1055
1056 1056 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1057 1057 ref_choices, _labels = ScmModel().get_repo_landing_revs(
1058 1058 request.translate, repo=repo)
1059 1059 ref_choices = list(set(ref_choices + [landing_ref]))
1060 1060
1061 1061 old_values = repo.get_api_data()
1062 1062 repo_type = repo.repo_type
1063 1063 schema = repo_schema.RepoSchema().bind(
1064 1064 repo_type_options=rhodecode.BACKENDS.keys(),
1065 1065 repo_ref_options=ref_choices,
1066 1066 repo_type=repo_type,
1067 1067 # user caller
1068 1068 user=apiuser,
1069 1069 old_values=old_values)
1070 1070 try:
1071 1071 schema_data = schema.deserialize(dict(
1072 1072 # we save old value, users cannot change type
1073 1073 repo_type=repo_type,
1074 1074
1075 1075 repo_name=updates['repo_name'],
1076 1076 repo_owner=updates['user'],
1077 1077 repo_description=updates['repo_description'],
1078 1078 repo_clone_uri=updates['clone_uri'],
1079 1079 repo_push_uri=updates['push_uri'],
1080 1080 repo_fork_of=updates['fork_id'],
1081 1081 repo_private=updates['repo_private'],
1082 1082 repo_landing_commit_ref=updates['repo_landing_rev'],
1083 1083 repo_enable_statistics=updates['repo_enable_statistics'],
1084 1084 repo_enable_downloads=updates['repo_enable_downloads'],
1085 1085 repo_enable_locking=updates['repo_enable_locking']))
1086 1086 except validation_schema.Invalid as err:
1087 1087 raise JSONRPCValidationError(colander_exc=err)
1088 1088
1089 1089 # save validated data back into the updates dict
1090 1090 validated_updates = dict(
1091 1091 repo_name=schema_data['repo_group']['repo_name_without_group'],
1092 1092 repo_group=schema_data['repo_group']['repo_group_id'],
1093 1093
1094 1094 user=schema_data['repo_owner'],
1095 1095 repo_description=schema_data['repo_description'],
1096 1096 repo_private=schema_data['repo_private'],
1097 1097 clone_uri=schema_data['repo_clone_uri'],
1098 1098 push_uri=schema_data['repo_push_uri'],
1099 1099 repo_landing_rev=schema_data['repo_landing_commit_ref'],
1100 1100 repo_enable_statistics=schema_data['repo_enable_statistics'],
1101 1101 repo_enable_locking=schema_data['repo_enable_locking'],
1102 1102 repo_enable_downloads=schema_data['repo_enable_downloads'],
1103 1103 )
1104 1104
1105 1105 if schema_data['repo_fork_of']:
1106 1106 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
1107 1107 validated_updates['fork_id'] = fork_repo.repo_id
1108 1108
1109 1109 # extra fields
1110 1110 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
1111 1111 if fields:
1112 1112 validated_updates.update(fields)
1113 1113
1114 1114 try:
1115 1115 RepoModel().update(repo, **validated_updates)
1116 1116 audit_logger.store_api(
1117 1117 'repo.edit', action_data={'old_data': old_values},
1118 1118 user=apiuser, repo=repo)
1119 1119 Session().commit()
1120 1120 return {
1121 1121 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1122 1122 'repository': repo.get_api_data(include_secrets=include_secrets)
1123 1123 }
1124 1124 except Exception:
1125 1125 log.exception(
1126 1126 u"Exception while trying to update the repository %s",
1127 1127 repoid)
1128 1128 raise JSONRPCError('failed to update repo `%s`' % repoid)
1129 1129
1130 1130
1131 1131 @jsonrpc_method()
1132 1132 def fork_repo(request, apiuser, repoid, fork_name,
1133 1133 owner=Optional(OAttr('apiuser')),
1134 1134 description=Optional(''),
1135 1135 private=Optional(False),
1136 1136 clone_uri=Optional(None),
1137 1137 landing_rev=Optional(None),
1138 1138 copy_permissions=Optional(False)):
1139 1139 """
1140 1140 Creates a fork of the specified |repo|.
1141 1141
1142 1142 * If the fork_name contains "/", fork will be created inside
1143 1143 a repository group or nested repository groups
1144 1144
1145 1145 For example "foo/bar/fork-repo" will create fork called "fork-repo"
1146 1146 inside group "foo/bar". You have to have permissions to access and
1147 1147 write to the last repository group ("bar" in this example)
1148 1148
1149 1149 This command can only be run using an |authtoken| with minimum
1150 1150 read permissions of the forked repo, create fork permissions for an user.
1151 1151
1152 1152 :param apiuser: This is filled automatically from the |authtoken|.
1153 1153 :type apiuser: AuthUser
1154 1154 :param repoid: Set repository name or repository ID.
1155 1155 :type repoid: str or int
1156 1156 :param fork_name: Set the fork name, including it's repository group membership.
1157 1157 :type fork_name: str
1158 1158 :param owner: Set the fork owner.
1159 1159 :type owner: str
1160 1160 :param description: Set the fork description.
1161 1161 :type description: str
1162 1162 :param copy_permissions: Copy permissions from parent |repo|. The
1163 1163 default is False.
1164 1164 :type copy_permissions: bool
1165 1165 :param private: Make the fork private. The default is False.
1166 1166 :type private: bool
1167 1167 :param landing_rev: Set the landing revision. E.g branch:default, book:dev, rev:abcd
1168 1168
1169 1169 Example output:
1170 1170
1171 1171 .. code-block:: bash
1172 1172
1173 1173 id : <id_for_response>
1174 1174 api_key : "<api_key>"
1175 1175 args: {
1176 1176 "repoid" : "<reponame or repo_id>",
1177 1177 "fork_name": "<forkname>",
1178 1178 "owner": "<username or user_id = Optional(=apiuser)>",
1179 1179 "description": "<description>",
1180 1180 "copy_permissions": "<bool>",
1181 1181 "private": "<bool>",
1182 1182 "landing_rev": "<landing_rev>"
1183 1183 }
1184 1184
1185 1185 Example error output:
1186 1186
1187 1187 .. code-block:: bash
1188 1188
1189 1189 id : <id_given_in_input>
1190 1190 result: {
1191 1191 "msg": "Created fork of `<reponame>` as `<forkname>`",
1192 1192 "success": true,
1193 1193 "task": "<celery task id or None if done sync>"
1194 1194 }
1195 1195 error: null
1196 1196
1197 1197 """
1198 1198
1199 1199 repo = get_repo_or_error(repoid)
1200 1200 repo_name = repo.repo_name
1201 1201
1202 1202 if not has_superadmin_permission(apiuser):
1203 1203 # check if we have at least read permission for
1204 1204 # this repo that we fork !
1205 1205 _perms = (
1206 1206 'repository.admin', 'repository.write', 'repository.read')
1207 1207 validate_repo_permissions(apiuser, repoid, repo, _perms)
1208 1208
1209 1209 # check if the regular user has at least fork permissions as well
1210 1210 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1211 1211 raise JSONRPCForbidden()
1212 1212
1213 1213 # check if user can set owner parameter
1214 1214 owner = validate_set_owner_permissions(apiuser, owner)
1215 1215
1216 1216 description = Optional.extract(description)
1217 1217 copy_permissions = Optional.extract(copy_permissions)
1218 1218 clone_uri = Optional.extract(clone_uri)
1219 1219
1220 1220 landing_ref, _label = ScmModel.backend_landing_ref(repo.repo_type)
1221 1221 ref_choices, _labels = ScmModel().get_repo_landing_revs(request.translate)
1222 1222 ref_choices = list(set(ref_choices + [landing_ref]))
1223 1223 landing_commit_ref = Optional.extract(landing_rev) or landing_ref
1224 1224
1225 1225 private = Optional.extract(private)
1226 1226
1227 1227 schema = repo_schema.RepoSchema().bind(
1228 1228 repo_type_options=rhodecode.BACKENDS.keys(),
1229 1229 repo_ref_options=ref_choices,
1230 1230 repo_type=repo.repo_type,
1231 1231 # user caller
1232 1232 user=apiuser)
1233 1233
1234 1234 try:
1235 1235 schema_data = schema.deserialize(dict(
1236 1236 repo_name=fork_name,
1237 1237 repo_type=repo.repo_type,
1238 1238 repo_owner=owner.username,
1239 1239 repo_description=description,
1240 1240 repo_landing_commit_ref=landing_commit_ref,
1241 1241 repo_clone_uri=clone_uri,
1242 1242 repo_private=private,
1243 1243 repo_copy_permissions=copy_permissions))
1244 1244 except validation_schema.Invalid as err:
1245 1245 raise JSONRPCValidationError(colander_exc=err)
1246 1246
1247 1247 try:
1248 1248 data = {
1249 1249 'fork_parent_id': repo.repo_id,
1250 1250
1251 1251 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1252 1252 'repo_name_full': schema_data['repo_name'],
1253 1253 'repo_group': schema_data['repo_group']['repo_group_id'],
1254 1254 'repo_type': schema_data['repo_type'],
1255 1255 'description': schema_data['repo_description'],
1256 1256 'private': schema_data['repo_private'],
1257 1257 'copy_permissions': schema_data['repo_copy_permissions'],
1258 1258 'landing_rev': schema_data['repo_landing_commit_ref'],
1259 1259 }
1260 1260
1261 1261 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1262 1262 # no commit, it's done in RepoModel, or async via celery
1263 1263 task_id = get_task_id(task)
1264 1264
1265 1265 return {
1266 1266 'msg': 'Created fork of `%s` as `%s`' % (
1267 1267 repo.repo_name, schema_data['repo_name']),
1268 1268 'success': True, # cannot return the repo data here since fork
1269 1269 # can be done async
1270 1270 'task': task_id
1271 1271 }
1272 1272 except Exception:
1273 1273 log.exception(
1274 1274 u"Exception while trying to create fork %s",
1275 1275 schema_data['repo_name'])
1276 1276 raise JSONRPCError(
1277 1277 'failed to fork repository `%s` as `%s`' % (
1278 1278 repo_name, schema_data['repo_name']))
1279 1279
1280 1280
1281 1281 @jsonrpc_method()
1282 1282 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1283 1283 """
1284 1284 Deletes a repository.
1285 1285
1286 1286 * When the `forks` parameter is set it's possible to detach or delete
1287 1287 forks of deleted repository.
1288 1288
1289 1289 This command can only be run using an |authtoken| with admin
1290 1290 permissions on the |repo|.
1291 1291
1292 1292 :param apiuser: This is filled automatically from the |authtoken|.
1293 1293 :type apiuser: AuthUser
1294 1294 :param repoid: Set the repository name or repository ID.
1295 1295 :type repoid: str or int
1296 1296 :param forks: Set to `detach` or `delete` forks from the |repo|.
1297 1297 :type forks: Optional(str)
1298 1298
1299 1299 Example error output:
1300 1300
1301 1301 .. code-block:: bash
1302 1302
1303 1303 id : <id_given_in_input>
1304 1304 result: {
1305 1305 "msg": "Deleted repository `<reponame>`",
1306 1306 "success": true
1307 1307 }
1308 1308 error: null
1309 1309 """
1310 1310
1311 1311 repo = get_repo_or_error(repoid)
1312 1312 repo_name = repo.repo_name
1313 1313 if not has_superadmin_permission(apiuser):
1314 1314 _perms = ('repository.admin',)
1315 1315 validate_repo_permissions(apiuser, repoid, repo, _perms)
1316 1316
1317 1317 try:
1318 1318 handle_forks = Optional.extract(forks)
1319 1319 _forks_msg = ''
1320 1320 _forks = [f for f in repo.forks]
1321 1321 if handle_forks == 'detach':
1322 1322 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1323 1323 elif handle_forks == 'delete':
1324 1324 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1325 1325 elif _forks:
1326 1326 raise JSONRPCError(
1327 1327 'Cannot delete `%s` it still contains attached forks' %
1328 1328 (repo.repo_name,)
1329 1329 )
1330 1330 old_data = repo.get_api_data()
1331 1331 RepoModel().delete(repo, forks=forks)
1332 1332
1333 1333 repo = audit_logger.RepoWrap(repo_id=None,
1334 1334 repo_name=repo.repo_name)
1335 1335
1336 1336 audit_logger.store_api(
1337 1337 'repo.delete', action_data={'old_data': old_data},
1338 1338 user=apiuser, repo=repo)
1339 1339
1340 1340 ScmModel().mark_for_invalidation(repo_name, delete=True)
1341 1341 Session().commit()
1342 1342 return {
1343 1343 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1344 1344 'success': True
1345 1345 }
1346 1346 except Exception:
1347 1347 log.exception("Exception occurred while trying to delete repo")
1348 1348 raise JSONRPCError(
1349 1349 'failed to delete repository `%s`' % (repo_name,)
1350 1350 )
1351 1351
1352 1352
1353 1353 #TODO: marcink, change name ?
1354 1354 @jsonrpc_method()
1355 1355 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1356 1356 """
1357 1357 Invalidates the cache for the specified repository.
1358 1358
1359 1359 This command can only be run using an |authtoken| with admin rights to
1360 1360 the specified repository.
1361 1361
1362 1362 This command takes the following options:
1363 1363
1364 1364 :param apiuser: This is filled automatically from |authtoken|.
1365 1365 :type apiuser: AuthUser
1366 1366 :param repoid: Sets the repository name or repository ID.
1367 1367 :type repoid: str or int
1368 1368 :param delete_keys: This deletes the invalidated keys instead of
1369 1369 just flagging them.
1370 1370 :type delete_keys: Optional(``True`` | ``False``)
1371 1371
1372 1372 Example output:
1373 1373
1374 1374 .. code-block:: bash
1375 1375
1376 1376 id : <id_given_in_input>
1377 1377 result : {
1378 1378 'msg': Cache for repository `<repository name>` was invalidated,
1379 1379 'repository': <repository name>
1380 1380 }
1381 1381 error : null
1382 1382
1383 1383 Example error output:
1384 1384
1385 1385 .. code-block:: bash
1386 1386
1387 1387 id : <id_given_in_input>
1388 1388 result : null
1389 1389 error : {
1390 1390 'Error occurred during cache invalidation action'
1391 1391 }
1392 1392
1393 1393 """
1394 1394
1395 1395 repo = get_repo_or_error(repoid)
1396 1396 if not has_superadmin_permission(apiuser):
1397 1397 _perms = ('repository.admin', 'repository.write',)
1398 1398 validate_repo_permissions(apiuser, repoid, repo, _perms)
1399 1399
1400 1400 delete = Optional.extract(delete_keys)
1401 1401 try:
1402 1402 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1403 1403 return {
1404 1404 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1405 1405 'repository': repo.repo_name
1406 1406 }
1407 1407 except Exception:
1408 1408 log.exception(
1409 1409 "Exception occurred while trying to invalidate repo cache")
1410 1410 raise JSONRPCError(
1411 1411 'Error occurred during cache invalidation action'
1412 1412 )
1413 1413
1414 1414
1415 1415 #TODO: marcink, change name ?
1416 1416 @jsonrpc_method()
1417 1417 def lock(request, apiuser, repoid, locked=Optional(None),
1418 1418 userid=Optional(OAttr('apiuser'))):
1419 1419 """
1420 1420 Sets the lock state of the specified |repo| by the given user.
1421 1421 From more information, see :ref:`repo-locking`.
1422 1422
1423 1423 * If the ``userid`` option is not set, the repository is locked to the
1424 1424 user who called the method.
1425 1425 * If the ``locked`` parameter is not set, the current lock state of the
1426 1426 repository is displayed.
1427 1427
1428 1428 This command can only be run using an |authtoken| with admin rights to
1429 1429 the specified repository.
1430 1430
1431 1431 This command takes the following options:
1432 1432
1433 1433 :param apiuser: This is filled automatically from the |authtoken|.
1434 1434 :type apiuser: AuthUser
1435 1435 :param repoid: Sets the repository name or repository ID.
1436 1436 :type repoid: str or int
1437 1437 :param locked: Sets the lock state.
1438 1438 :type locked: Optional(``True`` | ``False``)
1439 1439 :param userid: Set the repository lock to this user.
1440 1440 :type userid: Optional(str or int)
1441 1441
1442 1442 Example error output:
1443 1443
1444 1444 .. code-block:: bash
1445 1445
1446 1446 id : <id_given_in_input>
1447 1447 result : {
1448 1448 'repo': '<reponame>',
1449 1449 'locked': <bool: lock state>,
1450 1450 'locked_since': <int: lock timestamp>,
1451 1451 'locked_by': <username of person who made the lock>,
1452 1452 'lock_reason': <str: reason for locking>,
1453 1453 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1454 1454 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1455 1455 or
1456 1456 'msg': 'Repo `<repository name>` not locked.'
1457 1457 or
1458 1458 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1459 1459 }
1460 1460 error : null
1461 1461
1462 1462 Example error output:
1463 1463
1464 1464 .. code-block:: bash
1465 1465
1466 1466 id : <id_given_in_input>
1467 1467 result : null
1468 1468 error : {
1469 1469 'Error occurred locking repository `<reponame>`'
1470 1470 }
1471 1471 """
1472 1472
1473 1473 repo = get_repo_or_error(repoid)
1474 1474 if not has_superadmin_permission(apiuser):
1475 1475 # check if we have at least write permission for this repo !
1476 1476 _perms = ('repository.admin', 'repository.write',)
1477 1477 validate_repo_permissions(apiuser, repoid, repo, _perms)
1478 1478
1479 1479 # make sure normal user does not pass someone else userid,
1480 1480 # he is not allowed to do that
1481 1481 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1482 1482 raise JSONRPCError('userid is not the same as your user')
1483 1483
1484 1484 if isinstance(userid, Optional):
1485 1485 userid = apiuser.user_id
1486 1486
1487 1487 user = get_user_or_error(userid)
1488 1488
1489 1489 if isinstance(locked, Optional):
1490 1490 lockobj = repo.locked
1491 1491
1492 1492 if lockobj[0] is None:
1493 1493 _d = {
1494 1494 'repo': repo.repo_name,
1495 1495 'locked': False,
1496 1496 'locked_since': None,
1497 1497 'locked_by': None,
1498 1498 'lock_reason': None,
1499 1499 'lock_state_changed': False,
1500 1500 'msg': 'Repo `%s` not locked.' % repo.repo_name
1501 1501 }
1502 1502 return _d
1503 1503 else:
1504 1504 _user_id, _time, _reason = lockobj
1505 1505 lock_user = get_user_or_error(userid)
1506 1506 _d = {
1507 1507 'repo': repo.repo_name,
1508 1508 'locked': True,
1509 1509 'locked_since': _time,
1510 1510 'locked_by': lock_user.username,
1511 1511 'lock_reason': _reason,
1512 1512 'lock_state_changed': False,
1513 1513 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1514 1514 % (repo.repo_name, lock_user.username,
1515 1515 json.dumps(time_to_datetime(_time))))
1516 1516 }
1517 1517 return _d
1518 1518
1519 1519 # force locked state through a flag
1520 1520 else:
1521 1521 locked = str2bool(locked)
1522 1522 lock_reason = Repository.LOCK_API
1523 1523 try:
1524 1524 if locked:
1525 1525 lock_time = time.time()
1526 1526 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1527 1527 else:
1528 1528 lock_time = None
1529 1529 Repository.unlock(repo)
1530 1530 _d = {
1531 1531 'repo': repo.repo_name,
1532 1532 'locked': locked,
1533 1533 'locked_since': lock_time,
1534 1534 'locked_by': user.username,
1535 1535 'lock_reason': lock_reason,
1536 1536 'lock_state_changed': True,
1537 1537 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1538 1538 % (user.username, repo.repo_name, locked))
1539 1539 }
1540 1540 return _d
1541 1541 except Exception:
1542 1542 log.exception(
1543 1543 "Exception occurred while trying to lock repository")
1544 1544 raise JSONRPCError(
1545 1545 'Error occurred locking repository `%s`' % repo.repo_name
1546 1546 )
1547 1547
1548 1548
1549 1549 @jsonrpc_method()
1550 1550 def comment_commit(
1551 1551 request, apiuser, repoid, commit_id, message, status=Optional(None),
1552 1552 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1553 1553 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
1554 1554 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
1555 1555 """
1556 1556 Set a commit comment, and optionally change the status of the commit.
1557 1557
1558 1558 :param apiuser: This is filled automatically from the |authtoken|.
1559 1559 :type apiuser: AuthUser
1560 1560 :param repoid: Set the repository name or repository ID.
1561 1561 :type repoid: str or int
1562 1562 :param commit_id: Specify the commit_id for which to set a comment.
1563 1563 :type commit_id: str
1564 1564 :param message: The comment text.
1565 1565 :type message: str
1566 1566 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1567 1567 'approved', 'rejected', 'under_review'
1568 1568 :type status: str
1569 1569 :param comment_type: Comment type, one of: 'note', 'todo'
1570 1570 :type comment_type: Optional(str), default: 'note'
1571 1571 :param resolves_comment_id: id of comment which this one will resolve
1572 1572 :type resolves_comment_id: Optional(int)
1573 1573 :param extra_recipients: list of user ids or usernames to add
1574 1574 notifications for this comment. Acts like a CC for notification
1575 1575 :type extra_recipients: Optional(list)
1576 1576 :param userid: Set the user name of the comment creator.
1577 1577 :type userid: Optional(str or int)
1578 1578 :param send_email: Define if this comment should also send email notification
1579 1579 :type send_email: Optional(bool)
1580 1580
1581 1581 Example error output:
1582 1582
1583 1583 .. code-block:: bash
1584 1584
1585 1585 {
1586 1586 "id" : <id_given_in_input>,
1587 1587 "result" : {
1588 1588 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1589 1589 "status_change": null or <status>,
1590 1590 "success": true
1591 1591 },
1592 1592 "error" : null
1593 1593 }
1594 1594
1595 1595 """
1596 1596 repo = get_repo_or_error(repoid)
1597 1597 if not has_superadmin_permission(apiuser):
1598 1598 _perms = ('repository.read', 'repository.write', 'repository.admin')
1599 1599 validate_repo_permissions(apiuser, repoid, repo, _perms)
1600 1600
1601 1601 try:
1602 1602 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1603 1603 except Exception as e:
1604 1604 log.exception('Failed to fetch commit')
1605 1605 raise JSONRPCError(safe_str(e))
1606 1606
1607 1607 if isinstance(userid, Optional):
1608 1608 userid = apiuser.user_id
1609 1609
1610 1610 user = get_user_or_error(userid)
1611 1611 status = Optional.extract(status)
1612 1612 comment_type = Optional.extract(comment_type)
1613 1613 resolves_comment_id = Optional.extract(resolves_comment_id)
1614 1614 extra_recipients = Optional.extract(extra_recipients)
1615 1615 send_email = Optional.extract(send_email, binary=True)
1616 1616
1617 1617 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1618 1618 if status and status not in allowed_statuses:
1619 1619 raise JSONRPCError('Bad status, must be on '
1620 1620 'of %s got %s' % (allowed_statuses, status,))
1621 1621
1622 1622 if resolves_comment_id:
1623 1623 comment = ChangesetComment.get(resolves_comment_id)
1624 1624 if not comment:
1625 1625 raise JSONRPCError(
1626 1626 'Invalid resolves_comment_id `%s` for this commit.'
1627 1627 % resolves_comment_id)
1628 1628 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1629 1629 raise JSONRPCError(
1630 1630 'Comment `%s` is wrong type for setting status to resolved.'
1631 1631 % resolves_comment_id)
1632 1632
1633 1633 try:
1634 1634 rc_config = SettingsModel().get_all_settings()
1635 1635 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1636 1636 status_change_label = ChangesetStatus.get_status_lbl(status)
1637 1637 comment = CommentsModel().create(
1638 1638 message, repo, user, commit_id=commit_id,
1639 1639 status_change=status_change_label,
1640 1640 status_change_type=status,
1641 1641 renderer=renderer,
1642 1642 comment_type=comment_type,
1643 1643 resolves_comment_id=resolves_comment_id,
1644 1644 auth_user=apiuser,
1645 1645 extra_recipients=extra_recipients,
1646 1646 send_email=send_email
1647 1647 )
1648 1648 if status:
1649 1649 # also do a status change
1650 1650 try:
1651 1651 ChangesetStatusModel().set_status(
1652 1652 repo, status, user, comment, revision=commit_id,
1653 1653 dont_allow_on_closed_pull_request=True
1654 1654 )
1655 1655 except StatusChangeOnClosedPullRequestError:
1656 1656 log.exception(
1657 1657 "Exception occurred while trying to change repo commit status")
1658 msg = ('Changing status on a changeset associated with '
1658 msg = ('Changing status on a commit associated with '
1659 1659 'a closed pull request is not allowed')
1660 1660 raise JSONRPCError(msg)
1661 1661
1662 1662 Session().commit()
1663 1663 return {
1664 1664 'msg': (
1665 1665 'Commented on commit `%s` for repository `%s`' % (
1666 1666 comment.revision, repo.repo_name)),
1667 1667 'status_change': status,
1668 1668 'success': True,
1669 1669 }
1670 1670 except JSONRPCError:
1671 1671 # catch any inside errors, and re-raise them to prevent from
1672 1672 # below global catch to silence them
1673 1673 raise
1674 1674 except Exception:
1675 1675 log.exception("Exception occurred while trying to comment on commit")
1676 1676 raise JSONRPCError(
1677 1677 'failed to set comment on repository `%s`' % (repo.repo_name,)
1678 1678 )
1679 1679
1680 1680
1681 1681 @jsonrpc_method()
1682 1682 def get_repo_comments(request, apiuser, repoid,
1683 1683 commit_id=Optional(None), comment_type=Optional(None),
1684 1684 userid=Optional(None)):
1685 1685 """
1686 1686 Get all comments for a repository
1687 1687
1688 1688 :param apiuser: This is filled automatically from the |authtoken|.
1689 1689 :type apiuser: AuthUser
1690 1690 :param repoid: Set the repository name or repository ID.
1691 1691 :type repoid: str or int
1692 1692 :param commit_id: Optionally filter the comments by the commit_id
1693 1693 :type commit_id: Optional(str), default: None
1694 1694 :param comment_type: Optionally filter the comments by the comment_type
1695 1695 one of: 'note', 'todo'
1696 1696 :type comment_type: Optional(str), default: None
1697 1697 :param userid: Optionally filter the comments by the author of comment
1698 1698 :type userid: Optional(str or int), Default: None
1699 1699
1700 1700 Example error output:
1701 1701
1702 1702 .. code-block:: bash
1703 1703
1704 1704 {
1705 1705 "id" : <id_given_in_input>,
1706 1706 "result" : [
1707 1707 {
1708 1708 "comment_author": <USER_DETAILS>,
1709 1709 "comment_created_on": "2017-02-01T14:38:16.309",
1710 1710 "comment_f_path": "file.txt",
1711 1711 "comment_id": 282,
1712 1712 "comment_lineno": "n1",
1713 1713 "comment_resolved_by": null,
1714 1714 "comment_status": [],
1715 1715 "comment_text": "This file needs a header",
1716 1716 "comment_type": "todo"
1717 1717 }
1718 1718 ],
1719 1719 "error" : null
1720 1720 }
1721 1721
1722 1722 """
1723 1723 repo = get_repo_or_error(repoid)
1724 1724 if not has_superadmin_permission(apiuser):
1725 1725 _perms = ('repository.read', 'repository.write', 'repository.admin')
1726 1726 validate_repo_permissions(apiuser, repoid, repo, _perms)
1727 1727
1728 1728 commit_id = Optional.extract(commit_id)
1729 1729
1730 1730 userid = Optional.extract(userid)
1731 1731 if userid:
1732 1732 user = get_user_or_error(userid)
1733 1733 else:
1734 1734 user = None
1735 1735
1736 1736 comment_type = Optional.extract(comment_type)
1737 1737 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1738 1738 raise JSONRPCError(
1739 1739 'comment_type must be one of `{}` got {}'.format(
1740 1740 ChangesetComment.COMMENT_TYPES, comment_type)
1741 1741 )
1742 1742
1743 1743 comments = CommentsModel().get_repository_comments(
1744 1744 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1745 1745 return comments
1746 1746
1747 1747
1748 1748 @jsonrpc_method()
1749 1749 def grant_user_permission(request, apiuser, repoid, userid, perm):
1750 1750 """
1751 1751 Grant permissions for the specified user on the given repository,
1752 1752 or update existing permissions if found.
1753 1753
1754 1754 This command can only be run using an |authtoken| with admin
1755 1755 permissions on the |repo|.
1756 1756
1757 1757 :param apiuser: This is filled automatically from the |authtoken|.
1758 1758 :type apiuser: AuthUser
1759 1759 :param repoid: Set the repository name or repository ID.
1760 1760 :type repoid: str or int
1761 1761 :param userid: Set the user name.
1762 1762 :type userid: str
1763 1763 :param perm: Set the user permissions, using the following format
1764 1764 ``(repository.(none|read|write|admin))``
1765 1765 :type perm: str
1766 1766
1767 1767 Example output:
1768 1768
1769 1769 .. code-block:: bash
1770 1770
1771 1771 id : <id_given_in_input>
1772 1772 result: {
1773 1773 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1774 1774 "success": true
1775 1775 }
1776 1776 error: null
1777 1777 """
1778 1778
1779 1779 repo = get_repo_or_error(repoid)
1780 1780 user = get_user_or_error(userid)
1781 1781 perm = get_perm_or_error(perm)
1782 1782 if not has_superadmin_permission(apiuser):
1783 1783 _perms = ('repository.admin',)
1784 1784 validate_repo_permissions(apiuser, repoid, repo, _perms)
1785 1785
1786 1786 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1787 1787 try:
1788 1788 changes = RepoModel().update_permissions(
1789 1789 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1790 1790
1791 1791 action_data = {
1792 1792 'added': changes['added'],
1793 1793 'updated': changes['updated'],
1794 1794 'deleted': changes['deleted'],
1795 1795 }
1796 1796 audit_logger.store_api(
1797 1797 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1798 1798 Session().commit()
1799 1799 PermissionModel().flush_user_permission_caches(changes)
1800 1800
1801 1801 return {
1802 1802 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1803 1803 perm.permission_name, user.username, repo.repo_name
1804 1804 ),
1805 1805 'success': True
1806 1806 }
1807 1807 except Exception:
1808 1808 log.exception("Exception occurred while trying edit permissions for repo")
1809 1809 raise JSONRPCError(
1810 1810 'failed to edit permission for user: `%s` in repo: `%s`' % (
1811 1811 userid, repoid
1812 1812 )
1813 1813 )
1814 1814
1815 1815
1816 1816 @jsonrpc_method()
1817 1817 def revoke_user_permission(request, apiuser, repoid, userid):
1818 1818 """
1819 1819 Revoke permission for a user on the specified repository.
1820 1820
1821 1821 This command can only be run using an |authtoken| with admin
1822 1822 permissions on the |repo|.
1823 1823
1824 1824 :param apiuser: This is filled automatically from the |authtoken|.
1825 1825 :type apiuser: AuthUser
1826 1826 :param repoid: Set the repository name or repository ID.
1827 1827 :type repoid: str or int
1828 1828 :param userid: Set the user name of revoked user.
1829 1829 :type userid: str or int
1830 1830
1831 1831 Example error output:
1832 1832
1833 1833 .. code-block:: bash
1834 1834
1835 1835 id : <id_given_in_input>
1836 1836 result: {
1837 1837 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1838 1838 "success": true
1839 1839 }
1840 1840 error: null
1841 1841 """
1842 1842
1843 1843 repo = get_repo_or_error(repoid)
1844 1844 user = get_user_or_error(userid)
1845 1845 if not has_superadmin_permission(apiuser):
1846 1846 _perms = ('repository.admin',)
1847 1847 validate_repo_permissions(apiuser, repoid, repo, _perms)
1848 1848
1849 1849 perm_deletions = [[user.user_id, None, "user"]]
1850 1850 try:
1851 1851 changes = RepoModel().update_permissions(
1852 1852 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1853 1853
1854 1854 action_data = {
1855 1855 'added': changes['added'],
1856 1856 'updated': changes['updated'],
1857 1857 'deleted': changes['deleted'],
1858 1858 }
1859 1859 audit_logger.store_api(
1860 1860 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1861 1861 Session().commit()
1862 1862 PermissionModel().flush_user_permission_caches(changes)
1863 1863
1864 1864 return {
1865 1865 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1866 1866 user.username, repo.repo_name
1867 1867 ),
1868 1868 'success': True
1869 1869 }
1870 1870 except Exception:
1871 1871 log.exception("Exception occurred while trying revoke permissions to repo")
1872 1872 raise JSONRPCError(
1873 1873 'failed to edit permission for user: `%s` in repo: `%s`' % (
1874 1874 userid, repoid
1875 1875 )
1876 1876 )
1877 1877
1878 1878
1879 1879 @jsonrpc_method()
1880 1880 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1881 1881 """
1882 1882 Grant permission for a user group on the specified repository,
1883 1883 or update existing permissions.
1884 1884
1885 1885 This command can only be run using an |authtoken| with admin
1886 1886 permissions on the |repo|.
1887 1887
1888 1888 :param apiuser: This is filled automatically from the |authtoken|.
1889 1889 :type apiuser: AuthUser
1890 1890 :param repoid: Set the repository name or repository ID.
1891 1891 :type repoid: str or int
1892 1892 :param usergroupid: Specify the ID of the user group.
1893 1893 :type usergroupid: str or int
1894 1894 :param perm: Set the user group permissions using the following
1895 1895 format: (repository.(none|read|write|admin))
1896 1896 :type perm: str
1897 1897
1898 1898 Example output:
1899 1899
1900 1900 .. code-block:: bash
1901 1901
1902 1902 id : <id_given_in_input>
1903 1903 result : {
1904 1904 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1905 1905 "success": true
1906 1906
1907 1907 }
1908 1908 error : null
1909 1909
1910 1910 Example error output:
1911 1911
1912 1912 .. code-block:: bash
1913 1913
1914 1914 id : <id_given_in_input>
1915 1915 result : null
1916 1916 error : {
1917 1917 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1918 1918 }
1919 1919
1920 1920 """
1921 1921
1922 1922 repo = get_repo_or_error(repoid)
1923 1923 perm = get_perm_or_error(perm)
1924 1924 if not has_superadmin_permission(apiuser):
1925 1925 _perms = ('repository.admin',)
1926 1926 validate_repo_permissions(apiuser, repoid, repo, _perms)
1927 1927
1928 1928 user_group = get_user_group_or_error(usergroupid)
1929 1929 if not has_superadmin_permission(apiuser):
1930 1930 # check if we have at least read permission for this user group !
1931 1931 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1932 1932 if not HasUserGroupPermissionAnyApi(*_perms)(
1933 1933 user=apiuser, user_group_name=user_group.users_group_name):
1934 1934 raise JSONRPCError(
1935 1935 'user group `%s` does not exist' % (usergroupid,))
1936 1936
1937 1937 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1938 1938 try:
1939 1939 changes = RepoModel().update_permissions(
1940 1940 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1941 1941 action_data = {
1942 1942 'added': changes['added'],
1943 1943 'updated': changes['updated'],
1944 1944 'deleted': changes['deleted'],
1945 1945 }
1946 1946 audit_logger.store_api(
1947 1947 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1948 1948 Session().commit()
1949 1949 PermissionModel().flush_user_permission_caches(changes)
1950 1950
1951 1951 return {
1952 1952 'msg': 'Granted perm: `%s` for user group: `%s` in '
1953 1953 'repo: `%s`' % (
1954 1954 perm.permission_name, user_group.users_group_name,
1955 1955 repo.repo_name
1956 1956 ),
1957 1957 'success': True
1958 1958 }
1959 1959 except Exception:
1960 1960 log.exception(
1961 1961 "Exception occurred while trying change permission on repo")
1962 1962 raise JSONRPCError(
1963 1963 'failed to edit permission for user group: `%s` in '
1964 1964 'repo: `%s`' % (
1965 1965 usergroupid, repo.repo_name
1966 1966 )
1967 1967 )
1968 1968
1969 1969
1970 1970 @jsonrpc_method()
1971 1971 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1972 1972 """
1973 1973 Revoke the permissions of a user group on a given repository.
1974 1974
1975 1975 This command can only be run using an |authtoken| with admin
1976 1976 permissions on the |repo|.
1977 1977
1978 1978 :param apiuser: This is filled automatically from the |authtoken|.
1979 1979 :type apiuser: AuthUser
1980 1980 :param repoid: Set the repository name or repository ID.
1981 1981 :type repoid: str or int
1982 1982 :param usergroupid: Specify the user group ID.
1983 1983 :type usergroupid: str or int
1984 1984
1985 1985 Example output:
1986 1986
1987 1987 .. code-block:: bash
1988 1988
1989 1989 id : <id_given_in_input>
1990 1990 result: {
1991 1991 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1992 1992 "success": true
1993 1993 }
1994 1994 error: null
1995 1995 """
1996 1996
1997 1997 repo = get_repo_or_error(repoid)
1998 1998 if not has_superadmin_permission(apiuser):
1999 1999 _perms = ('repository.admin',)
2000 2000 validate_repo_permissions(apiuser, repoid, repo, _perms)
2001 2001
2002 2002 user_group = get_user_group_or_error(usergroupid)
2003 2003 if not has_superadmin_permission(apiuser):
2004 2004 # check if we have at least read permission for this user group !
2005 2005 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2006 2006 if not HasUserGroupPermissionAnyApi(*_perms)(
2007 2007 user=apiuser, user_group_name=user_group.users_group_name):
2008 2008 raise JSONRPCError(
2009 2009 'user group `%s` does not exist' % (usergroupid,))
2010 2010
2011 2011 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2012 2012 try:
2013 2013 changes = RepoModel().update_permissions(
2014 2014 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2015 2015 action_data = {
2016 2016 'added': changes['added'],
2017 2017 'updated': changes['updated'],
2018 2018 'deleted': changes['deleted'],
2019 2019 }
2020 2020 audit_logger.store_api(
2021 2021 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2022 2022 Session().commit()
2023 2023 PermissionModel().flush_user_permission_caches(changes)
2024 2024
2025 2025 return {
2026 2026 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2027 2027 user_group.users_group_name, repo.repo_name
2028 2028 ),
2029 2029 'success': True
2030 2030 }
2031 2031 except Exception:
2032 2032 log.exception("Exception occurred while trying revoke "
2033 2033 "user group permission on repo")
2034 2034 raise JSONRPCError(
2035 2035 'failed to edit permission for user group: `%s` in '
2036 2036 'repo: `%s`' % (
2037 2037 user_group.users_group_name, repo.repo_name
2038 2038 )
2039 2039 )
2040 2040
2041 2041
2042 2042 @jsonrpc_method()
2043 2043 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2044 2044 """
2045 2045 Triggers a pull on the given repository from a remote location. You
2046 2046 can use this to keep remote repositories up-to-date.
2047 2047
2048 2048 This command can only be run using an |authtoken| with admin
2049 2049 rights to the specified repository. For more information,
2050 2050 see :ref:`config-token-ref`.
2051 2051
2052 2052 This command takes the following options:
2053 2053
2054 2054 :param apiuser: This is filled automatically from the |authtoken|.
2055 2055 :type apiuser: AuthUser
2056 2056 :param repoid: The repository name or repository ID.
2057 2057 :type repoid: str or int
2058 2058 :param remote_uri: Optional remote URI to pass in for pull
2059 2059 :type remote_uri: str
2060 2060
2061 2061 Example output:
2062 2062
2063 2063 .. code-block:: bash
2064 2064
2065 2065 id : <id_given_in_input>
2066 2066 result : {
2067 2067 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2068 2068 "repository": "<repository name>"
2069 2069 }
2070 2070 error : null
2071 2071
2072 2072 Example error output:
2073 2073
2074 2074 .. code-block:: bash
2075 2075
2076 2076 id : <id_given_in_input>
2077 2077 result : null
2078 2078 error : {
2079 2079 "Unable to push changes from `<remote_url>`"
2080 2080 }
2081 2081
2082 2082 """
2083 2083
2084 2084 repo = get_repo_or_error(repoid)
2085 2085 remote_uri = Optional.extract(remote_uri)
2086 2086 remote_uri_display = remote_uri or repo.clone_uri_hidden
2087 2087 if not has_superadmin_permission(apiuser):
2088 2088 _perms = ('repository.admin',)
2089 2089 validate_repo_permissions(apiuser, repoid, repo, _perms)
2090 2090
2091 2091 try:
2092 2092 ScmModel().pull_changes(
2093 2093 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2094 2094 return {
2095 2095 'msg': 'Pulled from url `%s` on repo `%s`' % (
2096 2096 remote_uri_display, repo.repo_name),
2097 2097 'repository': repo.repo_name
2098 2098 }
2099 2099 except Exception:
2100 2100 log.exception("Exception occurred while trying to "
2101 2101 "pull changes from remote location")
2102 2102 raise JSONRPCError(
2103 2103 'Unable to pull changes from `%s`' % remote_uri_display
2104 2104 )
2105 2105
2106 2106
2107 2107 @jsonrpc_method()
2108 2108 def strip(request, apiuser, repoid, revision, branch):
2109 2109 """
2110 2110 Strips the given revision from the specified repository.
2111 2111
2112 2112 * This will remove the revision and all of its decendants.
2113 2113
2114 2114 This command can only be run using an |authtoken| with admin rights to
2115 2115 the specified repository.
2116 2116
2117 2117 This command takes the following options:
2118 2118
2119 2119 :param apiuser: This is filled automatically from the |authtoken|.
2120 2120 :type apiuser: AuthUser
2121 2121 :param repoid: The repository name or repository ID.
2122 2122 :type repoid: str or int
2123 2123 :param revision: The revision you wish to strip.
2124 2124 :type revision: str
2125 2125 :param branch: The branch from which to strip the revision.
2126 2126 :type branch: str
2127 2127
2128 2128 Example output:
2129 2129
2130 2130 .. code-block:: bash
2131 2131
2132 2132 id : <id_given_in_input>
2133 2133 result : {
2134 2134 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2135 2135 "repository": "<repository name>"
2136 2136 }
2137 2137 error : null
2138 2138
2139 2139 Example error output:
2140 2140
2141 2141 .. code-block:: bash
2142 2142
2143 2143 id : <id_given_in_input>
2144 2144 result : null
2145 2145 error : {
2146 2146 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2147 2147 }
2148 2148
2149 2149 """
2150 2150
2151 2151 repo = get_repo_or_error(repoid)
2152 2152 if not has_superadmin_permission(apiuser):
2153 2153 _perms = ('repository.admin',)
2154 2154 validate_repo_permissions(apiuser, repoid, repo, _perms)
2155 2155
2156 2156 try:
2157 2157 ScmModel().strip(repo, revision, branch)
2158 2158 audit_logger.store_api(
2159 2159 'repo.commit.strip', action_data={'commit_id': revision},
2160 2160 repo=repo,
2161 2161 user=apiuser, commit=True)
2162 2162
2163 2163 return {
2164 2164 'msg': 'Stripped commit %s from repo `%s`' % (
2165 2165 revision, repo.repo_name),
2166 2166 'repository': repo.repo_name
2167 2167 }
2168 2168 except Exception:
2169 2169 log.exception("Exception while trying to strip")
2170 2170 raise JSONRPCError(
2171 2171 'Unable to strip commit %s from repo `%s`' % (
2172 2172 revision, repo.repo_name)
2173 2173 )
2174 2174
2175 2175
2176 2176 @jsonrpc_method()
2177 2177 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2178 2178 """
2179 2179 Returns all settings for a repository. If key is given it only returns the
2180 2180 setting identified by the key or null.
2181 2181
2182 2182 :param apiuser: This is filled automatically from the |authtoken|.
2183 2183 :type apiuser: AuthUser
2184 2184 :param repoid: The repository name or repository id.
2185 2185 :type repoid: str or int
2186 2186 :param key: Key of the setting to return.
2187 2187 :type: key: Optional(str)
2188 2188
2189 2189 Example output:
2190 2190
2191 2191 .. code-block:: bash
2192 2192
2193 2193 {
2194 2194 "error": null,
2195 2195 "id": 237,
2196 2196 "result": {
2197 2197 "extensions_largefiles": true,
2198 2198 "extensions_evolve": true,
2199 2199 "hooks_changegroup_push_logger": true,
2200 2200 "hooks_changegroup_repo_size": false,
2201 2201 "hooks_outgoing_pull_logger": true,
2202 2202 "phases_publish": "True",
2203 2203 "rhodecode_hg_use_rebase_for_merging": true,
2204 2204 "rhodecode_pr_merge_enabled": true,
2205 2205 "rhodecode_use_outdated_comments": true
2206 2206 }
2207 2207 }
2208 2208 """
2209 2209
2210 2210 # Restrict access to this api method to admins only.
2211 2211 if not has_superadmin_permission(apiuser):
2212 2212 raise JSONRPCForbidden()
2213 2213
2214 2214 try:
2215 2215 repo = get_repo_or_error(repoid)
2216 2216 settings_model = VcsSettingsModel(repo=repo)
2217 2217 settings = settings_model.get_global_settings()
2218 2218 settings.update(settings_model.get_repo_settings())
2219 2219
2220 2220 # If only a single setting is requested fetch it from all settings.
2221 2221 key = Optional.extract(key)
2222 2222 if key is not None:
2223 2223 settings = settings.get(key, None)
2224 2224 except Exception:
2225 2225 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2226 2226 log.exception(msg)
2227 2227 raise JSONRPCError(msg)
2228 2228
2229 2229 return settings
2230 2230
2231 2231
2232 2232 @jsonrpc_method()
2233 2233 def set_repo_settings(request, apiuser, repoid, settings):
2234 2234 """
2235 2235 Update repository settings. Returns true on success.
2236 2236
2237 2237 :param apiuser: This is filled automatically from the |authtoken|.
2238 2238 :type apiuser: AuthUser
2239 2239 :param repoid: The repository name or repository id.
2240 2240 :type repoid: str or int
2241 2241 :param settings: The new settings for the repository.
2242 2242 :type: settings: dict
2243 2243
2244 2244 Example output:
2245 2245
2246 2246 .. code-block:: bash
2247 2247
2248 2248 {
2249 2249 "error": null,
2250 2250 "id": 237,
2251 2251 "result": true
2252 2252 }
2253 2253 """
2254 2254 # Restrict access to this api method to admins only.
2255 2255 if not has_superadmin_permission(apiuser):
2256 2256 raise JSONRPCForbidden()
2257 2257
2258 2258 if type(settings) is not dict:
2259 2259 raise JSONRPCError('Settings have to be a JSON Object.')
2260 2260
2261 2261 try:
2262 2262 settings_model = VcsSettingsModel(repo=repoid)
2263 2263
2264 2264 # Merge global, repo and incoming settings.
2265 2265 new_settings = settings_model.get_global_settings()
2266 2266 new_settings.update(settings_model.get_repo_settings())
2267 2267 new_settings.update(settings)
2268 2268
2269 2269 # Update the settings.
2270 2270 inherit_global_settings = new_settings.get(
2271 2271 'inherit_global_settings', False)
2272 2272 settings_model.create_or_update_repo_settings(
2273 2273 new_settings, inherit_global_settings=inherit_global_settings)
2274 2274 Session().commit()
2275 2275 except Exception:
2276 2276 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2277 2277 log.exception(msg)
2278 2278 raise JSONRPCError(msg)
2279 2279
2280 2280 # Indicate success.
2281 2281 return True
2282 2282
2283 2283
2284 2284 @jsonrpc_method()
2285 2285 def maintenance(request, apiuser, repoid):
2286 2286 """
2287 2287 Triggers a maintenance on the given repository.
2288 2288
2289 2289 This command can only be run using an |authtoken| with admin
2290 2290 rights to the specified repository. For more information,
2291 2291 see :ref:`config-token-ref`.
2292 2292
2293 2293 This command takes the following options:
2294 2294
2295 2295 :param apiuser: This is filled automatically from the |authtoken|.
2296 2296 :type apiuser: AuthUser
2297 2297 :param repoid: The repository name or repository ID.
2298 2298 :type repoid: str or int
2299 2299
2300 2300 Example output:
2301 2301
2302 2302 .. code-block:: bash
2303 2303
2304 2304 id : <id_given_in_input>
2305 2305 result : {
2306 2306 "msg": "executed maintenance command",
2307 2307 "executed_actions": [
2308 2308 <action_message>, <action_message2>...
2309 2309 ],
2310 2310 "repository": "<repository name>"
2311 2311 }
2312 2312 error : null
2313 2313
2314 2314 Example error output:
2315 2315
2316 2316 .. code-block:: bash
2317 2317
2318 2318 id : <id_given_in_input>
2319 2319 result : null
2320 2320 error : {
2321 2321 "Unable to execute maintenance on `<reponame>`"
2322 2322 }
2323 2323
2324 2324 """
2325 2325
2326 2326 repo = get_repo_or_error(repoid)
2327 2327 if not has_superadmin_permission(apiuser):
2328 2328 _perms = ('repository.admin',)
2329 2329 validate_repo_permissions(apiuser, repoid, repo, _perms)
2330 2330
2331 2331 try:
2332 2332 maintenance = repo_maintenance.RepoMaintenance()
2333 2333 executed_actions = maintenance.execute(repo)
2334 2334
2335 2335 return {
2336 2336 'msg': 'executed maintenance command',
2337 2337 'executed_actions': executed_actions,
2338 2338 'repository': repo.repo_name
2339 2339 }
2340 2340 except Exception:
2341 2341 log.exception("Exception occurred while trying to run maintenance")
2342 2342 raise JSONRPCError(
2343 2343 'Unable to execute maintenance on `%s`' % repo.repo_name)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now