##// END OF EJS Templates
api: add send_email flag for comments api to allow commenting without email notification....
bart -
r4196:bb0e450f stable
parent child Browse files
Show More
@@ -1,60 +1,61 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
24 24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
25 25
26 26
27 27 @pytest.mark.usefixtures("testuser_api", "app")
28 28 class TestGetMethod(object):
29 29 def test_get_methods_no_matches(self):
30 30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
31 31 response = api_call(self.app, params)
32 32
33 33 expected = []
34 34 assert_ok(id_, expected, given=response.body)
35 35
36 36 def test_get_methods(self):
37 37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
38 38 response = api_call(self.app, params)
39 39
40 40 expected = ['changeset_comment', 'comment_pull_request',
41 41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
42 42 assert_ok(id_, expected, given=response.body)
43 43
44 44 def test_get_methods_on_single_match(self):
45 45 id_, params = build_data(self.apikey, 'get_method',
46 46 pattern='*comment_commit*')
47 47 response = api_call(self.app, params)
48 48
49 49 expected = ['comment_commit',
50 50 {'apiuser': '<RequiredType>',
51 51 'comment_type': "<Optional:u'note'>",
52 52 'commit_id': '<RequiredType>',
53 53 'extra_recipients': '<Optional:[]>',
54 54 'message': '<RequiredType>',
55 55 'repoid': '<RequiredType>',
56 56 'request': '<RequiredType>',
57 57 'resolves_comment_id': '<Optional:None>',
58 58 'status': '<Optional:None>',
59 'userid': '<Optional:<OptionalAttr:apiuser>>'}]
59 'userid': '<Optional:<OptionalAttr:apiuser>>',
60 'send_email': '<Optional:True>'}]
60 61 assert_ok(id_, expected, given=response.body)
@@ -1,1011 +1,1015 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 "commit_ids": [
77 77 ...
78 78 "<commit_id>",
79 79 "<commit_id>",
80 80 ...
81 81 ],
82 82 "review_status": "<review_status>",
83 83 "mergeable": {
84 84 "status": "<bool>",
85 85 "message": "<message>",
86 86 },
87 87 "source": {
88 88 "clone_url": "<clone_url>",
89 89 "repository": "<repository_name>",
90 90 "reference":
91 91 {
92 92 "name": "<name>",
93 93 "type": "<type>",
94 94 "commit_id": "<commit_id>",
95 95 }
96 96 },
97 97 "target": {
98 98 "clone_url": "<clone_url>",
99 99 "repository": "<repository_name>",
100 100 "reference":
101 101 {
102 102 "name": "<name>",
103 103 "type": "<type>",
104 104 "commit_id": "<commit_id>",
105 105 }
106 106 },
107 107 "merge": {
108 108 "clone_url": "<clone_url>",
109 109 "reference":
110 110 {
111 111 "name": "<name>",
112 112 "type": "<type>",
113 113 "commit_id": "<commit_id>",
114 114 }
115 115 },
116 116 "author": <user_obj>,
117 117 "reviewers": [
118 118 ...
119 119 {
120 120 "user": "<user_obj>",
121 121 "review_status": "<review_status>",
122 122 }
123 123 ...
124 124 ]
125 125 },
126 126 "error": null
127 127 """
128 128
129 129 pull_request = get_pull_request_or_error(pullrequestid)
130 130 if Optional.extract(repoid):
131 131 repo = get_repo_or_error(repoid)
132 132 else:
133 133 repo = pull_request.target_repo
134 134
135 135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
136 136 raise JSONRPCError('repository `%s` or pull request `%s` '
137 137 'does not exist' % (repoid, pullrequestid))
138 138
139 139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
140 140 # otherwise we can lock the repo on calculation of merge state while update/merge
141 141 # is happening.
142 142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
143 143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
144 144 data = pull_request.get_api_data(with_merge_state=merge_state)
145 145 return data
146 146
147 147
148 148 @jsonrpc_method()
149 149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
150 150 merge_state=Optional(False)):
151 151 """
152 152 Get all pull requests from the repository specified in `repoid`.
153 153
154 154 :param apiuser: This is filled automatically from the |authtoken|.
155 155 :type apiuser: AuthUser
156 156 :param repoid: Optional repository name or repository ID.
157 157 :type repoid: str or int
158 158 :param status: Only return pull requests with the specified status.
159 159 Valid options are.
160 160 * ``new`` (default)
161 161 * ``open``
162 162 * ``closed``
163 163 :type status: str
164 164 :param merge_state: Optional calculate merge state for each repository.
165 165 This could result in longer time to fetch the data
166 166 :type merge_state: bool
167 167
168 168 Example output:
169 169
170 170 .. code-block:: bash
171 171
172 172 "id": <id_given_in_input>,
173 173 "result":
174 174 [
175 175 ...
176 176 {
177 177 "pull_request_id": "<pull_request_id>",
178 178 "url": "<url>",
179 179 "title" : "<title>",
180 180 "description": "<description>",
181 181 "status": "<status>",
182 182 "created_on": "<date_time_created>",
183 183 "updated_on": "<date_time_updated>",
184 184 "commit_ids": [
185 185 ...
186 186 "<commit_id>",
187 187 "<commit_id>",
188 188 ...
189 189 ],
190 190 "review_status": "<review_status>",
191 191 "mergeable": {
192 192 "status": "<bool>",
193 193 "message: "<message>",
194 194 },
195 195 "source": {
196 196 "clone_url": "<clone_url>",
197 197 "reference":
198 198 {
199 199 "name": "<name>",
200 200 "type": "<type>",
201 201 "commit_id": "<commit_id>",
202 202 }
203 203 },
204 204 "target": {
205 205 "clone_url": "<clone_url>",
206 206 "reference":
207 207 {
208 208 "name": "<name>",
209 209 "type": "<type>",
210 210 "commit_id": "<commit_id>",
211 211 }
212 212 },
213 213 "merge": {
214 214 "clone_url": "<clone_url>",
215 215 "reference":
216 216 {
217 217 "name": "<name>",
218 218 "type": "<type>",
219 219 "commit_id": "<commit_id>",
220 220 }
221 221 },
222 222 "author": <user_obj>,
223 223 "reviewers": [
224 224 ...
225 225 {
226 226 "user": "<user_obj>",
227 227 "review_status": "<review_status>",
228 228 }
229 229 ...
230 230 ]
231 231 }
232 232 ...
233 233 ],
234 234 "error": null
235 235
236 236 """
237 237 repo = get_repo_or_error(repoid)
238 238 if not has_superadmin_permission(apiuser):
239 239 _perms = (
240 240 'repository.admin', 'repository.write', 'repository.read',)
241 241 validate_repo_permissions(apiuser, repoid, repo, _perms)
242 242
243 243 status = Optional.extract(status)
244 244 merge_state = Optional.extract(merge_state, binary=True)
245 245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
246 246 order_by='id', order_dir='desc')
247 247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
248 248 return data
249 249
250 250
251 251 @jsonrpc_method()
252 252 def merge_pull_request(
253 253 request, apiuser, pullrequestid, repoid=Optional(None),
254 254 userid=Optional(OAttr('apiuser'))):
255 255 """
256 256 Merge the pull request specified by `pullrequestid` into its target
257 257 repository.
258 258
259 259 :param apiuser: This is filled automatically from the |authtoken|.
260 260 :type apiuser: AuthUser
261 261 :param repoid: Optional, repository name or repository ID of the
262 262 target repository to which the |pr| is to be merged.
263 263 :type repoid: str or int
264 264 :param pullrequestid: ID of the pull request which shall be merged.
265 265 :type pullrequestid: int
266 266 :param userid: Merge the pull request as this user.
267 267 :type userid: Optional(str or int)
268 268
269 269 Example output:
270 270
271 271 .. code-block:: bash
272 272
273 273 "id": <id_given_in_input>,
274 274 "result": {
275 275 "executed": "<bool>",
276 276 "failure_reason": "<int>",
277 277 "merge_status_message": "<str>",
278 278 "merge_commit_id": "<merge_commit_id>",
279 279 "possible": "<bool>",
280 280 "merge_ref": {
281 281 "commit_id": "<commit_id>",
282 282 "type": "<type>",
283 283 "name": "<name>"
284 284 }
285 285 },
286 286 "error": null
287 287 """
288 288 pull_request = get_pull_request_or_error(pullrequestid)
289 289 if Optional.extract(repoid):
290 290 repo = get_repo_or_error(repoid)
291 291 else:
292 292 repo = pull_request.target_repo
293 293 auth_user = apiuser
294 294 if not isinstance(userid, Optional):
295 295 if (has_superadmin_permission(apiuser) or
296 296 HasRepoPermissionAnyApi('repository.admin')(
297 297 user=apiuser, repo_name=repo.repo_name)):
298 298 apiuser = get_user_or_error(userid)
299 299 auth_user = apiuser.AuthUser()
300 300 else:
301 301 raise JSONRPCError('userid is not the same as your user')
302 302
303 303 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 304 raise JSONRPCError(
305 305 'Operation forbidden because pull request is in state {}, '
306 306 'only state {} is allowed.'.format(
307 307 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308 308
309 309 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 310 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 311 translator=request.translate)
312 312 merge_possible = not check.failed
313 313
314 314 if not merge_possible:
315 315 error_messages = []
316 316 for err_type, error_msg in check.errors:
317 317 error_msg = request.translate(error_msg)
318 318 error_messages.append(error_msg)
319 319
320 320 reasons = ','.join(error_messages)
321 321 raise JSONRPCError(
322 322 'merge not possible for following reasons: {}'.format(reasons))
323 323
324 324 target_repo = pull_request.target_repo
325 325 extras = vcs_operation_context(
326 326 request.environ, repo_name=target_repo.repo_name,
327 327 username=auth_user.username, action='push',
328 328 scm=target_repo.repo_type)
329 329 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 330 merge_response = PullRequestModel().merge_repo(
331 331 pull_request, apiuser, extras=extras)
332 332 if merge_response.executed:
333 333 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334 334
335 335 Session().commit()
336 336
337 337 # In previous versions the merge response directly contained the merge
338 338 # commit id. It is now contained in the merge reference object. To be
339 339 # backwards compatible we have to extract it again.
340 340 merge_response = merge_response.asdict()
341 341 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342 342
343 343 return merge_response
344 344
345 345
346 346 @jsonrpc_method()
347 347 def get_pull_request_comments(
348 348 request, apiuser, pullrequestid, repoid=Optional(None)):
349 349 """
350 350 Get all comments of pull request specified with the `pullrequestid`
351 351
352 352 :param apiuser: This is filled automatically from the |authtoken|.
353 353 :type apiuser: AuthUser
354 354 :param repoid: Optional repository name or repository ID.
355 355 :type repoid: str or int
356 356 :param pullrequestid: The pull request ID.
357 357 :type pullrequestid: int
358 358
359 359 Example output:
360 360
361 361 .. code-block:: bash
362 362
363 363 id : <id_given_in_input>
364 364 result : [
365 365 {
366 366 "comment_author": {
367 367 "active": true,
368 368 "full_name_or_username": "Tom Gore",
369 369 "username": "admin"
370 370 },
371 371 "comment_created_on": "2017-01-02T18:43:45.533",
372 372 "comment_f_path": null,
373 373 "comment_id": 25,
374 374 "comment_lineno": null,
375 375 "comment_status": {
376 376 "status": "under_review",
377 377 "status_lbl": "Under Review"
378 378 },
379 379 "comment_text": "Example text",
380 380 "comment_type": null,
381 381 "pull_request_version": null
382 382 }
383 383 ],
384 384 error : null
385 385 """
386 386
387 387 pull_request = get_pull_request_or_error(pullrequestid)
388 388 if Optional.extract(repoid):
389 389 repo = get_repo_or_error(repoid)
390 390 else:
391 391 repo = pull_request.target_repo
392 392
393 393 if not PullRequestModel().check_user_read(
394 394 pull_request, apiuser, api=True):
395 395 raise JSONRPCError('repository `%s` or pull request `%s` '
396 396 'does not exist' % (repoid, pullrequestid))
397 397
398 398 (pull_request_latest,
399 399 pull_request_at_ver,
400 400 pull_request_display_obj,
401 401 at_version) = PullRequestModel().get_pr_version(
402 402 pull_request.pull_request_id, version=None)
403 403
404 404 versions = pull_request_display_obj.versions()
405 405 ver_map = {
406 406 ver.pull_request_version_id: cnt
407 407 for cnt, ver in enumerate(versions, 1)
408 408 }
409 409
410 410 # GENERAL COMMENTS with versions #
411 411 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
412 412 q = q.order_by(ChangesetComment.comment_id.asc())
413 413 general_comments = q.all()
414 414
415 415 # INLINE COMMENTS with versions #
416 416 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
417 417 q = q.order_by(ChangesetComment.comment_id.asc())
418 418 inline_comments = q.all()
419 419
420 420 data = []
421 421 for comment in inline_comments + general_comments:
422 422 full_data = comment.get_api_data()
423 423 pr_version_id = None
424 424 if comment.pull_request_version_id:
425 425 pr_version_id = 'v{}'.format(
426 426 ver_map[comment.pull_request_version_id])
427 427
428 428 # sanitize some entries
429 429
430 430 full_data['pull_request_version'] = pr_version_id
431 431 full_data['comment_author'] = {
432 432 'username': full_data['comment_author'].username,
433 433 'full_name_or_username': full_data['comment_author'].full_name_or_username,
434 434 'active': full_data['comment_author'].active,
435 435 }
436 436
437 437 if full_data['comment_status']:
438 438 full_data['comment_status'] = {
439 439 'status': full_data['comment_status'][0].status,
440 440 'status_lbl': full_data['comment_status'][0].status_lbl,
441 441 }
442 442 else:
443 443 full_data['comment_status'] = {}
444 444
445 445 data.append(full_data)
446 446 return data
447 447
448 448
449 449 @jsonrpc_method()
450 450 def comment_pull_request(
451 451 request, apiuser, pullrequestid, repoid=Optional(None),
452 452 message=Optional(None), commit_id=Optional(None), status=Optional(None),
453 453 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
454 454 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
455 userid=Optional(OAttr('apiuser'))):
455 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
456 456 """
457 457 Comment on the pull request specified with the `pullrequestid`,
458 458 in the |repo| specified by the `repoid`, and optionally change the
459 459 review status.
460 460
461 461 :param apiuser: This is filled automatically from the |authtoken|.
462 462 :type apiuser: AuthUser
463 463 :param repoid: Optional repository name or repository ID.
464 464 :type repoid: str or int
465 465 :param pullrequestid: The pull request ID.
466 466 :type pullrequestid: int
467 467 :param commit_id: Specify the commit_id for which to set a comment. If
468 468 given commit_id is different than latest in the PR status
469 469 change won't be performed.
470 470 :type commit_id: str
471 471 :param message: The text content of the comment.
472 472 :type message: str
473 473 :param status: (**Optional**) Set the approval status of the pull
474 474 request. One of: 'not_reviewed', 'approved', 'rejected',
475 475 'under_review'
476 476 :type status: str
477 477 :param comment_type: Comment type, one of: 'note', 'todo'
478 478 :type comment_type: Optional(str), default: 'note'
479 479 :param resolves_comment_id: id of comment which this one will resolve
480 480 :type resolves_comment_id: Optional(int)
481 481 :param extra_recipients: list of user ids or usernames to add
482 482 notifications for this comment. Acts like a CC for notification
483 483 :type extra_recipients: Optional(list)
484 484 :param userid: Comment on the pull request as this user
485 485 :type userid: Optional(str or int)
486 :param send_email: Define if this comment should also send email notification
487 :type send_email: Optional(bool)
486 488
487 489 Example output:
488 490
489 491 .. code-block:: bash
490 492
491 493 id : <id_given_in_input>
492 494 result : {
493 495 "pull_request_id": "<Integer>",
494 496 "comment_id": "<Integer>",
495 497 "status": {"given": <given_status>,
496 498 "was_changed": <bool status_was_actually_changed> },
497 499 },
498 500 error : null
499 501 """
500 502 pull_request = get_pull_request_or_error(pullrequestid)
501 503 if Optional.extract(repoid):
502 504 repo = get_repo_or_error(repoid)
503 505 else:
504 506 repo = pull_request.target_repo
505 507
506 508 auth_user = apiuser
507 509 if not isinstance(userid, Optional):
508 510 if (has_superadmin_permission(apiuser) or
509 511 HasRepoPermissionAnyApi('repository.admin')(
510 512 user=apiuser, repo_name=repo.repo_name)):
511 513 apiuser = get_user_or_error(userid)
512 514 auth_user = apiuser.AuthUser()
513 515 else:
514 516 raise JSONRPCError('userid is not the same as your user')
515 517
516 518 if pull_request.is_closed():
517 519 raise JSONRPCError(
518 520 'pull request `%s` comment failed, pull request is closed' % (
519 521 pullrequestid,))
520 522
521 523 if not PullRequestModel().check_user_read(
522 524 pull_request, apiuser, api=True):
523 525 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
524 526 message = Optional.extract(message)
525 527 status = Optional.extract(status)
526 528 commit_id = Optional.extract(commit_id)
527 529 comment_type = Optional.extract(comment_type)
528 530 resolves_comment_id = Optional.extract(resolves_comment_id)
529 531 extra_recipients = Optional.extract(extra_recipients)
532 send_email = Optional.extract(send_email, binary=True)
530 533
531 534 if not message and not status:
532 535 raise JSONRPCError(
533 536 'Both message and status parameters are missing. '
534 537 'At least one is required.')
535 538
536 539 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
537 540 status is not None):
538 541 raise JSONRPCError('Unknown comment status: `%s`' % status)
539 542
540 543 if commit_id and commit_id not in pull_request.revisions:
541 544 raise JSONRPCError(
542 545 'Invalid commit_id `%s` for this pull request.' % commit_id)
543 546
544 547 allowed_to_change_status = PullRequestModel().check_user_change_status(
545 548 pull_request, apiuser)
546 549
547 550 # if commit_id is passed re-validated if user is allowed to change status
548 551 # based on latest commit_id from the PR
549 552 if commit_id:
550 553 commit_idx = pull_request.revisions.index(commit_id)
551 554 if commit_idx != 0:
552 555 allowed_to_change_status = False
553 556
554 557 if resolves_comment_id:
555 558 comment = ChangesetComment.get(resolves_comment_id)
556 559 if not comment:
557 560 raise JSONRPCError(
558 561 'Invalid resolves_comment_id `%s` for this pull request.'
559 562 % resolves_comment_id)
560 563 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
561 564 raise JSONRPCError(
562 565 'Comment `%s` is wrong type for setting status to resolved.'
563 566 % resolves_comment_id)
564 567
565 568 text = message
566 569 status_label = ChangesetStatus.get_status_lbl(status)
567 570 if status and allowed_to_change_status:
568 571 st_message = ('Status change %(transition_icon)s %(status)s'
569 572 % {'transition_icon': '>', 'status': status_label})
570 573 text = message or st_message
571 574
572 575 rc_config = SettingsModel().get_all_settings()
573 576 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
574 577
575 578 status_change = status and allowed_to_change_status
576 579 comment = CommentsModel().create(
577 580 text=text,
578 581 repo=pull_request.target_repo.repo_id,
579 582 user=apiuser.user_id,
580 583 pull_request=pull_request.pull_request_id,
581 584 f_path=None,
582 585 line_no=None,
583 586 status_change=(status_label if status_change else None),
584 587 status_change_type=(status if status_change else None),
585 588 closing_pr=False,
586 589 renderer=renderer,
587 590 comment_type=comment_type,
588 591 resolves_comment_id=resolves_comment_id,
589 592 auth_user=auth_user,
590 extra_recipients=extra_recipients
593 extra_recipients=extra_recipients,
594 send_email=send_email
591 595 )
592 596
593 597 if allowed_to_change_status and status:
594 598 old_calculated_status = pull_request.calculated_review_status()
595 599 ChangesetStatusModel().set_status(
596 600 pull_request.target_repo.repo_id,
597 601 status,
598 602 apiuser.user_id,
599 603 comment,
600 604 pull_request=pull_request.pull_request_id
601 605 )
602 606 Session().flush()
603 607
604 608 Session().commit()
605 609
606 610 PullRequestModel().trigger_pull_request_hook(
607 611 pull_request, apiuser, 'comment',
608 612 data={'comment': comment})
609 613
610 614 if allowed_to_change_status and status:
611 615 # we now calculate the status of pull request, and based on that
612 616 # calculation we set the commits status
613 617 calculated_status = pull_request.calculated_review_status()
614 618 if old_calculated_status != calculated_status:
615 619 PullRequestModel().trigger_pull_request_hook(
616 620 pull_request, apiuser, 'review_status_change',
617 621 data={'status': calculated_status})
618 622
619 623 data = {
620 624 'pull_request_id': pull_request.pull_request_id,
621 625 'comment_id': comment.comment_id if comment else None,
622 626 'status': {'given': status, 'was_changed': status_change},
623 627 }
624 628 return data
625 629
626 630
627 631 @jsonrpc_method()
628 632 def create_pull_request(
629 633 request, apiuser, source_repo, target_repo, source_ref, target_ref,
630 634 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
631 635 description_renderer=Optional(''), reviewers=Optional(None)):
632 636 """
633 637 Creates a new pull request.
634 638
635 639 Accepts refs in the following formats:
636 640
637 641 * branch:<branch_name>:<sha>
638 642 * branch:<branch_name>
639 643 * bookmark:<bookmark_name>:<sha> (Mercurial only)
640 644 * bookmark:<bookmark_name> (Mercurial only)
641 645
642 646 :param apiuser: This is filled automatically from the |authtoken|.
643 647 :type apiuser: AuthUser
644 648 :param source_repo: Set the source repository name.
645 649 :type source_repo: str
646 650 :param target_repo: Set the target repository name.
647 651 :type target_repo: str
648 652 :param source_ref: Set the source ref name.
649 653 :type source_ref: str
650 654 :param target_ref: Set the target ref name.
651 655 :type target_ref: str
652 656 :param owner: user_id or username
653 657 :type owner: Optional(str)
654 658 :param title: Optionally Set the pull request title, it's generated otherwise
655 659 :type title: str
656 660 :param description: Set the pull request description.
657 661 :type description: Optional(str)
658 662 :type description_renderer: Optional(str)
659 663 :param description_renderer: Set pull request renderer for the description.
660 664 It should be 'rst', 'markdown' or 'plain'. If not give default
661 665 system renderer will be used
662 666 :param reviewers: Set the new pull request reviewers list.
663 667 Reviewer defined by review rules will be added automatically to the
664 668 defined list.
665 669 :type reviewers: Optional(list)
666 670 Accepts username strings or objects of the format:
667 671
668 672 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
669 673 """
670 674
671 675 source_db_repo = get_repo_or_error(source_repo)
672 676 target_db_repo = get_repo_or_error(target_repo)
673 677 if not has_superadmin_permission(apiuser):
674 678 _perms = ('repository.admin', 'repository.write', 'repository.read',)
675 679 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
676 680
677 681 owner = validate_set_owner_permissions(apiuser, owner)
678 682
679 683 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
680 684 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
681 685
682 686 source_scm = source_db_repo.scm_instance()
683 687 target_scm = target_db_repo.scm_instance()
684 688
685 689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
686 690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
687 691
688 692 ancestor = source_scm.get_common_ancestor(
689 693 source_commit.raw_id, target_commit.raw_id, target_scm)
690 694 if not ancestor:
691 695 raise JSONRPCError('no common ancestor found')
692 696
693 697 # recalculate target ref based on ancestor
694 698 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
695 699 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
696 700
697 701 commit_ranges = target_scm.compare(
698 702 target_commit.raw_id, source_commit.raw_id, source_scm,
699 703 merge=True, pre_load=[])
700 704
701 705 if not commit_ranges:
702 706 raise JSONRPCError('no commits found')
703 707
704 708 reviewer_objects = Optional.extract(reviewers) or []
705 709
706 710 # serialize and validate passed in given reviewers
707 711 if reviewer_objects:
708 712 schema = ReviewerListSchema()
709 713 try:
710 714 reviewer_objects = schema.deserialize(reviewer_objects)
711 715 except Invalid as err:
712 716 raise JSONRPCValidationError(colander_exc=err)
713 717
714 718 # validate users
715 719 for reviewer_object in reviewer_objects:
716 720 user = get_user_or_error(reviewer_object['username'])
717 721 reviewer_object['user_id'] = user.user_id
718 722
719 723 get_default_reviewers_data, validate_default_reviewers = \
720 724 PullRequestModel().get_reviewer_functions()
721 725
722 726 # recalculate reviewers logic, to make sure we can validate this
723 727 reviewer_rules = get_default_reviewers_data(
724 728 owner, source_db_repo,
725 729 source_commit, target_db_repo, target_commit)
726 730
727 731 # now MERGE our given with the calculated
728 732 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
729 733
730 734 try:
731 735 reviewers = validate_default_reviewers(
732 736 reviewer_objects, reviewer_rules)
733 737 except ValueError as e:
734 738 raise JSONRPCError('Reviewers Validation: {}'.format(e))
735 739
736 740 title = Optional.extract(title)
737 741 if not title:
738 742 title_source_ref = source_ref.split(':', 2)[1]
739 743 title = PullRequestModel().generate_pullrequest_title(
740 744 source=source_repo,
741 745 source_ref=title_source_ref,
742 746 target=target_repo
743 747 )
744 748 # fetch renderer, if set fallback to plain in case of PR
745 749 rc_config = SettingsModel().get_all_settings()
746 750 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
747 751 description = Optional.extract(description)
748 752 description_renderer = Optional.extract(description_renderer) or default_system_renderer
749 753
750 754 pull_request = PullRequestModel().create(
751 755 created_by=owner.user_id,
752 756 source_repo=source_repo,
753 757 source_ref=full_source_ref,
754 758 target_repo=target_repo,
755 759 target_ref=full_target_ref,
756 760 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
757 761 reviewers=reviewers,
758 762 title=title,
759 763 description=description,
760 764 description_renderer=description_renderer,
761 765 reviewer_data=reviewer_rules,
762 766 auth_user=apiuser
763 767 )
764 768
765 769 Session().commit()
766 770 data = {
767 771 'msg': 'Created new pull request `{}`'.format(title),
768 772 'pull_request_id': pull_request.pull_request_id,
769 773 }
770 774 return data
771 775
772 776
773 777 @jsonrpc_method()
774 778 def update_pull_request(
775 779 request, apiuser, pullrequestid, repoid=Optional(None),
776 780 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
777 781 reviewers=Optional(None), update_commits=Optional(None)):
778 782 """
779 783 Updates a pull request.
780 784
781 785 :param apiuser: This is filled automatically from the |authtoken|.
782 786 :type apiuser: AuthUser
783 787 :param repoid: Optional repository name or repository ID.
784 788 :type repoid: str or int
785 789 :param pullrequestid: The pull request ID.
786 790 :type pullrequestid: int
787 791 :param title: Set the pull request title.
788 792 :type title: str
789 793 :param description: Update pull request description.
790 794 :type description: Optional(str)
791 795 :type description_renderer: Optional(str)
792 796 :param description_renderer: Update pull request renderer for the description.
793 797 It should be 'rst', 'markdown' or 'plain'
794 798 :param reviewers: Update pull request reviewers list with new value.
795 799 :type reviewers: Optional(list)
796 800 Accepts username strings or objects of the format:
797 801
798 802 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
799 803
800 804 :param update_commits: Trigger update of commits for this pull request
801 805 :type: update_commits: Optional(bool)
802 806
803 807 Example output:
804 808
805 809 .. code-block:: bash
806 810
807 811 id : <id_given_in_input>
808 812 result : {
809 813 "msg": "Updated pull request `63`",
810 814 "pull_request": <pull_request_object>,
811 815 "updated_reviewers": {
812 816 "added": [
813 817 "username"
814 818 ],
815 819 "removed": []
816 820 },
817 821 "updated_commits": {
818 822 "added": [
819 823 "<sha1_hash>"
820 824 ],
821 825 "common": [
822 826 "<sha1_hash>",
823 827 "<sha1_hash>",
824 828 ],
825 829 "removed": []
826 830 }
827 831 }
828 832 error : null
829 833 """
830 834
831 835 pull_request = get_pull_request_or_error(pullrequestid)
832 836 if Optional.extract(repoid):
833 837 repo = get_repo_or_error(repoid)
834 838 else:
835 839 repo = pull_request.target_repo
836 840
837 841 if not PullRequestModel().check_user_update(
838 842 pull_request, apiuser, api=True):
839 843 raise JSONRPCError(
840 844 'pull request `%s` update failed, no permission to update.' % (
841 845 pullrequestid,))
842 846 if pull_request.is_closed():
843 847 raise JSONRPCError(
844 848 'pull request `%s` update failed, pull request is closed' % (
845 849 pullrequestid,))
846 850
847 851 reviewer_objects = Optional.extract(reviewers) or []
848 852
849 853 if reviewer_objects:
850 854 schema = ReviewerListSchema()
851 855 try:
852 856 reviewer_objects = schema.deserialize(reviewer_objects)
853 857 except Invalid as err:
854 858 raise JSONRPCValidationError(colander_exc=err)
855 859
856 860 # validate users
857 861 for reviewer_object in reviewer_objects:
858 862 user = get_user_or_error(reviewer_object['username'])
859 863 reviewer_object['user_id'] = user.user_id
860 864
861 865 get_default_reviewers_data, get_validated_reviewers = \
862 866 PullRequestModel().get_reviewer_functions()
863 867
864 868 # re-use stored rules
865 869 reviewer_rules = pull_request.reviewer_data
866 870 try:
867 871 reviewers = get_validated_reviewers(
868 872 reviewer_objects, reviewer_rules)
869 873 except ValueError as e:
870 874 raise JSONRPCError('Reviewers Validation: {}'.format(e))
871 875 else:
872 876 reviewers = []
873 877
874 878 title = Optional.extract(title)
875 879 description = Optional.extract(description)
876 880 description_renderer = Optional.extract(description_renderer)
877 881
878 882 if title or description:
879 883 PullRequestModel().edit(
880 884 pull_request,
881 885 title or pull_request.title,
882 886 description or pull_request.description,
883 887 description_renderer or pull_request.description_renderer,
884 888 apiuser)
885 889 Session().commit()
886 890
887 891 commit_changes = {"added": [], "common": [], "removed": []}
888 892 if str2bool(Optional.extract(update_commits)):
889 893
890 894 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
891 895 raise JSONRPCError(
892 896 'Operation forbidden because pull request is in state {}, '
893 897 'only state {} is allowed.'.format(
894 898 pull_request.pull_request_state, PullRequest.STATE_CREATED))
895 899
896 900 with pull_request.set_state(PullRequest.STATE_UPDATING):
897 901 if PullRequestModel().has_valid_update_type(pull_request):
898 902 db_user = apiuser.get_instance()
899 903 update_response = PullRequestModel().update_commits(
900 904 pull_request, db_user)
901 905 commit_changes = update_response.changes or commit_changes
902 906 Session().commit()
903 907
904 908 reviewers_changes = {"added": [], "removed": []}
905 909 if reviewers:
906 910 old_calculated_status = pull_request.calculated_review_status()
907 911 added_reviewers, removed_reviewers = \
908 912 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
909 913
910 914 reviewers_changes['added'] = sorted(
911 915 [get_user_or_error(n).username for n in added_reviewers])
912 916 reviewers_changes['removed'] = sorted(
913 917 [get_user_or_error(n).username for n in removed_reviewers])
914 918 Session().commit()
915 919
916 920 # trigger status changed if change in reviewers changes the status
917 921 calculated_status = pull_request.calculated_review_status()
918 922 if old_calculated_status != calculated_status:
919 923 PullRequestModel().trigger_pull_request_hook(
920 924 pull_request, apiuser, 'review_status_change',
921 925 data={'status': calculated_status})
922 926
923 927 data = {
924 928 'msg': 'Updated pull request `{}`'.format(
925 929 pull_request.pull_request_id),
926 930 'pull_request': pull_request.get_api_data(),
927 931 'updated_commits': commit_changes,
928 932 'updated_reviewers': reviewers_changes
929 933 }
930 934
931 935 return data
932 936
933 937
934 938 @jsonrpc_method()
935 939 def close_pull_request(
936 940 request, apiuser, pullrequestid, repoid=Optional(None),
937 941 userid=Optional(OAttr('apiuser')), message=Optional('')):
938 942 """
939 943 Close the pull request specified by `pullrequestid`.
940 944
941 945 :param apiuser: This is filled automatically from the |authtoken|.
942 946 :type apiuser: AuthUser
943 947 :param repoid: Repository name or repository ID to which the pull
944 948 request belongs.
945 949 :type repoid: str or int
946 950 :param pullrequestid: ID of the pull request to be closed.
947 951 :type pullrequestid: int
948 952 :param userid: Close the pull request as this user.
949 953 :type userid: Optional(str or int)
950 954 :param message: Optional message to close the Pull Request with. If not
951 955 specified it will be generated automatically.
952 956 :type message: Optional(str)
953 957
954 958 Example output:
955 959
956 960 .. code-block:: bash
957 961
958 962 "id": <id_given_in_input>,
959 963 "result": {
960 964 "pull_request_id": "<int>",
961 965 "close_status": "<str:status_lbl>,
962 966 "closed": "<bool>"
963 967 },
964 968 "error": null
965 969
966 970 """
967 971 _ = request.translate
968 972
969 973 pull_request = get_pull_request_or_error(pullrequestid)
970 974 if Optional.extract(repoid):
971 975 repo = get_repo_or_error(repoid)
972 976 else:
973 977 repo = pull_request.target_repo
974 978
975 979 if not isinstance(userid, Optional):
976 980 if (has_superadmin_permission(apiuser) or
977 981 HasRepoPermissionAnyApi('repository.admin')(
978 982 user=apiuser, repo_name=repo.repo_name)):
979 983 apiuser = get_user_or_error(userid)
980 984 else:
981 985 raise JSONRPCError('userid is not the same as your user')
982 986
983 987 if pull_request.is_closed():
984 988 raise JSONRPCError(
985 989 'pull request `%s` is already closed' % (pullrequestid,))
986 990
987 991 # only owner or admin or person with write permissions
988 992 allowed_to_close = PullRequestModel().check_user_update(
989 993 pull_request, apiuser, api=True)
990 994
991 995 if not allowed_to_close:
992 996 raise JSONRPCError(
993 997 'pull request `%s` close failed, no permission to close.' % (
994 998 pullrequestid,))
995 999
996 1000 # message we're using to close the PR, else it's automatically generated
997 1001 message = Optional.extract(message)
998 1002
999 1003 # finally close the PR, with proper message comment
1000 1004 comment, status = PullRequestModel().close_pull_request_with_comment(
1001 1005 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1002 1006 status_lbl = ChangesetStatus.get_status_lbl(status)
1003 1007
1004 1008 Session().commit()
1005 1009
1006 1010 data = {
1007 1011 'pull_request_id': pull_request.pull_request_id,
1008 1012 'close_status': status_lbl,
1009 1013 'closed': True,
1010 1014 }
1011 1015 return data
@@ -1,2339 +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 userid=Optional(OAttr('apiuser'))):
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 :param send_email: Define if this comment should also send email notification
1579 :type send_email: Optional(bool)
1578 1580
1579 1581 Example error output:
1580 1582
1581 1583 .. code-block:: bash
1582 1584
1583 1585 {
1584 1586 "id" : <id_given_in_input>,
1585 1587 "result" : {
1586 1588 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1587 1589 "status_change": null or <status>,
1588 1590 "success": true
1589 1591 },
1590 1592 "error" : null
1591 1593 }
1592 1594
1593 1595 """
1594 1596 repo = get_repo_or_error(repoid)
1595 1597 if not has_superadmin_permission(apiuser):
1596 1598 _perms = ('repository.read', 'repository.write', 'repository.admin')
1597 1599 validate_repo_permissions(apiuser, repoid, repo, _perms)
1598 1600
1599 1601 try:
1600 1602 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1601 1603 except Exception as e:
1602 1604 log.exception('Failed to fetch commit')
1603 1605 raise JSONRPCError(safe_str(e))
1604 1606
1605 1607 if isinstance(userid, Optional):
1606 1608 userid = apiuser.user_id
1607 1609
1608 1610 user = get_user_or_error(userid)
1609 1611 status = Optional.extract(status)
1610 1612 comment_type = Optional.extract(comment_type)
1611 1613 resolves_comment_id = Optional.extract(resolves_comment_id)
1612 1614 extra_recipients = Optional.extract(extra_recipients)
1615 send_email = Optional.extract(send_email, binary=True)
1613 1616
1614 1617 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1615 1618 if status and status not in allowed_statuses:
1616 1619 raise JSONRPCError('Bad status, must be on '
1617 1620 'of %s got %s' % (allowed_statuses, status,))
1618 1621
1619 1622 if resolves_comment_id:
1620 1623 comment = ChangesetComment.get(resolves_comment_id)
1621 1624 if not comment:
1622 1625 raise JSONRPCError(
1623 1626 'Invalid resolves_comment_id `%s` for this commit.'
1624 1627 % resolves_comment_id)
1625 1628 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1626 1629 raise JSONRPCError(
1627 1630 'Comment `%s` is wrong type for setting status to resolved.'
1628 1631 % resolves_comment_id)
1629 1632
1630 1633 try:
1631 1634 rc_config = SettingsModel().get_all_settings()
1632 1635 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1633 1636 status_change_label = ChangesetStatus.get_status_lbl(status)
1634 1637 comment = CommentsModel().create(
1635 1638 message, repo, user, commit_id=commit_id,
1636 1639 status_change=status_change_label,
1637 1640 status_change_type=status,
1638 1641 renderer=renderer,
1639 1642 comment_type=comment_type,
1640 1643 resolves_comment_id=resolves_comment_id,
1641 1644 auth_user=apiuser,
1642 extra_recipients=extra_recipients
1645 extra_recipients=extra_recipients,
1646 send_email=send_email
1643 1647 )
1644 1648 if status:
1645 1649 # also do a status change
1646 1650 try:
1647 1651 ChangesetStatusModel().set_status(
1648 1652 repo, status, user, comment, revision=commit_id,
1649 1653 dont_allow_on_closed_pull_request=True
1650 1654 )
1651 1655 except StatusChangeOnClosedPullRequestError:
1652 1656 log.exception(
1653 1657 "Exception occurred while trying to change repo commit status")
1654 1658 msg = ('Changing status on a changeset associated with '
1655 1659 'a closed pull request is not allowed')
1656 1660 raise JSONRPCError(msg)
1657 1661
1658 1662 Session().commit()
1659 1663 return {
1660 1664 'msg': (
1661 1665 'Commented on commit `%s` for repository `%s`' % (
1662 1666 comment.revision, repo.repo_name)),
1663 1667 'status_change': status,
1664 1668 'success': True,
1665 1669 }
1666 1670 except JSONRPCError:
1667 1671 # catch any inside errors, and re-raise them to prevent from
1668 1672 # below global catch to silence them
1669 1673 raise
1670 1674 except Exception:
1671 1675 log.exception("Exception occurred while trying to comment on commit")
1672 1676 raise JSONRPCError(
1673 1677 'failed to set comment on repository `%s`' % (repo.repo_name,)
1674 1678 )
1675 1679
1676 1680
1677 1681 @jsonrpc_method()
1678 1682 def get_repo_comments(request, apiuser, repoid,
1679 1683 commit_id=Optional(None), comment_type=Optional(None),
1680 1684 userid=Optional(None)):
1681 1685 """
1682 1686 Get all comments for a repository
1683 1687
1684 1688 :param apiuser: This is filled automatically from the |authtoken|.
1685 1689 :type apiuser: AuthUser
1686 1690 :param repoid: Set the repository name or repository ID.
1687 1691 :type repoid: str or int
1688 1692 :param commit_id: Optionally filter the comments by the commit_id
1689 1693 :type commit_id: Optional(str), default: None
1690 1694 :param comment_type: Optionally filter the comments by the comment_type
1691 1695 one of: 'note', 'todo'
1692 1696 :type comment_type: Optional(str), default: None
1693 1697 :param userid: Optionally filter the comments by the author of comment
1694 1698 :type userid: Optional(str or int), Default: None
1695 1699
1696 1700 Example error output:
1697 1701
1698 1702 .. code-block:: bash
1699 1703
1700 1704 {
1701 1705 "id" : <id_given_in_input>,
1702 1706 "result" : [
1703 1707 {
1704 1708 "comment_author": <USER_DETAILS>,
1705 1709 "comment_created_on": "2017-02-01T14:38:16.309",
1706 1710 "comment_f_path": "file.txt",
1707 1711 "comment_id": 282,
1708 1712 "comment_lineno": "n1",
1709 1713 "comment_resolved_by": null,
1710 1714 "comment_status": [],
1711 1715 "comment_text": "This file needs a header",
1712 1716 "comment_type": "todo"
1713 1717 }
1714 1718 ],
1715 1719 "error" : null
1716 1720 }
1717 1721
1718 1722 """
1719 1723 repo = get_repo_or_error(repoid)
1720 1724 if not has_superadmin_permission(apiuser):
1721 1725 _perms = ('repository.read', 'repository.write', 'repository.admin')
1722 1726 validate_repo_permissions(apiuser, repoid, repo, _perms)
1723 1727
1724 1728 commit_id = Optional.extract(commit_id)
1725 1729
1726 1730 userid = Optional.extract(userid)
1727 1731 if userid:
1728 1732 user = get_user_or_error(userid)
1729 1733 else:
1730 1734 user = None
1731 1735
1732 1736 comment_type = Optional.extract(comment_type)
1733 1737 if comment_type and comment_type not in ChangesetComment.COMMENT_TYPES:
1734 1738 raise JSONRPCError(
1735 1739 'comment_type must be one of `{}` got {}'.format(
1736 1740 ChangesetComment.COMMENT_TYPES, comment_type)
1737 1741 )
1738 1742
1739 1743 comments = CommentsModel().get_repository_comments(
1740 1744 repo=repo, comment_type=comment_type, user=user, commit_id=commit_id)
1741 1745 return comments
1742 1746
1743 1747
1744 1748 @jsonrpc_method()
1745 1749 def grant_user_permission(request, apiuser, repoid, userid, perm):
1746 1750 """
1747 1751 Grant permissions for the specified user on the given repository,
1748 1752 or update existing permissions if found.
1749 1753
1750 1754 This command can only be run using an |authtoken| with admin
1751 1755 permissions on the |repo|.
1752 1756
1753 1757 :param apiuser: This is filled automatically from the |authtoken|.
1754 1758 :type apiuser: AuthUser
1755 1759 :param repoid: Set the repository name or repository ID.
1756 1760 :type repoid: str or int
1757 1761 :param userid: Set the user name.
1758 1762 :type userid: str
1759 1763 :param perm: Set the user permissions, using the following format
1760 1764 ``(repository.(none|read|write|admin))``
1761 1765 :type perm: str
1762 1766
1763 1767 Example output:
1764 1768
1765 1769 .. code-block:: bash
1766 1770
1767 1771 id : <id_given_in_input>
1768 1772 result: {
1769 1773 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1770 1774 "success": true
1771 1775 }
1772 1776 error: null
1773 1777 """
1774 1778
1775 1779 repo = get_repo_or_error(repoid)
1776 1780 user = get_user_or_error(userid)
1777 1781 perm = get_perm_or_error(perm)
1778 1782 if not has_superadmin_permission(apiuser):
1779 1783 _perms = ('repository.admin',)
1780 1784 validate_repo_permissions(apiuser, repoid, repo, _perms)
1781 1785
1782 1786 perm_additions = [[user.user_id, perm.permission_name, "user"]]
1783 1787 try:
1784 1788 changes = RepoModel().update_permissions(
1785 1789 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1786 1790
1787 1791 action_data = {
1788 1792 'added': changes['added'],
1789 1793 'updated': changes['updated'],
1790 1794 'deleted': changes['deleted'],
1791 1795 }
1792 1796 audit_logger.store_api(
1793 1797 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1794 1798 Session().commit()
1795 1799 PermissionModel().flush_user_permission_caches(changes)
1796 1800
1797 1801 return {
1798 1802 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1799 1803 perm.permission_name, user.username, repo.repo_name
1800 1804 ),
1801 1805 'success': True
1802 1806 }
1803 1807 except Exception:
1804 1808 log.exception("Exception occurred while trying edit permissions for repo")
1805 1809 raise JSONRPCError(
1806 1810 'failed to edit permission for user: `%s` in repo: `%s`' % (
1807 1811 userid, repoid
1808 1812 )
1809 1813 )
1810 1814
1811 1815
1812 1816 @jsonrpc_method()
1813 1817 def revoke_user_permission(request, apiuser, repoid, userid):
1814 1818 """
1815 1819 Revoke permission for a user on the specified repository.
1816 1820
1817 1821 This command can only be run using an |authtoken| with admin
1818 1822 permissions on the |repo|.
1819 1823
1820 1824 :param apiuser: This is filled automatically from the |authtoken|.
1821 1825 :type apiuser: AuthUser
1822 1826 :param repoid: Set the repository name or repository ID.
1823 1827 :type repoid: str or int
1824 1828 :param userid: Set the user name of revoked user.
1825 1829 :type userid: str or int
1826 1830
1827 1831 Example error output:
1828 1832
1829 1833 .. code-block:: bash
1830 1834
1831 1835 id : <id_given_in_input>
1832 1836 result: {
1833 1837 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1834 1838 "success": true
1835 1839 }
1836 1840 error: null
1837 1841 """
1838 1842
1839 1843 repo = get_repo_or_error(repoid)
1840 1844 user = get_user_or_error(userid)
1841 1845 if not has_superadmin_permission(apiuser):
1842 1846 _perms = ('repository.admin',)
1843 1847 validate_repo_permissions(apiuser, repoid, repo, _perms)
1844 1848
1845 1849 perm_deletions = [[user.user_id, None, "user"]]
1846 1850 try:
1847 1851 changes = RepoModel().update_permissions(
1848 1852 repo=repo, perm_deletions=perm_deletions, cur_user=user)
1849 1853
1850 1854 action_data = {
1851 1855 'added': changes['added'],
1852 1856 'updated': changes['updated'],
1853 1857 'deleted': changes['deleted'],
1854 1858 }
1855 1859 audit_logger.store_api(
1856 1860 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1857 1861 Session().commit()
1858 1862 PermissionModel().flush_user_permission_caches(changes)
1859 1863
1860 1864 return {
1861 1865 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1862 1866 user.username, repo.repo_name
1863 1867 ),
1864 1868 'success': True
1865 1869 }
1866 1870 except Exception:
1867 1871 log.exception("Exception occurred while trying revoke permissions to repo")
1868 1872 raise JSONRPCError(
1869 1873 'failed to edit permission for user: `%s` in repo: `%s`' % (
1870 1874 userid, repoid
1871 1875 )
1872 1876 )
1873 1877
1874 1878
1875 1879 @jsonrpc_method()
1876 1880 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1877 1881 """
1878 1882 Grant permission for a user group on the specified repository,
1879 1883 or update existing permissions.
1880 1884
1881 1885 This command can only be run using an |authtoken| with admin
1882 1886 permissions on the |repo|.
1883 1887
1884 1888 :param apiuser: This is filled automatically from the |authtoken|.
1885 1889 :type apiuser: AuthUser
1886 1890 :param repoid: Set the repository name or repository ID.
1887 1891 :type repoid: str or int
1888 1892 :param usergroupid: Specify the ID of the user group.
1889 1893 :type usergroupid: str or int
1890 1894 :param perm: Set the user group permissions using the following
1891 1895 format: (repository.(none|read|write|admin))
1892 1896 :type perm: str
1893 1897
1894 1898 Example output:
1895 1899
1896 1900 .. code-block:: bash
1897 1901
1898 1902 id : <id_given_in_input>
1899 1903 result : {
1900 1904 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1901 1905 "success": true
1902 1906
1903 1907 }
1904 1908 error : null
1905 1909
1906 1910 Example error output:
1907 1911
1908 1912 .. code-block:: bash
1909 1913
1910 1914 id : <id_given_in_input>
1911 1915 result : null
1912 1916 error : {
1913 1917 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1914 1918 }
1915 1919
1916 1920 """
1917 1921
1918 1922 repo = get_repo_or_error(repoid)
1919 1923 perm = get_perm_or_error(perm)
1920 1924 if not has_superadmin_permission(apiuser):
1921 1925 _perms = ('repository.admin',)
1922 1926 validate_repo_permissions(apiuser, repoid, repo, _perms)
1923 1927
1924 1928 user_group = get_user_group_or_error(usergroupid)
1925 1929 if not has_superadmin_permission(apiuser):
1926 1930 # check if we have at least read permission for this user group !
1927 1931 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1928 1932 if not HasUserGroupPermissionAnyApi(*_perms)(
1929 1933 user=apiuser, user_group_name=user_group.users_group_name):
1930 1934 raise JSONRPCError(
1931 1935 'user group `%s` does not exist' % (usergroupid,))
1932 1936
1933 1937 perm_additions = [[user_group.users_group_id, perm.permission_name, "user_group"]]
1934 1938 try:
1935 1939 changes = RepoModel().update_permissions(
1936 1940 repo=repo, perm_additions=perm_additions, cur_user=apiuser)
1937 1941 action_data = {
1938 1942 'added': changes['added'],
1939 1943 'updated': changes['updated'],
1940 1944 'deleted': changes['deleted'],
1941 1945 }
1942 1946 audit_logger.store_api(
1943 1947 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
1944 1948 Session().commit()
1945 1949 PermissionModel().flush_user_permission_caches(changes)
1946 1950
1947 1951 return {
1948 1952 'msg': 'Granted perm: `%s` for user group: `%s` in '
1949 1953 'repo: `%s`' % (
1950 1954 perm.permission_name, user_group.users_group_name,
1951 1955 repo.repo_name
1952 1956 ),
1953 1957 'success': True
1954 1958 }
1955 1959 except Exception:
1956 1960 log.exception(
1957 1961 "Exception occurred while trying change permission on repo")
1958 1962 raise JSONRPCError(
1959 1963 'failed to edit permission for user group: `%s` in '
1960 1964 'repo: `%s`' % (
1961 1965 usergroupid, repo.repo_name
1962 1966 )
1963 1967 )
1964 1968
1965 1969
1966 1970 @jsonrpc_method()
1967 1971 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1968 1972 """
1969 1973 Revoke the permissions of a user group on a given repository.
1970 1974
1971 1975 This command can only be run using an |authtoken| with admin
1972 1976 permissions on the |repo|.
1973 1977
1974 1978 :param apiuser: This is filled automatically from the |authtoken|.
1975 1979 :type apiuser: AuthUser
1976 1980 :param repoid: Set the repository name or repository ID.
1977 1981 :type repoid: str or int
1978 1982 :param usergroupid: Specify the user group ID.
1979 1983 :type usergroupid: str or int
1980 1984
1981 1985 Example output:
1982 1986
1983 1987 .. code-block:: bash
1984 1988
1985 1989 id : <id_given_in_input>
1986 1990 result: {
1987 1991 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1988 1992 "success": true
1989 1993 }
1990 1994 error: null
1991 1995 """
1992 1996
1993 1997 repo = get_repo_or_error(repoid)
1994 1998 if not has_superadmin_permission(apiuser):
1995 1999 _perms = ('repository.admin',)
1996 2000 validate_repo_permissions(apiuser, repoid, repo, _perms)
1997 2001
1998 2002 user_group = get_user_group_or_error(usergroupid)
1999 2003 if not has_superadmin_permission(apiuser):
2000 2004 # check if we have at least read permission for this user group !
2001 2005 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2002 2006 if not HasUserGroupPermissionAnyApi(*_perms)(
2003 2007 user=apiuser, user_group_name=user_group.users_group_name):
2004 2008 raise JSONRPCError(
2005 2009 'user group `%s` does not exist' % (usergroupid,))
2006 2010
2007 2011 perm_deletions = [[user_group.users_group_id, None, "user_group"]]
2008 2012 try:
2009 2013 changes = RepoModel().update_permissions(
2010 2014 repo=repo, perm_deletions=perm_deletions, cur_user=apiuser)
2011 2015 action_data = {
2012 2016 'added': changes['added'],
2013 2017 'updated': changes['updated'],
2014 2018 'deleted': changes['deleted'],
2015 2019 }
2016 2020 audit_logger.store_api(
2017 2021 'repo.edit.permissions', action_data=action_data, user=apiuser, repo=repo)
2018 2022 Session().commit()
2019 2023 PermissionModel().flush_user_permission_caches(changes)
2020 2024
2021 2025 return {
2022 2026 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
2023 2027 user_group.users_group_name, repo.repo_name
2024 2028 ),
2025 2029 'success': True
2026 2030 }
2027 2031 except Exception:
2028 2032 log.exception("Exception occurred while trying revoke "
2029 2033 "user group permission on repo")
2030 2034 raise JSONRPCError(
2031 2035 'failed to edit permission for user group: `%s` in '
2032 2036 'repo: `%s`' % (
2033 2037 user_group.users_group_name, repo.repo_name
2034 2038 )
2035 2039 )
2036 2040
2037 2041
2038 2042 @jsonrpc_method()
2039 2043 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
2040 2044 """
2041 2045 Triggers a pull on the given repository from a remote location. You
2042 2046 can use this to keep remote repositories up-to-date.
2043 2047
2044 2048 This command can only be run using an |authtoken| with admin
2045 2049 rights to the specified repository. For more information,
2046 2050 see :ref:`config-token-ref`.
2047 2051
2048 2052 This command takes the following options:
2049 2053
2050 2054 :param apiuser: This is filled automatically from the |authtoken|.
2051 2055 :type apiuser: AuthUser
2052 2056 :param repoid: The repository name or repository ID.
2053 2057 :type repoid: str or int
2054 2058 :param remote_uri: Optional remote URI to pass in for pull
2055 2059 :type remote_uri: str
2056 2060
2057 2061 Example output:
2058 2062
2059 2063 .. code-block:: bash
2060 2064
2061 2065 id : <id_given_in_input>
2062 2066 result : {
2063 2067 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
2064 2068 "repository": "<repository name>"
2065 2069 }
2066 2070 error : null
2067 2071
2068 2072 Example error output:
2069 2073
2070 2074 .. code-block:: bash
2071 2075
2072 2076 id : <id_given_in_input>
2073 2077 result : null
2074 2078 error : {
2075 2079 "Unable to push changes from `<remote_url>`"
2076 2080 }
2077 2081
2078 2082 """
2079 2083
2080 2084 repo = get_repo_or_error(repoid)
2081 2085 remote_uri = Optional.extract(remote_uri)
2082 2086 remote_uri_display = remote_uri or repo.clone_uri_hidden
2083 2087 if not has_superadmin_permission(apiuser):
2084 2088 _perms = ('repository.admin',)
2085 2089 validate_repo_permissions(apiuser, repoid, repo, _perms)
2086 2090
2087 2091 try:
2088 2092 ScmModel().pull_changes(
2089 2093 repo.repo_name, apiuser.username, remote_uri=remote_uri)
2090 2094 return {
2091 2095 'msg': 'Pulled from url `%s` on repo `%s`' % (
2092 2096 remote_uri_display, repo.repo_name),
2093 2097 'repository': repo.repo_name
2094 2098 }
2095 2099 except Exception:
2096 2100 log.exception("Exception occurred while trying to "
2097 2101 "pull changes from remote location")
2098 2102 raise JSONRPCError(
2099 2103 'Unable to pull changes from `%s`' % remote_uri_display
2100 2104 )
2101 2105
2102 2106
2103 2107 @jsonrpc_method()
2104 2108 def strip(request, apiuser, repoid, revision, branch):
2105 2109 """
2106 2110 Strips the given revision from the specified repository.
2107 2111
2108 2112 * This will remove the revision and all of its decendants.
2109 2113
2110 2114 This command can only be run using an |authtoken| with admin rights to
2111 2115 the specified repository.
2112 2116
2113 2117 This command takes the following options:
2114 2118
2115 2119 :param apiuser: This is filled automatically from the |authtoken|.
2116 2120 :type apiuser: AuthUser
2117 2121 :param repoid: The repository name or repository ID.
2118 2122 :type repoid: str or int
2119 2123 :param revision: The revision you wish to strip.
2120 2124 :type revision: str
2121 2125 :param branch: The branch from which to strip the revision.
2122 2126 :type branch: str
2123 2127
2124 2128 Example output:
2125 2129
2126 2130 .. code-block:: bash
2127 2131
2128 2132 id : <id_given_in_input>
2129 2133 result : {
2130 2134 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
2131 2135 "repository": "<repository name>"
2132 2136 }
2133 2137 error : null
2134 2138
2135 2139 Example error output:
2136 2140
2137 2141 .. code-block:: bash
2138 2142
2139 2143 id : <id_given_in_input>
2140 2144 result : null
2141 2145 error : {
2142 2146 "Unable to strip commit <commit_hash> from repo `<repository name>`"
2143 2147 }
2144 2148
2145 2149 """
2146 2150
2147 2151 repo = get_repo_or_error(repoid)
2148 2152 if not has_superadmin_permission(apiuser):
2149 2153 _perms = ('repository.admin',)
2150 2154 validate_repo_permissions(apiuser, repoid, repo, _perms)
2151 2155
2152 2156 try:
2153 2157 ScmModel().strip(repo, revision, branch)
2154 2158 audit_logger.store_api(
2155 2159 'repo.commit.strip', action_data={'commit_id': revision},
2156 2160 repo=repo,
2157 2161 user=apiuser, commit=True)
2158 2162
2159 2163 return {
2160 2164 'msg': 'Stripped commit %s from repo `%s`' % (
2161 2165 revision, repo.repo_name),
2162 2166 'repository': repo.repo_name
2163 2167 }
2164 2168 except Exception:
2165 2169 log.exception("Exception while trying to strip")
2166 2170 raise JSONRPCError(
2167 2171 'Unable to strip commit %s from repo `%s`' % (
2168 2172 revision, repo.repo_name)
2169 2173 )
2170 2174
2171 2175
2172 2176 @jsonrpc_method()
2173 2177 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
2174 2178 """
2175 2179 Returns all settings for a repository. If key is given it only returns the
2176 2180 setting identified by the key or null.
2177 2181
2178 2182 :param apiuser: This is filled automatically from the |authtoken|.
2179 2183 :type apiuser: AuthUser
2180 2184 :param repoid: The repository name or repository id.
2181 2185 :type repoid: str or int
2182 2186 :param key: Key of the setting to return.
2183 2187 :type: key: Optional(str)
2184 2188
2185 2189 Example output:
2186 2190
2187 2191 .. code-block:: bash
2188 2192
2189 2193 {
2190 2194 "error": null,
2191 2195 "id": 237,
2192 2196 "result": {
2193 2197 "extensions_largefiles": true,
2194 2198 "extensions_evolve": true,
2195 2199 "hooks_changegroup_push_logger": true,
2196 2200 "hooks_changegroup_repo_size": false,
2197 2201 "hooks_outgoing_pull_logger": true,
2198 2202 "phases_publish": "True",
2199 2203 "rhodecode_hg_use_rebase_for_merging": true,
2200 2204 "rhodecode_pr_merge_enabled": true,
2201 2205 "rhodecode_use_outdated_comments": true
2202 2206 }
2203 2207 }
2204 2208 """
2205 2209
2206 2210 # Restrict access to this api method to admins only.
2207 2211 if not has_superadmin_permission(apiuser):
2208 2212 raise JSONRPCForbidden()
2209 2213
2210 2214 try:
2211 2215 repo = get_repo_or_error(repoid)
2212 2216 settings_model = VcsSettingsModel(repo=repo)
2213 2217 settings = settings_model.get_global_settings()
2214 2218 settings.update(settings_model.get_repo_settings())
2215 2219
2216 2220 # If only a single setting is requested fetch it from all settings.
2217 2221 key = Optional.extract(key)
2218 2222 if key is not None:
2219 2223 settings = settings.get(key, None)
2220 2224 except Exception:
2221 2225 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
2222 2226 log.exception(msg)
2223 2227 raise JSONRPCError(msg)
2224 2228
2225 2229 return settings
2226 2230
2227 2231
2228 2232 @jsonrpc_method()
2229 2233 def set_repo_settings(request, apiuser, repoid, settings):
2230 2234 """
2231 2235 Update repository settings. Returns true on success.
2232 2236
2233 2237 :param apiuser: This is filled automatically from the |authtoken|.
2234 2238 :type apiuser: AuthUser
2235 2239 :param repoid: The repository name or repository id.
2236 2240 :type repoid: str or int
2237 2241 :param settings: The new settings for the repository.
2238 2242 :type: settings: dict
2239 2243
2240 2244 Example output:
2241 2245
2242 2246 .. code-block:: bash
2243 2247
2244 2248 {
2245 2249 "error": null,
2246 2250 "id": 237,
2247 2251 "result": true
2248 2252 }
2249 2253 """
2250 2254 # Restrict access to this api method to admins only.
2251 2255 if not has_superadmin_permission(apiuser):
2252 2256 raise JSONRPCForbidden()
2253 2257
2254 2258 if type(settings) is not dict:
2255 2259 raise JSONRPCError('Settings have to be a JSON Object.')
2256 2260
2257 2261 try:
2258 2262 settings_model = VcsSettingsModel(repo=repoid)
2259 2263
2260 2264 # Merge global, repo and incoming settings.
2261 2265 new_settings = settings_model.get_global_settings()
2262 2266 new_settings.update(settings_model.get_repo_settings())
2263 2267 new_settings.update(settings)
2264 2268
2265 2269 # Update the settings.
2266 2270 inherit_global_settings = new_settings.get(
2267 2271 'inherit_global_settings', False)
2268 2272 settings_model.create_or_update_repo_settings(
2269 2273 new_settings, inherit_global_settings=inherit_global_settings)
2270 2274 Session().commit()
2271 2275 except Exception:
2272 2276 msg = 'Failed to update settings for repository `{}`'.format(repoid)
2273 2277 log.exception(msg)
2274 2278 raise JSONRPCError(msg)
2275 2279
2276 2280 # Indicate success.
2277 2281 return True
2278 2282
2279 2283
2280 2284 @jsonrpc_method()
2281 2285 def maintenance(request, apiuser, repoid):
2282 2286 """
2283 2287 Triggers a maintenance on the given repository.
2284 2288
2285 2289 This command can only be run using an |authtoken| with admin
2286 2290 rights to the specified repository. For more information,
2287 2291 see :ref:`config-token-ref`.
2288 2292
2289 2293 This command takes the following options:
2290 2294
2291 2295 :param apiuser: This is filled automatically from the |authtoken|.
2292 2296 :type apiuser: AuthUser
2293 2297 :param repoid: The repository name or repository ID.
2294 2298 :type repoid: str or int
2295 2299
2296 2300 Example output:
2297 2301
2298 2302 .. code-block:: bash
2299 2303
2300 2304 id : <id_given_in_input>
2301 2305 result : {
2302 2306 "msg": "executed maintenance command",
2303 2307 "executed_actions": [
2304 2308 <action_message>, <action_message2>...
2305 2309 ],
2306 2310 "repository": "<repository name>"
2307 2311 }
2308 2312 error : null
2309 2313
2310 2314 Example error output:
2311 2315
2312 2316 .. code-block:: bash
2313 2317
2314 2318 id : <id_given_in_input>
2315 2319 result : null
2316 2320 error : {
2317 2321 "Unable to execute maintenance on `<reponame>`"
2318 2322 }
2319 2323
2320 2324 """
2321 2325
2322 2326 repo = get_repo_or_error(repoid)
2323 2327 if not has_superadmin_permission(apiuser):
2324 2328 _perms = ('repository.admin',)
2325 2329 validate_repo_permissions(apiuser, repoid, repo, _perms)
2326 2330
2327 2331 try:
2328 2332 maintenance = repo_maintenance.RepoMaintenance()
2329 2333 executed_actions = maintenance.execute(repo)
2330 2334
2331 2335 return {
2332 2336 'msg': 'executed maintenance command',
2333 2337 'executed_actions': executed_actions,
2334 2338 'repository': repo.repo_name
2335 2339 }
2336 2340 except Exception:
2337 2341 log.exception("Exception occurred while trying to run maintenance")
2338 2342 raise JSONRPCError(
2339 2343 'Unable to execute maintenance on `%s`' % repo.repo_name)
General Comments 0
You need to be logged in to leave comments. Login now