##// END OF EJS Templates
audit-logs: store properly IP and user for certain comments types....
marcink -
r2728:9f3cefa1 default
parent child Browse files
Show More
@@ -1,903 +1,904 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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)
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
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 """
48 48 Get a pull request based on the given ID.
49 49
50 50 :param apiuser: This is filled automatically from the |authtoken|.
51 51 :type apiuser: AuthUser
52 52 :param repoid: Optional, repository name or repository ID from where
53 53 the pull request was opened.
54 54 :type repoid: str or int
55 55 :param pullrequestid: ID of the requested pull request.
56 56 :type pullrequestid: int
57 57
58 58 Example output:
59 59
60 60 .. code-block:: bash
61 61
62 62 "id": <id_given_in_input>,
63 63 "result":
64 64 {
65 65 "pull_request_id": "<pull_request_id>",
66 66 "url": "<url>",
67 67 "title": "<title>",
68 68 "description": "<description>",
69 69 "status" : "<status>",
70 70 "created_on": "<date_time_created>",
71 71 "updated_on": "<date_time_updated>",
72 72 "commit_ids": [
73 73 ...
74 74 "<commit_id>",
75 75 "<commit_id>",
76 76 ...
77 77 ],
78 78 "review_status": "<review_status>",
79 79 "mergeable": {
80 80 "status": "<bool>",
81 81 "message": "<message>",
82 82 },
83 83 "source": {
84 84 "clone_url": "<clone_url>",
85 85 "repository": "<repository_name>",
86 86 "reference":
87 87 {
88 88 "name": "<name>",
89 89 "type": "<type>",
90 90 "commit_id": "<commit_id>",
91 91 }
92 92 },
93 93 "target": {
94 94 "clone_url": "<clone_url>",
95 95 "repository": "<repository_name>",
96 96 "reference":
97 97 {
98 98 "name": "<name>",
99 99 "type": "<type>",
100 100 "commit_id": "<commit_id>",
101 101 }
102 102 },
103 103 "merge": {
104 104 "clone_url": "<clone_url>",
105 105 "reference":
106 106 {
107 107 "name": "<name>",
108 108 "type": "<type>",
109 109 "commit_id": "<commit_id>",
110 110 }
111 111 },
112 112 "author": <user_obj>,
113 113 "reviewers": [
114 114 ...
115 115 {
116 116 "user": "<user_obj>",
117 117 "review_status": "<review_status>",
118 118 }
119 119 ...
120 120 ]
121 121 },
122 122 "error": null
123 123 """
124 124
125 125 pull_request = get_pull_request_or_error(pullrequestid)
126 126 if Optional.extract(repoid):
127 127 repo = get_repo_or_error(repoid)
128 128 else:
129 129 repo = pull_request.target_repo
130 130
131 131 if not PullRequestModel().check_user_read(
132 132 pull_request, apiuser, api=True):
133 133 raise JSONRPCError('repository `%s` or pull request `%s` '
134 134 'does not exist' % (repoid, pullrequestid))
135 135 data = pull_request.get_api_data()
136 136 return data
137 137
138 138
139 139 @jsonrpc_method()
140 140 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
141 141 """
142 142 Get all pull requests from the repository specified in `repoid`.
143 143
144 144 :param apiuser: This is filled automatically from the |authtoken|.
145 145 :type apiuser: AuthUser
146 146 :param repoid: Optional repository name or repository ID.
147 147 :type repoid: str or int
148 148 :param status: Only return pull requests with the specified status.
149 149 Valid options are.
150 150 * ``new`` (default)
151 151 * ``open``
152 152 * ``closed``
153 153 :type status: str
154 154
155 155 Example output:
156 156
157 157 .. code-block:: bash
158 158
159 159 "id": <id_given_in_input>,
160 160 "result":
161 161 [
162 162 ...
163 163 {
164 164 "pull_request_id": "<pull_request_id>",
165 165 "url": "<url>",
166 166 "title" : "<title>",
167 167 "description": "<description>",
168 168 "status": "<status>",
169 169 "created_on": "<date_time_created>",
170 170 "updated_on": "<date_time_updated>",
171 171 "commit_ids": [
172 172 ...
173 173 "<commit_id>",
174 174 "<commit_id>",
175 175 ...
176 176 ],
177 177 "review_status": "<review_status>",
178 178 "mergeable": {
179 179 "status": "<bool>",
180 180 "message: "<message>",
181 181 },
182 182 "source": {
183 183 "clone_url": "<clone_url>",
184 184 "reference":
185 185 {
186 186 "name": "<name>",
187 187 "type": "<type>",
188 188 "commit_id": "<commit_id>",
189 189 }
190 190 },
191 191 "target": {
192 192 "clone_url": "<clone_url>",
193 193 "reference":
194 194 {
195 195 "name": "<name>",
196 196 "type": "<type>",
197 197 "commit_id": "<commit_id>",
198 198 }
199 199 },
200 200 "merge": {
201 201 "clone_url": "<clone_url>",
202 202 "reference":
203 203 {
204 204 "name": "<name>",
205 205 "type": "<type>",
206 206 "commit_id": "<commit_id>",
207 207 }
208 208 },
209 209 "author": <user_obj>,
210 210 "reviewers": [
211 211 ...
212 212 {
213 213 "user": "<user_obj>",
214 214 "review_status": "<review_status>",
215 215 }
216 216 ...
217 217 ]
218 218 }
219 219 ...
220 220 ],
221 221 "error": null
222 222
223 223 """
224 224 repo = get_repo_or_error(repoid)
225 225 if not has_superadmin_permission(apiuser):
226 226 _perms = (
227 227 'repository.admin', 'repository.write', 'repository.read',)
228 228 validate_repo_permissions(apiuser, repoid, repo, _perms)
229 229
230 230 status = Optional.extract(status)
231 231 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
232 232 data = [pr.get_api_data() for pr in pull_requests]
233 233 return data
234 234
235 235
236 236 @jsonrpc_method()
237 237 def merge_pull_request(
238 238 request, apiuser, pullrequestid, repoid=Optional(None),
239 239 userid=Optional(OAttr('apiuser'))):
240 240 """
241 241 Merge the pull request specified by `pullrequestid` into its target
242 242 repository.
243 243
244 244 :param apiuser: This is filled automatically from the |authtoken|.
245 245 :type apiuser: AuthUser
246 246 :param repoid: Optional, repository name or repository ID of the
247 247 target repository to which the |pr| is to be merged.
248 248 :type repoid: str or int
249 249 :param pullrequestid: ID of the pull request which shall be merged.
250 250 :type pullrequestid: int
251 251 :param userid: Merge the pull request as this user.
252 252 :type userid: Optional(str or int)
253 253
254 254 Example output:
255 255
256 256 .. code-block:: bash
257 257
258 258 "id": <id_given_in_input>,
259 259 "result": {
260 260 "executed": "<bool>",
261 261 "failure_reason": "<int>",
262 262 "merge_commit_id": "<merge_commit_id>",
263 263 "possible": "<bool>",
264 264 "merge_ref": {
265 265 "commit_id": "<commit_id>",
266 266 "type": "<type>",
267 267 "name": "<name>"
268 268 }
269 269 },
270 270 "error": null
271 271 """
272 272 pull_request = get_pull_request_or_error(pullrequestid)
273 273 if Optional.extract(repoid):
274 274 repo = get_repo_or_error(repoid)
275 275 else:
276 276 repo = pull_request.target_repo
277 277
278 278 if not isinstance(userid, Optional):
279 279 if (has_superadmin_permission(apiuser) or
280 280 HasRepoPermissionAnyApi('repository.admin')(
281 281 user=apiuser, repo_name=repo.repo_name)):
282 282 apiuser = get_user_or_error(userid)
283 283 else:
284 284 raise JSONRPCError('userid is not the same as your user')
285 285
286 286 check = MergeCheck.validate(
287 287 pull_request, user=apiuser, translator=request.translate)
288 288 merge_possible = not check.failed
289 289
290 290 if not merge_possible:
291 291 error_messages = []
292 292 for err_type, error_msg in check.errors:
293 293 error_msg = request.translate(error_msg)
294 294 error_messages.append(error_msg)
295 295
296 296 reasons = ','.join(error_messages)
297 297 raise JSONRPCError(
298 298 'merge not possible for following reasons: {}'.format(reasons))
299 299
300 300 target_repo = pull_request.target_repo
301 301 extras = vcs_operation_context(
302 302 request.environ, repo_name=target_repo.repo_name,
303 303 username=apiuser.username, action='push',
304 304 scm=target_repo.repo_type)
305 305 merge_response = PullRequestModel().merge(
306 306 pull_request, apiuser, extras=extras)
307 307 if merge_response.executed:
308 308 PullRequestModel().close_pull_request(
309 309 pull_request.pull_request_id, apiuser)
310 310
311 311 Session().commit()
312 312
313 313 # In previous versions the merge response directly contained the merge
314 314 # commit id. It is now contained in the merge reference object. To be
315 315 # backwards compatible we have to extract it again.
316 316 merge_response = merge_response._asdict()
317 317 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
318 318
319 319 return merge_response
320 320
321 321
322 322 @jsonrpc_method()
323 323 def get_pull_request_comments(
324 324 request, apiuser, pullrequestid, repoid=Optional(None)):
325 325 """
326 326 Get all comments of pull request specified with the `pullrequestid`
327 327
328 328 :param apiuser: This is filled automatically from the |authtoken|.
329 329 :type apiuser: AuthUser
330 330 :param repoid: Optional repository name or repository ID.
331 331 :type repoid: str or int
332 332 :param pullrequestid: The pull request ID.
333 333 :type pullrequestid: int
334 334
335 335 Example output:
336 336
337 337 .. code-block:: bash
338 338
339 339 id : <id_given_in_input>
340 340 result : [
341 341 {
342 342 "comment_author": {
343 343 "active": true,
344 344 "full_name_or_username": "Tom Gore",
345 345 "username": "admin"
346 346 },
347 347 "comment_created_on": "2017-01-02T18:43:45.533",
348 348 "comment_f_path": null,
349 349 "comment_id": 25,
350 350 "comment_lineno": null,
351 351 "comment_status": {
352 352 "status": "under_review",
353 353 "status_lbl": "Under Review"
354 354 },
355 355 "comment_text": "Example text",
356 356 "comment_type": null,
357 357 "pull_request_version": null
358 358 }
359 359 ],
360 360 error : null
361 361 """
362 362
363 363 pull_request = get_pull_request_or_error(pullrequestid)
364 364 if Optional.extract(repoid):
365 365 repo = get_repo_or_error(repoid)
366 366 else:
367 367 repo = pull_request.target_repo
368 368
369 369 if not PullRequestModel().check_user_read(
370 370 pull_request, apiuser, api=True):
371 371 raise JSONRPCError('repository `%s` or pull request `%s` '
372 372 'does not exist' % (repoid, pullrequestid))
373 373
374 374 (pull_request_latest,
375 375 pull_request_at_ver,
376 376 pull_request_display_obj,
377 377 at_version) = PullRequestModel().get_pr_version(
378 378 pull_request.pull_request_id, version=None)
379 379
380 380 versions = pull_request_display_obj.versions()
381 381 ver_map = {
382 382 ver.pull_request_version_id: cnt
383 383 for cnt, ver in enumerate(versions, 1)
384 384 }
385 385
386 386 # GENERAL COMMENTS with versions #
387 387 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
388 388 q = q.order_by(ChangesetComment.comment_id.asc())
389 389 general_comments = q.all()
390 390
391 391 # INLINE COMMENTS with versions #
392 392 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
393 393 q = q.order_by(ChangesetComment.comment_id.asc())
394 394 inline_comments = q.all()
395 395
396 396 data = []
397 397 for comment in inline_comments + general_comments:
398 398 full_data = comment.get_api_data()
399 399 pr_version_id = None
400 400 if comment.pull_request_version_id:
401 401 pr_version_id = 'v{}'.format(
402 402 ver_map[comment.pull_request_version_id])
403 403
404 404 # sanitize some entries
405 405
406 406 full_data['pull_request_version'] = pr_version_id
407 407 full_data['comment_author'] = {
408 408 'username': full_data['comment_author'].username,
409 409 'full_name_or_username': full_data['comment_author'].full_name_or_username,
410 410 'active': full_data['comment_author'].active,
411 411 }
412 412
413 413 if full_data['comment_status']:
414 414 full_data['comment_status'] = {
415 415 'status': full_data['comment_status'][0].status,
416 416 'status_lbl': full_data['comment_status'][0].status_lbl,
417 417 }
418 418 else:
419 419 full_data['comment_status'] = {}
420 420
421 421 data.append(full_data)
422 422 return data
423 423
424 424
425 425 @jsonrpc_method()
426 426 def comment_pull_request(
427 427 request, apiuser, pullrequestid, repoid=Optional(None),
428 428 message=Optional(None), commit_id=Optional(None), status=Optional(None),
429 429 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
430 430 resolves_comment_id=Optional(None),
431 431 userid=Optional(OAttr('apiuser'))):
432 432 """
433 433 Comment on the pull request specified with the `pullrequestid`,
434 434 in the |repo| specified by the `repoid`, and optionally change the
435 435 review status.
436 436
437 437 :param apiuser: This is filled automatically from the |authtoken|.
438 438 :type apiuser: AuthUser
439 439 :param repoid: Optional repository name or repository ID.
440 440 :type repoid: str or int
441 441 :param pullrequestid: The pull request ID.
442 442 :type pullrequestid: int
443 443 :param commit_id: Specify the commit_id for which to set a comment. If
444 444 given commit_id is different than latest in the PR status
445 445 change won't be performed.
446 446 :type commit_id: str
447 447 :param message: The text content of the comment.
448 448 :type message: str
449 449 :param status: (**Optional**) Set the approval status of the pull
450 450 request. One of: 'not_reviewed', 'approved', 'rejected',
451 451 'under_review'
452 452 :type status: str
453 453 :param comment_type: Comment type, one of: 'note', 'todo'
454 454 :type comment_type: Optional(str), default: 'note'
455 455 :param userid: Comment on the pull request as this user
456 456 :type userid: Optional(str or int)
457 457
458 458 Example output:
459 459
460 460 .. code-block:: bash
461 461
462 462 id : <id_given_in_input>
463 463 result : {
464 464 "pull_request_id": "<Integer>",
465 465 "comment_id": "<Integer>",
466 466 "status": {"given": <given_status>,
467 467 "was_changed": <bool status_was_actually_changed> },
468 468 },
469 469 error : null
470 470 """
471 471 pull_request = get_pull_request_or_error(pullrequestid)
472 472 if Optional.extract(repoid):
473 473 repo = get_repo_or_error(repoid)
474 474 else:
475 475 repo = pull_request.target_repo
476 476
477 477 if not isinstance(userid, Optional):
478 478 if (has_superadmin_permission(apiuser) or
479 479 HasRepoPermissionAnyApi('repository.admin')(
480 480 user=apiuser, repo_name=repo.repo_name)):
481 481 apiuser = get_user_or_error(userid)
482 482 else:
483 483 raise JSONRPCError('userid is not the same as your user')
484 484
485 485 if not PullRequestModel().check_user_read(
486 486 pull_request, apiuser, api=True):
487 487 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
488 488 message = Optional.extract(message)
489 489 status = Optional.extract(status)
490 490 commit_id = Optional.extract(commit_id)
491 491 comment_type = Optional.extract(comment_type)
492 492 resolves_comment_id = Optional.extract(resolves_comment_id)
493 493
494 494 if not message and not status:
495 495 raise JSONRPCError(
496 496 'Both message and status parameters are missing. '
497 497 'At least one is required.')
498 498
499 499 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
500 500 status is not None):
501 501 raise JSONRPCError('Unknown comment status: `%s`' % status)
502 502
503 503 if commit_id and commit_id not in pull_request.revisions:
504 504 raise JSONRPCError(
505 505 'Invalid commit_id `%s` for this pull request.' % commit_id)
506 506
507 507 allowed_to_change_status = PullRequestModel().check_user_change_status(
508 508 pull_request, apiuser)
509 509
510 510 # if commit_id is passed re-validated if user is allowed to change status
511 511 # based on latest commit_id from the PR
512 512 if commit_id:
513 513 commit_idx = pull_request.revisions.index(commit_id)
514 514 if commit_idx != 0:
515 515 allowed_to_change_status = False
516 516
517 517 if resolves_comment_id:
518 518 comment = ChangesetComment.get(resolves_comment_id)
519 519 if not comment:
520 520 raise JSONRPCError(
521 521 'Invalid resolves_comment_id `%s` for this pull request.'
522 522 % resolves_comment_id)
523 523 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
524 524 raise JSONRPCError(
525 525 'Comment `%s` is wrong type for setting status to resolved.'
526 526 % resolves_comment_id)
527 527
528 528 text = message
529 529 status_label = ChangesetStatus.get_status_lbl(status)
530 530 if status and allowed_to_change_status:
531 531 st_message = ('Status change %(transition_icon)s %(status)s'
532 532 % {'transition_icon': '>', 'status': status_label})
533 533 text = message or st_message
534 534
535 535 rc_config = SettingsModel().get_all_settings()
536 536 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
537 537
538 538 status_change = status and allowed_to_change_status
539 539 comment = CommentsModel().create(
540 540 text=text,
541 541 repo=pull_request.target_repo.repo_id,
542 542 user=apiuser.user_id,
543 543 pull_request=pull_request.pull_request_id,
544 544 f_path=None,
545 545 line_no=None,
546 546 status_change=(status_label if status_change else None),
547 547 status_change_type=(status if status_change else None),
548 548 closing_pr=False,
549 549 renderer=renderer,
550 550 comment_type=comment_type,
551 resolves_comment_id=resolves_comment_id
551 resolves_comment_id=resolves_comment_id,
552 auth_user=apiuser
552 553 )
553 554
554 555 if allowed_to_change_status and status:
555 556 ChangesetStatusModel().set_status(
556 557 pull_request.target_repo.repo_id,
557 558 status,
558 559 apiuser.user_id,
559 560 comment,
560 561 pull_request=pull_request.pull_request_id
561 562 )
562 563 Session().flush()
563 564
564 565 Session().commit()
565 566 data = {
566 567 'pull_request_id': pull_request.pull_request_id,
567 568 'comment_id': comment.comment_id if comment else None,
568 569 'status': {'given': status, 'was_changed': status_change},
569 570 }
570 571 return data
571 572
572 573
573 574 @jsonrpc_method()
574 575 def create_pull_request(
575 576 request, apiuser, source_repo, target_repo, source_ref, target_ref,
576 577 title, description=Optional(''), reviewers=Optional(None)):
577 578 """
578 579 Creates a new pull request.
579 580
580 581 Accepts refs in the following formats:
581 582
582 583 * branch:<branch_name>:<sha>
583 584 * branch:<branch_name>
584 585 * bookmark:<bookmark_name>:<sha> (Mercurial only)
585 586 * bookmark:<bookmark_name> (Mercurial only)
586 587
587 588 :param apiuser: This is filled automatically from the |authtoken|.
588 589 :type apiuser: AuthUser
589 590 :param source_repo: Set the source repository name.
590 591 :type source_repo: str
591 592 :param target_repo: Set the target repository name.
592 593 :type target_repo: str
593 594 :param source_ref: Set the source ref name.
594 595 :type source_ref: str
595 596 :param target_ref: Set the target ref name.
596 597 :type target_ref: str
597 598 :param title: Set the pull request title.
598 599 :type title: str
599 600 :param description: Set the pull request description.
600 601 :type description: Optional(str)
601 602 :param reviewers: Set the new pull request reviewers list.
602 603 Reviewer defined by review rules will be added automatically to the
603 604 defined list.
604 605 :type reviewers: Optional(list)
605 606 Accepts username strings or objects of the format:
606 607
607 608 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
608 609 """
609 610
610 611 source_db_repo = get_repo_or_error(source_repo)
611 612 target_db_repo = get_repo_or_error(target_repo)
612 613 if not has_superadmin_permission(apiuser):
613 614 _perms = ('repository.admin', 'repository.write', 'repository.read',)
614 615 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
615 616
616 617 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
617 618 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
618 619 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
619 620 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
620 621 source_scm = source_db_repo.scm_instance()
621 622 target_scm = target_db_repo.scm_instance()
622 623
623 624 commit_ranges = target_scm.compare(
624 625 target_commit.raw_id, source_commit.raw_id, source_scm,
625 626 merge=True, pre_load=[])
626 627
627 628 ancestor = target_scm.get_common_ancestor(
628 629 target_commit.raw_id, source_commit.raw_id, source_scm)
629 630
630 631 if not commit_ranges:
631 632 raise JSONRPCError('no commits found')
632 633
633 634 if not ancestor:
634 635 raise JSONRPCError('no common ancestor found')
635 636
636 637 reviewer_objects = Optional.extract(reviewers) or []
637 638
638 639 if reviewer_objects:
639 640 schema = ReviewerListSchema()
640 641 try:
641 642 reviewer_objects = schema.deserialize(reviewer_objects)
642 643 except Invalid as err:
643 644 raise JSONRPCValidationError(colander_exc=err)
644 645
645 646 # validate users
646 647 for reviewer_object in reviewer_objects:
647 648 user = get_user_or_error(reviewer_object['username'])
648 649 reviewer_object['user_id'] = user.user_id
649 650
650 651 get_default_reviewers_data, get_validated_reviewers = \
651 652 PullRequestModel().get_reviewer_functions()
652 653
653 654 reviewer_rules = get_default_reviewers_data(
654 655 apiuser.get_instance(), source_db_repo,
655 656 source_commit, target_db_repo, target_commit)
656 657
657 658 # specified rules are later re-validated, thus we can assume users will
658 659 # eventually provide those that meet the reviewer criteria.
659 660 if not reviewer_objects:
660 661 reviewer_objects = reviewer_rules['reviewers']
661 662
662 663 try:
663 664 reviewers = get_validated_reviewers(
664 665 reviewer_objects, reviewer_rules)
665 666 except ValueError as e:
666 667 raise JSONRPCError('Reviewers Validation: {}'.format(e))
667 668
668 669 pull_request_model = PullRequestModel()
669 670 pull_request = pull_request_model.create(
670 671 created_by=apiuser.user_id,
671 672 source_repo=source_repo,
672 673 source_ref=full_source_ref,
673 674 target_repo=target_repo,
674 675 target_ref=full_target_ref,
675 676 revisions=reversed(
676 677 [commit.raw_id for commit in reversed(commit_ranges)]),
677 678 reviewers=reviewers,
678 679 title=title,
679 680 description=Optional.extract(description)
680 681 )
681 682
682 683 Session().commit()
683 684 data = {
684 685 'msg': 'Created new pull request `{}`'.format(title),
685 686 'pull_request_id': pull_request.pull_request_id,
686 687 }
687 688 return data
688 689
689 690
690 691 @jsonrpc_method()
691 692 def update_pull_request(
692 693 request, apiuser, pullrequestid, repoid=Optional(None),
693 694 title=Optional(''), description=Optional(''), reviewers=Optional(None),
694 695 update_commits=Optional(None)):
695 696 """
696 697 Updates a pull request.
697 698
698 699 :param apiuser: This is filled automatically from the |authtoken|.
699 700 :type apiuser: AuthUser
700 701 :param repoid: Optional repository name or repository ID.
701 702 :type repoid: str or int
702 703 :param pullrequestid: The pull request ID.
703 704 :type pullrequestid: int
704 705 :param title: Set the pull request title.
705 706 :type title: str
706 707 :param description: Update pull request description.
707 708 :type description: Optional(str)
708 709 :param reviewers: Update pull request reviewers list with new value.
709 710 :type reviewers: Optional(list)
710 711 Accepts username strings or objects of the format:
711 712
712 713 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
713 714
714 715 :param update_commits: Trigger update of commits for this pull request
715 716 :type: update_commits: Optional(bool)
716 717
717 718 Example output:
718 719
719 720 .. code-block:: bash
720 721
721 722 id : <id_given_in_input>
722 723 result : {
723 724 "msg": "Updated pull request `63`",
724 725 "pull_request": <pull_request_object>,
725 726 "updated_reviewers": {
726 727 "added": [
727 728 "username"
728 729 ],
729 730 "removed": []
730 731 },
731 732 "updated_commits": {
732 733 "added": [
733 734 "<sha1_hash>"
734 735 ],
735 736 "common": [
736 737 "<sha1_hash>",
737 738 "<sha1_hash>",
738 739 ],
739 740 "removed": []
740 741 }
741 742 }
742 743 error : null
743 744 """
744 745
745 746 pull_request = get_pull_request_or_error(pullrequestid)
746 747 if Optional.extract(repoid):
747 748 repo = get_repo_or_error(repoid)
748 749 else:
749 750 repo = pull_request.target_repo
750 751
751 752 if not PullRequestModel().check_user_update(
752 753 pull_request, apiuser, api=True):
753 754 raise JSONRPCError(
754 755 'pull request `%s` update failed, no permission to update.' % (
755 756 pullrequestid,))
756 757 if pull_request.is_closed():
757 758 raise JSONRPCError(
758 759 'pull request `%s` update failed, pull request is closed' % (
759 760 pullrequestid,))
760 761
761 762 reviewer_objects = Optional.extract(reviewers) or []
762 763
763 764 if reviewer_objects:
764 765 schema = ReviewerListSchema()
765 766 try:
766 767 reviewer_objects = schema.deserialize(reviewer_objects)
767 768 except Invalid as err:
768 769 raise JSONRPCValidationError(colander_exc=err)
769 770
770 771 # validate users
771 772 for reviewer_object in reviewer_objects:
772 773 user = get_user_or_error(reviewer_object['username'])
773 774 reviewer_object['user_id'] = user.user_id
774 775
775 776 get_default_reviewers_data, get_validated_reviewers = \
776 777 PullRequestModel().get_reviewer_functions()
777 778
778 779 # re-use stored rules
779 780 reviewer_rules = pull_request.reviewer_data
780 781 try:
781 782 reviewers = get_validated_reviewers(
782 783 reviewer_objects, reviewer_rules)
783 784 except ValueError as e:
784 785 raise JSONRPCError('Reviewers Validation: {}'.format(e))
785 786 else:
786 787 reviewers = []
787 788
788 789 title = Optional.extract(title)
789 790 description = Optional.extract(description)
790 791 if title or description:
791 792 PullRequestModel().edit(
792 793 pull_request, title or pull_request.title,
793 794 description or pull_request.description, apiuser)
794 795 Session().commit()
795 796
796 797 commit_changes = {"added": [], "common": [], "removed": []}
797 798 if str2bool(Optional.extract(update_commits)):
798 799 if PullRequestModel().has_valid_update_type(pull_request):
799 800 update_response = PullRequestModel().update_commits(
800 801 pull_request)
801 802 commit_changes = update_response.changes or commit_changes
802 803 Session().commit()
803 804
804 805 reviewers_changes = {"added": [], "removed": []}
805 806 if reviewers:
806 807 added_reviewers, removed_reviewers = \
807 808 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
808 809
809 810 reviewers_changes['added'] = sorted(
810 811 [get_user_or_error(n).username for n in added_reviewers])
811 812 reviewers_changes['removed'] = sorted(
812 813 [get_user_or_error(n).username for n in removed_reviewers])
813 814 Session().commit()
814 815
815 816 data = {
816 817 'msg': 'Updated pull request `{}`'.format(
817 818 pull_request.pull_request_id),
818 819 'pull_request': pull_request.get_api_data(),
819 820 'updated_commits': commit_changes,
820 821 'updated_reviewers': reviewers_changes
821 822 }
822 823
823 824 return data
824 825
825 826
826 827 @jsonrpc_method()
827 828 def close_pull_request(
828 829 request, apiuser, pullrequestid, repoid=Optional(None),
829 830 userid=Optional(OAttr('apiuser')), message=Optional('')):
830 831 """
831 832 Close the pull request specified by `pullrequestid`.
832 833
833 834 :param apiuser: This is filled automatically from the |authtoken|.
834 835 :type apiuser: AuthUser
835 836 :param repoid: Repository name or repository ID to which the pull
836 837 request belongs.
837 838 :type repoid: str or int
838 839 :param pullrequestid: ID of the pull request to be closed.
839 840 :type pullrequestid: int
840 841 :param userid: Close the pull request as this user.
841 842 :type userid: Optional(str or int)
842 843 :param message: Optional message to close the Pull Request with. If not
843 844 specified it will be generated automatically.
844 845 :type message: Optional(str)
845 846
846 847 Example output:
847 848
848 849 .. code-block:: bash
849 850
850 851 "id": <id_given_in_input>,
851 852 "result": {
852 853 "pull_request_id": "<int>",
853 854 "close_status": "<str:status_lbl>,
854 855 "closed": "<bool>"
855 856 },
856 857 "error": null
857 858
858 859 """
859 860 _ = request.translate
860 861
861 862 pull_request = get_pull_request_or_error(pullrequestid)
862 863 if Optional.extract(repoid):
863 864 repo = get_repo_or_error(repoid)
864 865 else:
865 866 repo = pull_request.target_repo
866 867
867 868 if not isinstance(userid, Optional):
868 869 if (has_superadmin_permission(apiuser) or
869 870 HasRepoPermissionAnyApi('repository.admin')(
870 871 user=apiuser, repo_name=repo.repo_name)):
871 872 apiuser = get_user_or_error(userid)
872 873 else:
873 874 raise JSONRPCError('userid is not the same as your user')
874 875
875 876 if pull_request.is_closed():
876 877 raise JSONRPCError(
877 878 'pull request `%s` is already closed' % (pullrequestid,))
878 879
879 880 # only owner or admin or person with write permissions
880 881 allowed_to_close = PullRequestModel().check_user_update(
881 882 pull_request, apiuser, api=True)
882 883
883 884 if not allowed_to_close:
884 885 raise JSONRPCError(
885 886 'pull request `%s` close failed, no permission to close.' % (
886 887 pullrequestid,))
887 888
888 889 # message we're using to close the PR, else it's automatically generated
889 890 message = Optional.extract(message)
890 891
891 892 # finally close the PR, with proper message comment
892 893 comment, status = PullRequestModel().close_pull_request_with_comment(
893 894 pull_request, apiuser, repo, message=message)
894 895 status_lbl = ChangesetStatus.get_status_lbl(status)
895 896
896 897 Session().commit()
897 898
898 899 data = {
899 900 'pull_request_id': pull_request.pull_request_id,
900 901 'close_status': status_lbl,
901 902 'closed': True,
902 903 }
903 904 return data
@@ -1,2064 +1,2065 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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
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
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 39 from rhodecode.model.changeset_status import ChangesetStatusModel
40 40 from rhodecode.model.comment import CommentsModel
41 41 from rhodecode.model.db import (
42 42 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
43 43 ChangesetComment)
44 44 from rhodecode.model.repo import RepoModel
45 45 from rhodecode.model.scm import ScmModel, RepoList
46 46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
47 47 from rhodecode.model import validation_schema
48 48 from rhodecode.model.validation_schema.schemas import repo_schema
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 @jsonrpc_method()
54 54 def get_repo(request, apiuser, repoid, cache=Optional(True)):
55 55 """
56 56 Gets an existing repository by its name or repository_id.
57 57
58 58 The members section so the output returns users groups or users
59 59 associated with that repository.
60 60
61 61 This command can only be run using an |authtoken| with admin rights,
62 62 or users with at least read rights to the |repo|.
63 63
64 64 :param apiuser: This is filled automatically from the |authtoken|.
65 65 :type apiuser: AuthUser
66 66 :param repoid: The repository name or repository id.
67 67 :type repoid: str or int
68 68 :param cache: use the cached value for last changeset
69 69 :type: cache: Optional(bool)
70 70
71 71 Example output:
72 72
73 73 .. code-block:: bash
74 74
75 75 {
76 76 "error": null,
77 77 "id": <repo_id>,
78 78 "result": {
79 79 "clone_uri": null,
80 80 "created_on": "timestamp",
81 81 "description": "repo description",
82 82 "enable_downloads": false,
83 83 "enable_locking": false,
84 84 "enable_statistics": false,
85 85 "followers": [
86 86 {
87 87 "active": true,
88 88 "admin": false,
89 89 "api_key": "****************************************",
90 90 "api_keys": [
91 91 "****************************************"
92 92 ],
93 93 "email": "user@example.com",
94 94 "emails": [
95 95 "user@example.com"
96 96 ],
97 97 "extern_name": "rhodecode",
98 98 "extern_type": "rhodecode",
99 99 "firstname": "username",
100 100 "ip_addresses": [],
101 101 "language": null,
102 102 "last_login": "2015-09-16T17:16:35.854",
103 103 "lastname": "surname",
104 104 "user_id": <user_id>,
105 105 "username": "name"
106 106 }
107 107 ],
108 108 "fork_of": "parent-repo",
109 109 "landing_rev": [
110 110 "rev",
111 111 "tip"
112 112 ],
113 113 "last_changeset": {
114 114 "author": "User <user@example.com>",
115 115 "branch": "default",
116 116 "date": "timestamp",
117 117 "message": "last commit message",
118 118 "parents": [
119 119 {
120 120 "raw_id": "commit-id"
121 121 }
122 122 ],
123 123 "raw_id": "commit-id",
124 124 "revision": <revision number>,
125 125 "short_id": "short id"
126 126 },
127 127 "lock_reason": null,
128 128 "locked_by": null,
129 129 "locked_date": null,
130 130 "owner": "owner-name",
131 131 "permissions": [
132 132 {
133 133 "name": "super-admin-name",
134 134 "origin": "super-admin",
135 135 "permission": "repository.admin",
136 136 "type": "user"
137 137 },
138 138 {
139 139 "name": "owner-name",
140 140 "origin": "owner",
141 141 "permission": "repository.admin",
142 142 "type": "user"
143 143 },
144 144 {
145 145 "name": "user-group-name",
146 146 "origin": "permission",
147 147 "permission": "repository.write",
148 148 "type": "user_group"
149 149 }
150 150 ],
151 151 "private": true,
152 152 "repo_id": 676,
153 153 "repo_name": "user-group/repo-name",
154 154 "repo_type": "hg"
155 155 }
156 156 }
157 157 """
158 158
159 159 repo = get_repo_or_error(repoid)
160 160 cache = Optional.extract(cache)
161 161
162 162 include_secrets = False
163 163 if has_superadmin_permission(apiuser):
164 164 include_secrets = True
165 165 else:
166 166 # check if we have at least read permission for this repo !
167 167 _perms = (
168 168 'repository.admin', 'repository.write', 'repository.read',)
169 169 validate_repo_permissions(apiuser, repoid, repo, _perms)
170 170
171 171 permissions = []
172 172 for _user in repo.permissions():
173 173 user_data = {
174 174 'name': _user.username,
175 175 'permission': _user.permission,
176 176 'origin': get_origin(_user),
177 177 'type': "user",
178 178 }
179 179 permissions.append(user_data)
180 180
181 181 for _user_group in repo.permission_user_groups():
182 182 user_group_data = {
183 183 'name': _user_group.users_group_name,
184 184 'permission': _user_group.permission,
185 185 'origin': get_origin(_user_group),
186 186 'type': "user_group",
187 187 }
188 188 permissions.append(user_group_data)
189 189
190 190 following_users = [
191 191 user.user.get_api_data(include_secrets=include_secrets)
192 192 for user in repo.followers]
193 193
194 194 if not cache:
195 195 repo.update_commit_cache()
196 196 data = repo.get_api_data(include_secrets=include_secrets)
197 197 data['permissions'] = permissions
198 198 data['followers'] = following_users
199 199 return data
200 200
201 201
202 202 @jsonrpc_method()
203 203 def get_repos(request, apiuser, root=Optional(None), traverse=Optional(True)):
204 204 """
205 205 Lists all existing repositories.
206 206
207 207 This command can only be run using an |authtoken| with admin rights,
208 208 or users with at least read rights to |repos|.
209 209
210 210 :param apiuser: This is filled automatically from the |authtoken|.
211 211 :type apiuser: AuthUser
212 212 :param root: specify root repository group to fetch repositories.
213 213 filters the returned repositories to be members of given root group.
214 214 :type root: Optional(None)
215 215 :param traverse: traverse given root into subrepositories. With this flag
216 216 set to False, it will only return top-level repositories from `root`.
217 217 if root is empty it will return just top-level repositories.
218 218 :type traverse: Optional(True)
219 219
220 220
221 221 Example output:
222 222
223 223 .. code-block:: bash
224 224
225 225 id : <id_given_in_input>
226 226 result: [
227 227 {
228 228 "repo_id" : "<repo_id>",
229 229 "repo_name" : "<reponame>"
230 230 "repo_type" : "<repo_type>",
231 231 "clone_uri" : "<clone_uri>",
232 232 "private": : "<bool>",
233 233 "created_on" : "<datetimecreated>",
234 234 "description" : "<description>",
235 235 "landing_rev": "<landing_rev>",
236 236 "owner": "<repo_owner>",
237 237 "fork_of": "<name_of_fork_parent>",
238 238 "enable_downloads": "<bool>",
239 239 "enable_locking": "<bool>",
240 240 "enable_statistics": "<bool>",
241 241 },
242 242 ...
243 243 ]
244 244 error: null
245 245 """
246 246
247 247 include_secrets = has_superadmin_permission(apiuser)
248 248 _perms = ('repository.read', 'repository.write', 'repository.admin',)
249 249 extras = {'user': apiuser}
250 250
251 251 root = Optional.extract(root)
252 252 traverse = Optional.extract(traverse, binary=True)
253 253
254 254 if root:
255 255 # verify parent existance, if it's empty return an error
256 256 parent = RepoGroup.get_by_group_name(root)
257 257 if not parent:
258 258 raise JSONRPCError(
259 259 'Root repository group `{}` does not exist'.format(root))
260 260
261 261 if traverse:
262 262 repos = RepoModel().get_repos_for_root(root=root, traverse=traverse)
263 263 else:
264 264 repos = RepoModel().get_repos_for_root(root=parent)
265 265 else:
266 266 if traverse:
267 267 repos = RepoModel().get_all()
268 268 else:
269 269 # return just top-level
270 270 repos = RepoModel().get_repos_for_root(root=None)
271 271
272 272 repo_list = RepoList(repos, perm_set=_perms, extra_kwargs=extras)
273 273 return [repo.get_api_data(include_secrets=include_secrets)
274 274 for repo in repo_list]
275 275
276 276
277 277 @jsonrpc_method()
278 278 def get_repo_changeset(request, apiuser, repoid, revision,
279 279 details=Optional('basic')):
280 280 """
281 281 Returns information about a changeset.
282 282
283 283 Additionally parameters define the amount of details returned by
284 284 this function.
285 285
286 286 This command can only be run using an |authtoken| with admin rights,
287 287 or users with at least read rights to the |repo|.
288 288
289 289 :param apiuser: This is filled automatically from the |authtoken|.
290 290 :type apiuser: AuthUser
291 291 :param repoid: The repository name or repository id
292 292 :type repoid: str or int
293 293 :param revision: revision for which listing should be done
294 294 :type revision: str
295 295 :param details: details can be 'basic|extended|full' full gives diff
296 296 info details like the diff itself, and number of changed files etc.
297 297 :type details: Optional(str)
298 298
299 299 """
300 300 repo = get_repo_or_error(repoid)
301 301 if not has_superadmin_permission(apiuser):
302 302 _perms = (
303 303 'repository.admin', 'repository.write', 'repository.read',)
304 304 validate_repo_permissions(apiuser, repoid, repo, _perms)
305 305
306 306 changes_details = Optional.extract(details)
307 307 _changes_details_types = ['basic', 'extended', 'full']
308 308 if changes_details not in _changes_details_types:
309 309 raise JSONRPCError(
310 310 'ret_type must be one of %s' % (
311 311 ','.join(_changes_details_types)))
312 312
313 313 pre_load = ['author', 'branch', 'date', 'message', 'parents',
314 314 'status', '_commit', '_file_paths']
315 315
316 316 try:
317 317 cs = repo.get_commit(commit_id=revision, pre_load=pre_load)
318 318 except TypeError as e:
319 319 raise JSONRPCError(e.message)
320 320 _cs_json = cs.__json__()
321 321 _cs_json['diff'] = build_commit_data(cs, changes_details)
322 322 if changes_details == 'full':
323 323 _cs_json['refs'] = cs._get_refs()
324 324 return _cs_json
325 325
326 326
327 327 @jsonrpc_method()
328 328 def get_repo_changesets(request, apiuser, repoid, start_rev, limit,
329 329 details=Optional('basic')):
330 330 """
331 331 Returns a set of commits limited by the number starting
332 332 from the `start_rev` option.
333 333
334 334 Additional parameters define the amount of details returned by this
335 335 function.
336 336
337 337 This command can only be run using an |authtoken| with admin rights,
338 338 or users with at least read rights to |repos|.
339 339
340 340 :param apiuser: This is filled automatically from the |authtoken|.
341 341 :type apiuser: AuthUser
342 342 :param repoid: The repository name or repository ID.
343 343 :type repoid: str or int
344 344 :param start_rev: The starting revision from where to get changesets.
345 345 :type start_rev: str
346 346 :param limit: Limit the number of commits to this amount
347 347 :type limit: str or int
348 348 :param details: Set the level of detail returned. Valid option are:
349 349 ``basic``, ``extended`` and ``full``.
350 350 :type details: Optional(str)
351 351
352 352 .. note::
353 353
354 354 Setting the parameter `details` to the value ``full`` is extensive
355 355 and returns details like the diff itself, and the number
356 356 of changed files.
357 357
358 358 """
359 359 repo = get_repo_or_error(repoid)
360 360 if not has_superadmin_permission(apiuser):
361 361 _perms = (
362 362 'repository.admin', 'repository.write', 'repository.read',)
363 363 validate_repo_permissions(apiuser, repoid, repo, _perms)
364 364
365 365 changes_details = Optional.extract(details)
366 366 _changes_details_types = ['basic', 'extended', 'full']
367 367 if changes_details not in _changes_details_types:
368 368 raise JSONRPCError(
369 369 'ret_type must be one of %s' % (
370 370 ','.join(_changes_details_types)))
371 371
372 372 limit = int(limit)
373 373 pre_load = ['author', 'branch', 'date', 'message', 'parents',
374 374 'status', '_commit', '_file_paths']
375 375
376 376 vcs_repo = repo.scm_instance()
377 377 # SVN needs a special case to distinguish its index and commit id
378 378 if vcs_repo and vcs_repo.alias == 'svn' and (start_rev == '0'):
379 379 start_rev = vcs_repo.commit_ids[0]
380 380
381 381 try:
382 382 commits = vcs_repo.get_commits(
383 383 start_id=start_rev, pre_load=pre_load)
384 384 except TypeError as e:
385 385 raise JSONRPCError(e.message)
386 386 except Exception:
387 387 log.exception('Fetching of commits failed')
388 388 raise JSONRPCError('Error occurred during commit fetching')
389 389
390 390 ret = []
391 391 for cnt, commit in enumerate(commits):
392 392 if cnt >= limit != -1:
393 393 break
394 394 _cs_json = commit.__json__()
395 395 _cs_json['diff'] = build_commit_data(commit, changes_details)
396 396 if changes_details == 'full':
397 397 _cs_json['refs'] = {
398 398 'branches': [commit.branch],
399 399 'bookmarks': getattr(commit, 'bookmarks', []),
400 400 'tags': commit.tags
401 401 }
402 402 ret.append(_cs_json)
403 403 return ret
404 404
405 405
406 406 @jsonrpc_method()
407 407 def get_repo_nodes(request, apiuser, repoid, revision, root_path,
408 408 ret_type=Optional('all'), details=Optional('basic'),
409 409 max_file_bytes=Optional(None)):
410 410 """
411 411 Returns a list of nodes and children in a flat list for a given
412 412 path at given revision.
413 413
414 414 It's possible to specify ret_type to show only `files` or `dirs`.
415 415
416 416 This command can only be run using an |authtoken| with admin rights,
417 417 or users with at least read rights to |repos|.
418 418
419 419 :param apiuser: This is filled automatically from the |authtoken|.
420 420 :type apiuser: AuthUser
421 421 :param repoid: The repository name or repository ID.
422 422 :type repoid: str or int
423 423 :param revision: The revision for which listing should be done.
424 424 :type revision: str
425 425 :param root_path: The path from which to start displaying.
426 426 :type root_path: str
427 427 :param ret_type: Set the return type. Valid options are
428 428 ``all`` (default), ``files`` and ``dirs``.
429 429 :type ret_type: Optional(str)
430 430 :param details: Returns extended information about nodes, such as
431 431 md5, binary, and or content. The valid options are ``basic`` and
432 432 ``full``.
433 433 :type details: Optional(str)
434 434 :param max_file_bytes: Only return file content under this file size bytes
435 435 :type details: Optional(int)
436 436
437 437 Example output:
438 438
439 439 .. code-block:: bash
440 440
441 441 id : <id_given_in_input>
442 442 result: [
443 443 {
444 444 "name" : "<name>"
445 445 "type" : "<type>",
446 446 "binary": "<true|false>" (only in extended mode)
447 447 "md5" : "<md5 of file content>" (only in extended mode)
448 448 },
449 449 ...
450 450 ]
451 451 error: null
452 452 """
453 453
454 454 repo = get_repo_or_error(repoid)
455 455 if not has_superadmin_permission(apiuser):
456 456 _perms = (
457 457 'repository.admin', 'repository.write', 'repository.read',)
458 458 validate_repo_permissions(apiuser, repoid, repo, _perms)
459 459
460 460 ret_type = Optional.extract(ret_type)
461 461 details = Optional.extract(details)
462 462 _extended_types = ['basic', 'full']
463 463 if details not in _extended_types:
464 464 raise JSONRPCError(
465 465 'ret_type must be one of %s' % (','.join(_extended_types)))
466 466 extended_info = False
467 467 content = False
468 468 if details == 'basic':
469 469 extended_info = True
470 470
471 471 if details == 'full':
472 472 extended_info = content = True
473 473
474 474 _map = {}
475 475 try:
476 476 # check if repo is not empty by any chance, skip quicker if it is.
477 477 _scm = repo.scm_instance()
478 478 if _scm.is_empty():
479 479 return []
480 480
481 481 _d, _f = ScmModel().get_nodes(
482 482 repo, revision, root_path, flat=False,
483 483 extended_info=extended_info, content=content,
484 484 max_file_bytes=max_file_bytes)
485 485 _map = {
486 486 'all': _d + _f,
487 487 'files': _f,
488 488 'dirs': _d,
489 489 }
490 490 return _map[ret_type]
491 491 except KeyError:
492 492 raise JSONRPCError(
493 493 'ret_type must be one of %s' % (','.join(sorted(_map.keys()))))
494 494 except Exception:
495 495 log.exception("Exception occurred while trying to get repo nodes")
496 496 raise JSONRPCError(
497 497 'failed to get repo: `%s` nodes' % repo.repo_name
498 498 )
499 499
500 500
501 501 @jsonrpc_method()
502 502 def get_repo_refs(request, apiuser, repoid):
503 503 """
504 504 Returns a dictionary of current references. It returns
505 505 bookmarks, branches, closed_branches, and tags for given repository
506 506
507 507 It's possible to specify ret_type to show only `files` or `dirs`.
508 508
509 509 This command can only be run using an |authtoken| with admin rights,
510 510 or users with at least read rights to |repos|.
511 511
512 512 :param apiuser: This is filled automatically from the |authtoken|.
513 513 :type apiuser: AuthUser
514 514 :param repoid: The repository name or repository ID.
515 515 :type repoid: str or int
516 516
517 517 Example output:
518 518
519 519 .. code-block:: bash
520 520
521 521 id : <id_given_in_input>
522 522 "result": {
523 523 "bookmarks": {
524 524 "dev": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
525 525 "master": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
526 526 },
527 527 "branches": {
528 528 "default": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
529 529 "stable": "367f590445081d8ec8c2ea0456e73ae1f1c3d6cf"
530 530 },
531 531 "branches_closed": {},
532 532 "tags": {
533 533 "tip": "5611d30200f4040ba2ab4f3d64e5b06408a02188",
534 534 "v4.4.0": "1232313f9e6adac5ce5399c2a891dc1e72b79022",
535 535 "v4.4.1": "cbb9f1d329ae5768379cdec55a62ebdd546c4e27",
536 536 "v4.4.2": "24ffe44a27fcd1c5b6936144e176b9f6dd2f3a17",
537 537 }
538 538 }
539 539 error: null
540 540 """
541 541
542 542 repo = get_repo_or_error(repoid)
543 543 if not has_superadmin_permission(apiuser):
544 544 _perms = ('repository.admin', 'repository.write', 'repository.read',)
545 545 validate_repo_permissions(apiuser, repoid, repo, _perms)
546 546
547 547 try:
548 548 # check if repo is not empty by any chance, skip quicker if it is.
549 549 vcs_instance = repo.scm_instance()
550 550 refs = vcs_instance.refs()
551 551 return refs
552 552 except Exception:
553 553 log.exception("Exception occurred while trying to get repo refs")
554 554 raise JSONRPCError(
555 555 'failed to get repo: `%s` references' % repo.repo_name
556 556 )
557 557
558 558
559 559 @jsonrpc_method()
560 560 def create_repo(
561 561 request, apiuser, repo_name, repo_type,
562 562 owner=Optional(OAttr('apiuser')),
563 563 description=Optional(''),
564 564 private=Optional(False),
565 565 clone_uri=Optional(None),
566 566 push_uri=Optional(None),
567 567 landing_rev=Optional('rev:tip'),
568 568 enable_statistics=Optional(False),
569 569 enable_locking=Optional(False),
570 570 enable_downloads=Optional(False),
571 571 copy_permissions=Optional(False)):
572 572 """
573 573 Creates a repository.
574 574
575 575 * If the repository name contains "/", repository will be created inside
576 576 a repository group or nested repository groups
577 577
578 578 For example "foo/bar/repo1" will create |repo| called "repo1" inside
579 579 group "foo/bar". You have to have permissions to access and write to
580 580 the last repository group ("bar" in this example)
581 581
582 582 This command can only be run using an |authtoken| with at least
583 583 permissions to create repositories, or write permissions to
584 584 parent repository groups.
585 585
586 586 :param apiuser: This is filled automatically from the |authtoken|.
587 587 :type apiuser: AuthUser
588 588 :param repo_name: Set the repository name.
589 589 :type repo_name: str
590 590 :param repo_type: Set the repository type; 'hg','git', or 'svn'.
591 591 :type repo_type: str
592 592 :param owner: user_id or username
593 593 :type owner: Optional(str)
594 594 :param description: Set the repository description.
595 595 :type description: Optional(str)
596 596 :param private: set repository as private
597 597 :type private: bool
598 598 :param clone_uri: set clone_uri
599 599 :type clone_uri: str
600 600 :param push_uri: set push_uri
601 601 :type push_uri: str
602 602 :param landing_rev: <rev_type>:<rev>
603 603 :type landing_rev: str
604 604 :param enable_locking:
605 605 :type enable_locking: bool
606 606 :param enable_downloads:
607 607 :type enable_downloads: bool
608 608 :param enable_statistics:
609 609 :type enable_statistics: bool
610 610 :param copy_permissions: Copy permission from group in which the
611 611 repository is being created.
612 612 :type copy_permissions: bool
613 613
614 614
615 615 Example output:
616 616
617 617 .. code-block:: bash
618 618
619 619 id : <id_given_in_input>
620 620 result: {
621 621 "msg": "Created new repository `<reponame>`",
622 622 "success": true,
623 623 "task": "<celery task id or None if done sync>"
624 624 }
625 625 error: null
626 626
627 627
628 628 Example error output:
629 629
630 630 .. code-block:: bash
631 631
632 632 id : <id_given_in_input>
633 633 result : null
634 634 error : {
635 635 'failed to create repository `<repo_name>`'
636 636 }
637 637
638 638 """
639 639
640 640 owner = validate_set_owner_permissions(apiuser, owner)
641 641
642 642 description = Optional.extract(description)
643 643 copy_permissions = Optional.extract(copy_permissions)
644 644 clone_uri = Optional.extract(clone_uri)
645 645 push_uri = Optional.extract(push_uri)
646 646 landing_commit_ref = Optional.extract(landing_rev)
647 647
648 648 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
649 649 if isinstance(private, Optional):
650 650 private = defs.get('repo_private') or Optional.extract(private)
651 651 if isinstance(repo_type, Optional):
652 652 repo_type = defs.get('repo_type')
653 653 if isinstance(enable_statistics, Optional):
654 654 enable_statistics = defs.get('repo_enable_statistics')
655 655 if isinstance(enable_locking, Optional):
656 656 enable_locking = defs.get('repo_enable_locking')
657 657 if isinstance(enable_downloads, Optional):
658 658 enable_downloads = defs.get('repo_enable_downloads')
659 659
660 660 schema = repo_schema.RepoSchema().bind(
661 661 repo_type_options=rhodecode.BACKENDS.keys(),
662 662 repo_type=repo_type,
663 663 # user caller
664 664 user=apiuser)
665 665
666 666 try:
667 667 schema_data = schema.deserialize(dict(
668 668 repo_name=repo_name,
669 669 repo_type=repo_type,
670 670 repo_owner=owner.username,
671 671 repo_description=description,
672 672 repo_landing_commit_ref=landing_commit_ref,
673 673 repo_clone_uri=clone_uri,
674 674 repo_push_uri=push_uri,
675 675 repo_private=private,
676 676 repo_copy_permissions=copy_permissions,
677 677 repo_enable_statistics=enable_statistics,
678 678 repo_enable_downloads=enable_downloads,
679 679 repo_enable_locking=enable_locking))
680 680 except validation_schema.Invalid as err:
681 681 raise JSONRPCValidationError(colander_exc=err)
682 682
683 683 try:
684 684 data = {
685 685 'owner': owner,
686 686 'repo_name': schema_data['repo_group']['repo_name_without_group'],
687 687 'repo_name_full': schema_data['repo_name'],
688 688 'repo_group': schema_data['repo_group']['repo_group_id'],
689 689 'repo_type': schema_data['repo_type'],
690 690 'repo_description': schema_data['repo_description'],
691 691 'repo_private': schema_data['repo_private'],
692 692 'clone_uri': schema_data['repo_clone_uri'],
693 693 'push_uri': schema_data['repo_push_uri'],
694 694 'repo_landing_rev': schema_data['repo_landing_commit_ref'],
695 695 'enable_statistics': schema_data['repo_enable_statistics'],
696 696 'enable_locking': schema_data['repo_enable_locking'],
697 697 'enable_downloads': schema_data['repo_enable_downloads'],
698 698 'repo_copy_permissions': schema_data['repo_copy_permissions'],
699 699 }
700 700
701 701 task = RepoModel().create(form_data=data, cur_user=owner.user_id)
702 702 task_id = get_task_id(task)
703 703 # no commit, it's done in RepoModel, or async via celery
704 704 return {
705 705 'msg': "Created new repository `%s`" % (schema_data['repo_name'],),
706 706 'success': True, # cannot return the repo data here since fork
707 707 # can be done async
708 708 'task': task_id
709 709 }
710 710 except Exception:
711 711 log.exception(
712 712 u"Exception while trying to create the repository %s",
713 713 schema_data['repo_name'])
714 714 raise JSONRPCError(
715 715 'failed to create repository `%s`' % (schema_data['repo_name'],))
716 716
717 717
718 718 @jsonrpc_method()
719 719 def add_field_to_repo(request, apiuser, repoid, key, label=Optional(''),
720 720 description=Optional('')):
721 721 """
722 722 Adds an extra field to a repository.
723 723
724 724 This command can only be run using an |authtoken| with at least
725 725 write permissions to the |repo|.
726 726
727 727 :param apiuser: This is filled automatically from the |authtoken|.
728 728 :type apiuser: AuthUser
729 729 :param repoid: Set the repository name or repository id.
730 730 :type repoid: str or int
731 731 :param key: Create a unique field key for this repository.
732 732 :type key: str
733 733 :param label:
734 734 :type label: Optional(str)
735 735 :param description:
736 736 :type description: Optional(str)
737 737 """
738 738 repo = get_repo_or_error(repoid)
739 739 if not has_superadmin_permission(apiuser):
740 740 _perms = ('repository.admin',)
741 741 validate_repo_permissions(apiuser, repoid, repo, _perms)
742 742
743 743 label = Optional.extract(label) or key
744 744 description = Optional.extract(description)
745 745
746 746 field = RepositoryField.get_by_key_name(key, repo)
747 747 if field:
748 748 raise JSONRPCError('Field with key '
749 749 '`%s` exists for repo `%s`' % (key, repoid))
750 750
751 751 try:
752 752 RepoModel().add_repo_field(repo, key, field_label=label,
753 753 field_desc=description)
754 754 Session().commit()
755 755 return {
756 756 'msg': "Added new repository field `%s`" % (key,),
757 757 'success': True,
758 758 }
759 759 except Exception:
760 760 log.exception("Exception occurred while trying to add field to repo")
761 761 raise JSONRPCError(
762 762 'failed to create new field for repository `%s`' % (repoid,))
763 763
764 764
765 765 @jsonrpc_method()
766 766 def remove_field_from_repo(request, apiuser, repoid, key):
767 767 """
768 768 Removes an extra field from a repository.
769 769
770 770 This command can only be run using an |authtoken| with at least
771 771 write permissions to the |repo|.
772 772
773 773 :param apiuser: This is filled automatically from the |authtoken|.
774 774 :type apiuser: AuthUser
775 775 :param repoid: Set the repository name or repository ID.
776 776 :type repoid: str or int
777 777 :param key: Set the unique field key for this repository.
778 778 :type key: str
779 779 """
780 780
781 781 repo = get_repo_or_error(repoid)
782 782 if not has_superadmin_permission(apiuser):
783 783 _perms = ('repository.admin',)
784 784 validate_repo_permissions(apiuser, repoid, repo, _perms)
785 785
786 786 field = RepositoryField.get_by_key_name(key, repo)
787 787 if not field:
788 788 raise JSONRPCError('Field with key `%s` does not '
789 789 'exists for repo `%s`' % (key, repoid))
790 790
791 791 try:
792 792 RepoModel().delete_repo_field(repo, field_key=key)
793 793 Session().commit()
794 794 return {
795 795 'msg': "Deleted repository field `%s`" % (key,),
796 796 'success': True,
797 797 }
798 798 except Exception:
799 799 log.exception(
800 800 "Exception occurred while trying to delete field from repo")
801 801 raise JSONRPCError(
802 802 'failed to delete field for repository `%s`' % (repoid,))
803 803
804 804
805 805 @jsonrpc_method()
806 806 def update_repo(
807 807 request, apiuser, repoid, repo_name=Optional(None),
808 808 owner=Optional(OAttr('apiuser')), description=Optional(''),
809 809 private=Optional(False),
810 810 clone_uri=Optional(None), push_uri=Optional(None),
811 811 landing_rev=Optional('rev:tip'), fork_of=Optional(None),
812 812 enable_statistics=Optional(False),
813 813 enable_locking=Optional(False),
814 814 enable_downloads=Optional(False), fields=Optional('')):
815 815 """
816 816 Updates a repository with the given information.
817 817
818 818 This command can only be run using an |authtoken| with at least
819 819 admin permissions to the |repo|.
820 820
821 821 * If the repository name contains "/", repository will be updated
822 822 accordingly with a repository group or nested repository groups
823 823
824 824 For example repoid=repo-test name="foo/bar/repo-test" will update |repo|
825 825 called "repo-test" and place it inside group "foo/bar".
826 826 You have to have permissions to access and write to the last repository
827 827 group ("bar" in this example)
828 828
829 829 :param apiuser: This is filled automatically from the |authtoken|.
830 830 :type apiuser: AuthUser
831 831 :param repoid: repository name or repository ID.
832 832 :type repoid: str or int
833 833 :param repo_name: Update the |repo| name, including the
834 834 repository group it's in.
835 835 :type repo_name: str
836 836 :param owner: Set the |repo| owner.
837 837 :type owner: str
838 838 :param fork_of: Set the |repo| as fork of another |repo|.
839 839 :type fork_of: str
840 840 :param description: Update the |repo| description.
841 841 :type description: str
842 842 :param private: Set the |repo| as private. (True | False)
843 843 :type private: bool
844 844 :param clone_uri: Update the |repo| clone URI.
845 845 :type clone_uri: str
846 846 :param landing_rev: Set the |repo| landing revision. Default is ``rev:tip``.
847 847 :type landing_rev: str
848 848 :param enable_statistics: Enable statistics on the |repo|, (True | False).
849 849 :type enable_statistics: bool
850 850 :param enable_locking: Enable |repo| locking.
851 851 :type enable_locking: bool
852 852 :param enable_downloads: Enable downloads from the |repo|, (True | False).
853 853 :type enable_downloads: bool
854 854 :param fields: Add extra fields to the |repo|. Use the following
855 855 example format: ``field_key=field_val,field_key2=fieldval2``.
856 856 Escape ', ' with \,
857 857 :type fields: str
858 858 """
859 859
860 860 repo = get_repo_or_error(repoid)
861 861
862 862 include_secrets = False
863 863 if not has_superadmin_permission(apiuser):
864 864 validate_repo_permissions(apiuser, repoid, repo, ('repository.admin',))
865 865 else:
866 866 include_secrets = True
867 867
868 868 updates = dict(
869 869 repo_name=repo_name
870 870 if not isinstance(repo_name, Optional) else repo.repo_name,
871 871
872 872 fork_id=fork_of
873 873 if not isinstance(fork_of, Optional) else repo.fork.repo_name if repo.fork else None,
874 874
875 875 user=owner
876 876 if not isinstance(owner, Optional) else repo.user.username,
877 877
878 878 repo_description=description
879 879 if not isinstance(description, Optional) else repo.description,
880 880
881 881 repo_private=private
882 882 if not isinstance(private, Optional) else repo.private,
883 883
884 884 clone_uri=clone_uri
885 885 if not isinstance(clone_uri, Optional) else repo.clone_uri,
886 886
887 887 push_uri=push_uri
888 888 if not isinstance(push_uri, Optional) else repo.push_uri,
889 889
890 890 repo_landing_rev=landing_rev
891 891 if not isinstance(landing_rev, Optional) else repo._landing_revision,
892 892
893 893 repo_enable_statistics=enable_statistics
894 894 if not isinstance(enable_statistics, Optional) else repo.enable_statistics,
895 895
896 896 repo_enable_locking=enable_locking
897 897 if not isinstance(enable_locking, Optional) else repo.enable_locking,
898 898
899 899 repo_enable_downloads=enable_downloads
900 900 if not isinstance(enable_downloads, Optional) else repo.enable_downloads)
901 901
902 902 ref_choices, _labels = ScmModel().get_repo_landing_revs(
903 903 request.translate, repo=repo)
904 904
905 905 old_values = repo.get_api_data()
906 906 repo_type = repo.repo_type
907 907 schema = repo_schema.RepoSchema().bind(
908 908 repo_type_options=rhodecode.BACKENDS.keys(),
909 909 repo_ref_options=ref_choices,
910 910 repo_type=repo_type,
911 911 # user caller
912 912 user=apiuser,
913 913 old_values=old_values)
914 914 try:
915 915 schema_data = schema.deserialize(dict(
916 916 # we save old value, users cannot change type
917 917 repo_type=repo_type,
918 918
919 919 repo_name=updates['repo_name'],
920 920 repo_owner=updates['user'],
921 921 repo_description=updates['repo_description'],
922 922 repo_clone_uri=updates['clone_uri'],
923 923 repo_push_uri=updates['push_uri'],
924 924 repo_fork_of=updates['fork_id'],
925 925 repo_private=updates['repo_private'],
926 926 repo_landing_commit_ref=updates['repo_landing_rev'],
927 927 repo_enable_statistics=updates['repo_enable_statistics'],
928 928 repo_enable_downloads=updates['repo_enable_downloads'],
929 929 repo_enable_locking=updates['repo_enable_locking']))
930 930 except validation_schema.Invalid as err:
931 931 raise JSONRPCValidationError(colander_exc=err)
932 932
933 933 # save validated data back into the updates dict
934 934 validated_updates = dict(
935 935 repo_name=schema_data['repo_group']['repo_name_without_group'],
936 936 repo_group=schema_data['repo_group']['repo_group_id'],
937 937
938 938 user=schema_data['repo_owner'],
939 939 repo_description=schema_data['repo_description'],
940 940 repo_private=schema_data['repo_private'],
941 941 clone_uri=schema_data['repo_clone_uri'],
942 942 push_uri=schema_data['repo_push_uri'],
943 943 repo_landing_rev=schema_data['repo_landing_commit_ref'],
944 944 repo_enable_statistics=schema_data['repo_enable_statistics'],
945 945 repo_enable_locking=schema_data['repo_enable_locking'],
946 946 repo_enable_downloads=schema_data['repo_enable_downloads'],
947 947 )
948 948
949 949 if schema_data['repo_fork_of']:
950 950 fork_repo = get_repo_or_error(schema_data['repo_fork_of'])
951 951 validated_updates['fork_id'] = fork_repo.repo_id
952 952
953 953 # extra fields
954 954 fields = parse_args(Optional.extract(fields), key_prefix='ex_')
955 955 if fields:
956 956 validated_updates.update(fields)
957 957
958 958 try:
959 959 RepoModel().update(repo, **validated_updates)
960 960 audit_logger.store_api(
961 961 'repo.edit', action_data={'old_data': old_values},
962 962 user=apiuser, repo=repo)
963 963 Session().commit()
964 964 return {
965 965 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
966 966 'repository': repo.get_api_data(include_secrets=include_secrets)
967 967 }
968 968 except Exception:
969 969 log.exception(
970 970 u"Exception while trying to update the repository %s",
971 971 repoid)
972 972 raise JSONRPCError('failed to update repo `%s`' % repoid)
973 973
974 974
975 975 @jsonrpc_method()
976 976 def fork_repo(request, apiuser, repoid, fork_name,
977 977 owner=Optional(OAttr('apiuser')),
978 978 description=Optional(''),
979 979 private=Optional(False),
980 980 clone_uri=Optional(None),
981 981 landing_rev=Optional('rev:tip'),
982 982 copy_permissions=Optional(False)):
983 983 """
984 984 Creates a fork of the specified |repo|.
985 985
986 986 * If the fork_name contains "/", fork will be created inside
987 987 a repository group or nested repository groups
988 988
989 989 For example "foo/bar/fork-repo" will create fork called "fork-repo"
990 990 inside group "foo/bar". You have to have permissions to access and
991 991 write to the last repository group ("bar" in this example)
992 992
993 993 This command can only be run using an |authtoken| with minimum
994 994 read permissions of the forked repo, create fork permissions for an user.
995 995
996 996 :param apiuser: This is filled automatically from the |authtoken|.
997 997 :type apiuser: AuthUser
998 998 :param repoid: Set repository name or repository ID.
999 999 :type repoid: str or int
1000 1000 :param fork_name: Set the fork name, including it's repository group membership.
1001 1001 :type fork_name: str
1002 1002 :param owner: Set the fork owner.
1003 1003 :type owner: str
1004 1004 :param description: Set the fork description.
1005 1005 :type description: str
1006 1006 :param copy_permissions: Copy permissions from parent |repo|. The
1007 1007 default is False.
1008 1008 :type copy_permissions: bool
1009 1009 :param private: Make the fork private. The default is False.
1010 1010 :type private: bool
1011 1011 :param landing_rev: Set the landing revision. The default is tip.
1012 1012
1013 1013 Example output:
1014 1014
1015 1015 .. code-block:: bash
1016 1016
1017 1017 id : <id_for_response>
1018 1018 api_key : "<api_key>"
1019 1019 args: {
1020 1020 "repoid" : "<reponame or repo_id>",
1021 1021 "fork_name": "<forkname>",
1022 1022 "owner": "<username or user_id = Optional(=apiuser)>",
1023 1023 "description": "<description>",
1024 1024 "copy_permissions": "<bool>",
1025 1025 "private": "<bool>",
1026 1026 "landing_rev": "<landing_rev>"
1027 1027 }
1028 1028
1029 1029 Example error output:
1030 1030
1031 1031 .. code-block:: bash
1032 1032
1033 1033 id : <id_given_in_input>
1034 1034 result: {
1035 1035 "msg": "Created fork of `<reponame>` as `<forkname>`",
1036 1036 "success": true,
1037 1037 "task": "<celery task id or None if done sync>"
1038 1038 }
1039 1039 error: null
1040 1040
1041 1041 """
1042 1042
1043 1043 repo = get_repo_or_error(repoid)
1044 1044 repo_name = repo.repo_name
1045 1045
1046 1046 if not has_superadmin_permission(apiuser):
1047 1047 # check if we have at least read permission for
1048 1048 # this repo that we fork !
1049 1049 _perms = (
1050 1050 'repository.admin', 'repository.write', 'repository.read')
1051 1051 validate_repo_permissions(apiuser, repoid, repo, _perms)
1052 1052
1053 1053 # check if the regular user has at least fork permissions as well
1054 1054 if not HasPermissionAnyApi('hg.fork.repository')(user=apiuser):
1055 1055 raise JSONRPCForbidden()
1056 1056
1057 1057 # check if user can set owner parameter
1058 1058 owner = validate_set_owner_permissions(apiuser, owner)
1059 1059
1060 1060 description = Optional.extract(description)
1061 1061 copy_permissions = Optional.extract(copy_permissions)
1062 1062 clone_uri = Optional.extract(clone_uri)
1063 1063 landing_commit_ref = Optional.extract(landing_rev)
1064 1064 private = Optional.extract(private)
1065 1065
1066 1066 schema = repo_schema.RepoSchema().bind(
1067 1067 repo_type_options=rhodecode.BACKENDS.keys(),
1068 1068 repo_type=repo.repo_type,
1069 1069 # user caller
1070 1070 user=apiuser)
1071 1071
1072 1072 try:
1073 1073 schema_data = schema.deserialize(dict(
1074 1074 repo_name=fork_name,
1075 1075 repo_type=repo.repo_type,
1076 1076 repo_owner=owner.username,
1077 1077 repo_description=description,
1078 1078 repo_landing_commit_ref=landing_commit_ref,
1079 1079 repo_clone_uri=clone_uri,
1080 1080 repo_private=private,
1081 1081 repo_copy_permissions=copy_permissions))
1082 1082 except validation_schema.Invalid as err:
1083 1083 raise JSONRPCValidationError(colander_exc=err)
1084 1084
1085 1085 try:
1086 1086 data = {
1087 1087 'fork_parent_id': repo.repo_id,
1088 1088
1089 1089 'repo_name': schema_data['repo_group']['repo_name_without_group'],
1090 1090 'repo_name_full': schema_data['repo_name'],
1091 1091 'repo_group': schema_data['repo_group']['repo_group_id'],
1092 1092 'repo_type': schema_data['repo_type'],
1093 1093 'description': schema_data['repo_description'],
1094 1094 'private': schema_data['repo_private'],
1095 1095 'copy_permissions': schema_data['repo_copy_permissions'],
1096 1096 'landing_rev': schema_data['repo_landing_commit_ref'],
1097 1097 }
1098 1098
1099 1099 task = RepoModel().create_fork(data, cur_user=owner.user_id)
1100 1100 # no commit, it's done in RepoModel, or async via celery
1101 1101 task_id = get_task_id(task)
1102 1102
1103 1103 return {
1104 1104 'msg': 'Created fork of `%s` as `%s`' % (
1105 1105 repo.repo_name, schema_data['repo_name']),
1106 1106 'success': True, # cannot return the repo data here since fork
1107 1107 # can be done async
1108 1108 'task': task_id
1109 1109 }
1110 1110 except Exception:
1111 1111 log.exception(
1112 1112 u"Exception while trying to create fork %s",
1113 1113 schema_data['repo_name'])
1114 1114 raise JSONRPCError(
1115 1115 'failed to fork repository `%s` as `%s`' % (
1116 1116 repo_name, schema_data['repo_name']))
1117 1117
1118 1118
1119 1119 @jsonrpc_method()
1120 1120 def delete_repo(request, apiuser, repoid, forks=Optional('')):
1121 1121 """
1122 1122 Deletes a repository.
1123 1123
1124 1124 * When the `forks` parameter is set it's possible to detach or delete
1125 1125 forks of deleted repository.
1126 1126
1127 1127 This command can only be run using an |authtoken| with admin
1128 1128 permissions on the |repo|.
1129 1129
1130 1130 :param apiuser: This is filled automatically from the |authtoken|.
1131 1131 :type apiuser: AuthUser
1132 1132 :param repoid: Set the repository name or repository ID.
1133 1133 :type repoid: str or int
1134 1134 :param forks: Set to `detach` or `delete` forks from the |repo|.
1135 1135 :type forks: Optional(str)
1136 1136
1137 1137 Example error output:
1138 1138
1139 1139 .. code-block:: bash
1140 1140
1141 1141 id : <id_given_in_input>
1142 1142 result: {
1143 1143 "msg": "Deleted repository `<reponame>`",
1144 1144 "success": true
1145 1145 }
1146 1146 error: null
1147 1147 """
1148 1148
1149 1149 repo = get_repo_or_error(repoid)
1150 1150 repo_name = repo.repo_name
1151 1151 if not has_superadmin_permission(apiuser):
1152 1152 _perms = ('repository.admin',)
1153 1153 validate_repo_permissions(apiuser, repoid, repo, _perms)
1154 1154
1155 1155 try:
1156 1156 handle_forks = Optional.extract(forks)
1157 1157 _forks_msg = ''
1158 1158 _forks = [f for f in repo.forks]
1159 1159 if handle_forks == 'detach':
1160 1160 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1161 1161 elif handle_forks == 'delete':
1162 1162 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1163 1163 elif _forks:
1164 1164 raise JSONRPCError(
1165 1165 'Cannot delete `%s` it still contains attached forks' %
1166 1166 (repo.repo_name,)
1167 1167 )
1168 1168 old_data = repo.get_api_data()
1169 1169 RepoModel().delete(repo, forks=forks)
1170 1170
1171 1171 repo = audit_logger.RepoWrap(repo_id=None,
1172 1172 repo_name=repo.repo_name)
1173 1173
1174 1174 audit_logger.store_api(
1175 1175 'repo.delete', action_data={'old_data': old_data},
1176 1176 user=apiuser, repo=repo)
1177 1177
1178 1178 ScmModel().mark_for_invalidation(repo_name, delete=True)
1179 1179 Session().commit()
1180 1180 return {
1181 1181 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1182 1182 'success': True
1183 1183 }
1184 1184 except Exception:
1185 1185 log.exception("Exception occurred while trying to delete repo")
1186 1186 raise JSONRPCError(
1187 1187 'failed to delete repository `%s`' % (repo_name,)
1188 1188 )
1189 1189
1190 1190
1191 1191 #TODO: marcink, change name ?
1192 1192 @jsonrpc_method()
1193 1193 def invalidate_cache(request, apiuser, repoid, delete_keys=Optional(False)):
1194 1194 """
1195 1195 Invalidates the cache for the specified repository.
1196 1196
1197 1197 This command can only be run using an |authtoken| with admin rights to
1198 1198 the specified repository.
1199 1199
1200 1200 This command takes the following options:
1201 1201
1202 1202 :param apiuser: This is filled automatically from |authtoken|.
1203 1203 :type apiuser: AuthUser
1204 1204 :param repoid: Sets the repository name or repository ID.
1205 1205 :type repoid: str or int
1206 1206 :param delete_keys: This deletes the invalidated keys instead of
1207 1207 just flagging them.
1208 1208 :type delete_keys: Optional(``True`` | ``False``)
1209 1209
1210 1210 Example output:
1211 1211
1212 1212 .. code-block:: bash
1213 1213
1214 1214 id : <id_given_in_input>
1215 1215 result : {
1216 1216 'msg': Cache for repository `<repository name>` was invalidated,
1217 1217 'repository': <repository name>
1218 1218 }
1219 1219 error : null
1220 1220
1221 1221 Example error output:
1222 1222
1223 1223 .. code-block:: bash
1224 1224
1225 1225 id : <id_given_in_input>
1226 1226 result : null
1227 1227 error : {
1228 1228 'Error occurred during cache invalidation action'
1229 1229 }
1230 1230
1231 1231 """
1232 1232
1233 1233 repo = get_repo_or_error(repoid)
1234 1234 if not has_superadmin_permission(apiuser):
1235 1235 _perms = ('repository.admin', 'repository.write',)
1236 1236 validate_repo_permissions(apiuser, repoid, repo, _perms)
1237 1237
1238 1238 delete = Optional.extract(delete_keys)
1239 1239 try:
1240 1240 ScmModel().mark_for_invalidation(repo.repo_name, delete=delete)
1241 1241 return {
1242 1242 'msg': 'Cache for repository `%s` was invalidated' % (repoid,),
1243 1243 'repository': repo.repo_name
1244 1244 }
1245 1245 except Exception:
1246 1246 log.exception(
1247 1247 "Exception occurred while trying to invalidate repo cache")
1248 1248 raise JSONRPCError(
1249 1249 'Error occurred during cache invalidation action'
1250 1250 )
1251 1251
1252 1252
1253 1253 #TODO: marcink, change name ?
1254 1254 @jsonrpc_method()
1255 1255 def lock(request, apiuser, repoid, locked=Optional(None),
1256 1256 userid=Optional(OAttr('apiuser'))):
1257 1257 """
1258 1258 Sets the lock state of the specified |repo| by the given user.
1259 1259 From more information, see :ref:`repo-locking`.
1260 1260
1261 1261 * If the ``userid`` option is not set, the repository is locked to the
1262 1262 user who called the method.
1263 1263 * If the ``locked`` parameter is not set, the current lock state of the
1264 1264 repository is displayed.
1265 1265
1266 1266 This command can only be run using an |authtoken| with admin rights to
1267 1267 the specified repository.
1268 1268
1269 1269 This command takes the following options:
1270 1270
1271 1271 :param apiuser: This is filled automatically from the |authtoken|.
1272 1272 :type apiuser: AuthUser
1273 1273 :param repoid: Sets the repository name or repository ID.
1274 1274 :type repoid: str or int
1275 1275 :param locked: Sets the lock state.
1276 1276 :type locked: Optional(``True`` | ``False``)
1277 1277 :param userid: Set the repository lock to this user.
1278 1278 :type userid: Optional(str or int)
1279 1279
1280 1280 Example error output:
1281 1281
1282 1282 .. code-block:: bash
1283 1283
1284 1284 id : <id_given_in_input>
1285 1285 result : {
1286 1286 'repo': '<reponame>',
1287 1287 'locked': <bool: lock state>,
1288 1288 'locked_since': <int: lock timestamp>,
1289 1289 'locked_by': <username of person who made the lock>,
1290 1290 'lock_reason': <str: reason for locking>,
1291 1291 'lock_state_changed': <bool: True if lock state has been changed in this request>,
1292 1292 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
1293 1293 or
1294 1294 'msg': 'Repo `<repository name>` not locked.'
1295 1295 or
1296 1296 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
1297 1297 }
1298 1298 error : null
1299 1299
1300 1300 Example error output:
1301 1301
1302 1302 .. code-block:: bash
1303 1303
1304 1304 id : <id_given_in_input>
1305 1305 result : null
1306 1306 error : {
1307 1307 'Error occurred locking repository `<reponame>`'
1308 1308 }
1309 1309 """
1310 1310
1311 1311 repo = get_repo_or_error(repoid)
1312 1312 if not has_superadmin_permission(apiuser):
1313 1313 # check if we have at least write permission for this repo !
1314 1314 _perms = ('repository.admin', 'repository.write',)
1315 1315 validate_repo_permissions(apiuser, repoid, repo, _perms)
1316 1316
1317 1317 # make sure normal user does not pass someone else userid,
1318 1318 # he is not allowed to do that
1319 1319 if not isinstance(userid, Optional) and userid != apiuser.user_id:
1320 1320 raise JSONRPCError('userid is not the same as your user')
1321 1321
1322 1322 if isinstance(userid, Optional):
1323 1323 userid = apiuser.user_id
1324 1324
1325 1325 user = get_user_or_error(userid)
1326 1326
1327 1327 if isinstance(locked, Optional):
1328 1328 lockobj = repo.locked
1329 1329
1330 1330 if lockobj[0] is None:
1331 1331 _d = {
1332 1332 'repo': repo.repo_name,
1333 1333 'locked': False,
1334 1334 'locked_since': None,
1335 1335 'locked_by': None,
1336 1336 'lock_reason': None,
1337 1337 'lock_state_changed': False,
1338 1338 'msg': 'Repo `%s` not locked.' % repo.repo_name
1339 1339 }
1340 1340 return _d
1341 1341 else:
1342 1342 _user_id, _time, _reason = lockobj
1343 1343 lock_user = get_user_or_error(userid)
1344 1344 _d = {
1345 1345 'repo': repo.repo_name,
1346 1346 'locked': True,
1347 1347 'locked_since': _time,
1348 1348 'locked_by': lock_user.username,
1349 1349 'lock_reason': _reason,
1350 1350 'lock_state_changed': False,
1351 1351 'msg': ('Repo `%s` locked by `%s` on `%s`.'
1352 1352 % (repo.repo_name, lock_user.username,
1353 1353 json.dumps(time_to_datetime(_time))))
1354 1354 }
1355 1355 return _d
1356 1356
1357 1357 # force locked state through a flag
1358 1358 else:
1359 1359 locked = str2bool(locked)
1360 1360 lock_reason = Repository.LOCK_API
1361 1361 try:
1362 1362 if locked:
1363 1363 lock_time = time.time()
1364 1364 Repository.lock(repo, user.user_id, lock_time, lock_reason)
1365 1365 else:
1366 1366 lock_time = None
1367 1367 Repository.unlock(repo)
1368 1368 _d = {
1369 1369 'repo': repo.repo_name,
1370 1370 'locked': locked,
1371 1371 'locked_since': lock_time,
1372 1372 'locked_by': user.username,
1373 1373 'lock_reason': lock_reason,
1374 1374 'lock_state_changed': True,
1375 1375 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
1376 1376 % (user.username, repo.repo_name, locked))
1377 1377 }
1378 1378 return _d
1379 1379 except Exception:
1380 1380 log.exception(
1381 1381 "Exception occurred while trying to lock repository")
1382 1382 raise JSONRPCError(
1383 1383 'Error occurred locking repository `%s`' % repo.repo_name
1384 1384 )
1385 1385
1386 1386
1387 1387 @jsonrpc_method()
1388 1388 def comment_commit(
1389 1389 request, apiuser, repoid, commit_id, message, status=Optional(None),
1390 1390 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
1391 1391 resolves_comment_id=Optional(None),
1392 1392 userid=Optional(OAttr('apiuser'))):
1393 1393 """
1394 1394 Set a commit comment, and optionally change the status of the commit.
1395 1395
1396 1396 :param apiuser: This is filled automatically from the |authtoken|.
1397 1397 :type apiuser: AuthUser
1398 1398 :param repoid: Set the repository name or repository ID.
1399 1399 :type repoid: str or int
1400 1400 :param commit_id: Specify the commit_id for which to set a comment.
1401 1401 :type commit_id: str
1402 1402 :param message: The comment text.
1403 1403 :type message: str
1404 1404 :param status: (**Optional**) status of commit, one of: 'not_reviewed',
1405 1405 'approved', 'rejected', 'under_review'
1406 1406 :type status: str
1407 1407 :param comment_type: Comment type, one of: 'note', 'todo'
1408 1408 :type comment_type: Optional(str), default: 'note'
1409 1409 :param userid: Set the user name of the comment creator.
1410 1410 :type userid: Optional(str or int)
1411 1411
1412 1412 Example error output:
1413 1413
1414 1414 .. code-block:: bash
1415 1415
1416 1416 {
1417 1417 "id" : <id_given_in_input>,
1418 1418 "result" : {
1419 1419 "msg": "Commented on commit `<commit_id>` for repository `<repoid>`",
1420 1420 "status_change": null or <status>,
1421 1421 "success": true
1422 1422 },
1423 1423 "error" : null
1424 1424 }
1425 1425
1426 1426 """
1427 1427 repo = get_repo_or_error(repoid)
1428 1428 if not has_superadmin_permission(apiuser):
1429 1429 _perms = ('repository.read', 'repository.write', 'repository.admin')
1430 1430 validate_repo_permissions(apiuser, repoid, repo, _perms)
1431 1431
1432 1432 try:
1433 1433 commit_id = repo.scm_instance().get_commit(commit_id=commit_id).raw_id
1434 1434 except Exception as e:
1435 1435 log.exception('Failed to fetch commit')
1436 1436 raise JSONRPCError(e.message)
1437 1437
1438 1438 if isinstance(userid, Optional):
1439 1439 userid = apiuser.user_id
1440 1440
1441 1441 user = get_user_or_error(userid)
1442 1442 status = Optional.extract(status)
1443 1443 comment_type = Optional.extract(comment_type)
1444 1444 resolves_comment_id = Optional.extract(resolves_comment_id)
1445 1445
1446 1446 allowed_statuses = [x[0] for x in ChangesetStatus.STATUSES]
1447 1447 if status and status not in allowed_statuses:
1448 1448 raise JSONRPCError('Bad status, must be on '
1449 1449 'of %s got %s' % (allowed_statuses, status,))
1450 1450
1451 1451 if resolves_comment_id:
1452 1452 comment = ChangesetComment.get(resolves_comment_id)
1453 1453 if not comment:
1454 1454 raise JSONRPCError(
1455 1455 'Invalid resolves_comment_id `%s` for this commit.'
1456 1456 % resolves_comment_id)
1457 1457 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
1458 1458 raise JSONRPCError(
1459 1459 'Comment `%s` is wrong type for setting status to resolved.'
1460 1460 % resolves_comment_id)
1461 1461
1462 1462 try:
1463 1463 rc_config = SettingsModel().get_all_settings()
1464 1464 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1465 1465 status_change_label = ChangesetStatus.get_status_lbl(status)
1466 1466 comment = CommentsModel().create(
1467 1467 message, repo, user, commit_id=commit_id,
1468 1468 status_change=status_change_label,
1469 1469 status_change_type=status,
1470 1470 renderer=renderer,
1471 1471 comment_type=comment_type,
1472 resolves_comment_id=resolves_comment_id
1472 resolves_comment_id=resolves_comment_id,
1473 auth_user=apiuser
1473 1474 )
1474 1475 if status:
1475 1476 # also do a status change
1476 1477 try:
1477 1478 ChangesetStatusModel().set_status(
1478 1479 repo, status, user, comment, revision=commit_id,
1479 1480 dont_allow_on_closed_pull_request=True
1480 1481 )
1481 1482 except StatusChangeOnClosedPullRequestError:
1482 1483 log.exception(
1483 1484 "Exception occurred while trying to change repo commit status")
1484 1485 msg = ('Changing status on a changeset associated with '
1485 1486 'a closed pull request is not allowed')
1486 1487 raise JSONRPCError(msg)
1487 1488
1488 1489 Session().commit()
1489 1490 return {
1490 1491 'msg': (
1491 1492 'Commented on commit `%s` for repository `%s`' % (
1492 1493 comment.revision, repo.repo_name)),
1493 1494 'status_change': status,
1494 1495 'success': True,
1495 1496 }
1496 1497 except JSONRPCError:
1497 1498 # catch any inside errors, and re-raise them to prevent from
1498 1499 # below global catch to silence them
1499 1500 raise
1500 1501 except Exception:
1501 1502 log.exception("Exception occurred while trying to comment on commit")
1502 1503 raise JSONRPCError(
1503 1504 'failed to set comment on repository `%s`' % (repo.repo_name,)
1504 1505 )
1505 1506
1506 1507
1507 1508 @jsonrpc_method()
1508 1509 def grant_user_permission(request, apiuser, repoid, userid, perm):
1509 1510 """
1510 1511 Grant permissions for the specified user on the given repository,
1511 1512 or update existing permissions if found.
1512 1513
1513 1514 This command can only be run using an |authtoken| with admin
1514 1515 permissions on the |repo|.
1515 1516
1516 1517 :param apiuser: This is filled automatically from the |authtoken|.
1517 1518 :type apiuser: AuthUser
1518 1519 :param repoid: Set the repository name or repository ID.
1519 1520 :type repoid: str or int
1520 1521 :param userid: Set the user name.
1521 1522 :type userid: str
1522 1523 :param perm: Set the user permissions, using the following format
1523 1524 ``(repository.(none|read|write|admin))``
1524 1525 :type perm: str
1525 1526
1526 1527 Example output:
1527 1528
1528 1529 .. code-block:: bash
1529 1530
1530 1531 id : <id_given_in_input>
1531 1532 result: {
1532 1533 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1533 1534 "success": true
1534 1535 }
1535 1536 error: null
1536 1537 """
1537 1538
1538 1539 repo = get_repo_or_error(repoid)
1539 1540 user = get_user_or_error(userid)
1540 1541 perm = get_perm_or_error(perm)
1541 1542 if not has_superadmin_permission(apiuser):
1542 1543 _perms = ('repository.admin',)
1543 1544 validate_repo_permissions(apiuser, repoid, repo, _perms)
1544 1545
1545 1546 try:
1546 1547
1547 1548 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1548 1549
1549 1550 Session().commit()
1550 1551 return {
1551 1552 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1552 1553 perm.permission_name, user.username, repo.repo_name
1553 1554 ),
1554 1555 'success': True
1555 1556 }
1556 1557 except Exception:
1557 1558 log.exception(
1558 1559 "Exception occurred while trying edit permissions for repo")
1559 1560 raise JSONRPCError(
1560 1561 'failed to edit permission for user: `%s` in repo: `%s`' % (
1561 1562 userid, repoid
1562 1563 )
1563 1564 )
1564 1565
1565 1566
1566 1567 @jsonrpc_method()
1567 1568 def revoke_user_permission(request, apiuser, repoid, userid):
1568 1569 """
1569 1570 Revoke permission for a user on the specified repository.
1570 1571
1571 1572 This command can only be run using an |authtoken| with admin
1572 1573 permissions on the |repo|.
1573 1574
1574 1575 :param apiuser: This is filled automatically from the |authtoken|.
1575 1576 :type apiuser: AuthUser
1576 1577 :param repoid: Set the repository name or repository ID.
1577 1578 :type repoid: str or int
1578 1579 :param userid: Set the user name of revoked user.
1579 1580 :type userid: str or int
1580 1581
1581 1582 Example error output:
1582 1583
1583 1584 .. code-block:: bash
1584 1585
1585 1586 id : <id_given_in_input>
1586 1587 result: {
1587 1588 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1588 1589 "success": true
1589 1590 }
1590 1591 error: null
1591 1592 """
1592 1593
1593 1594 repo = get_repo_or_error(repoid)
1594 1595 user = get_user_or_error(userid)
1595 1596 if not has_superadmin_permission(apiuser):
1596 1597 _perms = ('repository.admin',)
1597 1598 validate_repo_permissions(apiuser, repoid, repo, _perms)
1598 1599
1599 1600 try:
1600 1601 RepoModel().revoke_user_permission(repo=repo, user=user)
1601 1602 Session().commit()
1602 1603 return {
1603 1604 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1604 1605 user.username, repo.repo_name
1605 1606 ),
1606 1607 'success': True
1607 1608 }
1608 1609 except Exception:
1609 1610 log.exception(
1610 1611 "Exception occurred while trying revoke permissions to repo")
1611 1612 raise JSONRPCError(
1612 1613 'failed to edit permission for user: `%s` in repo: `%s`' % (
1613 1614 userid, repoid
1614 1615 )
1615 1616 )
1616 1617
1617 1618
1618 1619 @jsonrpc_method()
1619 1620 def grant_user_group_permission(request, apiuser, repoid, usergroupid, perm):
1620 1621 """
1621 1622 Grant permission for a user group on the specified repository,
1622 1623 or update existing permissions.
1623 1624
1624 1625 This command can only be run using an |authtoken| with admin
1625 1626 permissions on the |repo|.
1626 1627
1627 1628 :param apiuser: This is filled automatically from the |authtoken|.
1628 1629 :type apiuser: AuthUser
1629 1630 :param repoid: Set the repository name or repository ID.
1630 1631 :type repoid: str or int
1631 1632 :param usergroupid: Specify the ID of the user group.
1632 1633 :type usergroupid: str or int
1633 1634 :param perm: Set the user group permissions using the following
1634 1635 format: (repository.(none|read|write|admin))
1635 1636 :type perm: str
1636 1637
1637 1638 Example output:
1638 1639
1639 1640 .. code-block:: bash
1640 1641
1641 1642 id : <id_given_in_input>
1642 1643 result : {
1643 1644 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1644 1645 "success": true
1645 1646
1646 1647 }
1647 1648 error : null
1648 1649
1649 1650 Example error output:
1650 1651
1651 1652 .. code-block:: bash
1652 1653
1653 1654 id : <id_given_in_input>
1654 1655 result : null
1655 1656 error : {
1656 1657 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1657 1658 }
1658 1659
1659 1660 """
1660 1661
1661 1662 repo = get_repo_or_error(repoid)
1662 1663 perm = get_perm_or_error(perm)
1663 1664 if not has_superadmin_permission(apiuser):
1664 1665 _perms = ('repository.admin',)
1665 1666 validate_repo_permissions(apiuser, repoid, repo, _perms)
1666 1667
1667 1668 user_group = get_user_group_or_error(usergroupid)
1668 1669 if not has_superadmin_permission(apiuser):
1669 1670 # check if we have at least read permission for this user group !
1670 1671 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1671 1672 if not HasUserGroupPermissionAnyApi(*_perms)(
1672 1673 user=apiuser, user_group_name=user_group.users_group_name):
1673 1674 raise JSONRPCError(
1674 1675 'user group `%s` does not exist' % (usergroupid,))
1675 1676
1676 1677 try:
1677 1678 RepoModel().grant_user_group_permission(
1678 1679 repo=repo, group_name=user_group, perm=perm)
1679 1680
1680 1681 Session().commit()
1681 1682 return {
1682 1683 'msg': 'Granted perm: `%s` for user group: `%s` in '
1683 1684 'repo: `%s`' % (
1684 1685 perm.permission_name, user_group.users_group_name,
1685 1686 repo.repo_name
1686 1687 ),
1687 1688 'success': True
1688 1689 }
1689 1690 except Exception:
1690 1691 log.exception(
1691 1692 "Exception occurred while trying change permission on repo")
1692 1693 raise JSONRPCError(
1693 1694 'failed to edit permission for user group: `%s` in '
1694 1695 'repo: `%s`' % (
1695 1696 usergroupid, repo.repo_name
1696 1697 )
1697 1698 )
1698 1699
1699 1700
1700 1701 @jsonrpc_method()
1701 1702 def revoke_user_group_permission(request, apiuser, repoid, usergroupid):
1702 1703 """
1703 1704 Revoke the permissions of a user group on a given repository.
1704 1705
1705 1706 This command can only be run using an |authtoken| with admin
1706 1707 permissions on the |repo|.
1707 1708
1708 1709 :param apiuser: This is filled automatically from the |authtoken|.
1709 1710 :type apiuser: AuthUser
1710 1711 :param repoid: Set the repository name or repository ID.
1711 1712 :type repoid: str or int
1712 1713 :param usergroupid: Specify the user group ID.
1713 1714 :type usergroupid: str or int
1714 1715
1715 1716 Example output:
1716 1717
1717 1718 .. code-block:: bash
1718 1719
1719 1720 id : <id_given_in_input>
1720 1721 result: {
1721 1722 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1722 1723 "success": true
1723 1724 }
1724 1725 error: null
1725 1726 """
1726 1727
1727 1728 repo = get_repo_or_error(repoid)
1728 1729 if not has_superadmin_permission(apiuser):
1729 1730 _perms = ('repository.admin',)
1730 1731 validate_repo_permissions(apiuser, repoid, repo, _perms)
1731 1732
1732 1733 user_group = get_user_group_or_error(usergroupid)
1733 1734 if not has_superadmin_permission(apiuser):
1734 1735 # check if we have at least read permission for this user group !
1735 1736 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1736 1737 if not HasUserGroupPermissionAnyApi(*_perms)(
1737 1738 user=apiuser, user_group_name=user_group.users_group_name):
1738 1739 raise JSONRPCError(
1739 1740 'user group `%s` does not exist' % (usergroupid,))
1740 1741
1741 1742 try:
1742 1743 RepoModel().revoke_user_group_permission(
1743 1744 repo=repo, group_name=user_group)
1744 1745
1745 1746 Session().commit()
1746 1747 return {
1747 1748 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1748 1749 user_group.users_group_name, repo.repo_name
1749 1750 ),
1750 1751 'success': True
1751 1752 }
1752 1753 except Exception:
1753 1754 log.exception("Exception occurred while trying revoke "
1754 1755 "user group permission on repo")
1755 1756 raise JSONRPCError(
1756 1757 'failed to edit permission for user group: `%s` in '
1757 1758 'repo: `%s`' % (
1758 1759 user_group.users_group_name, repo.repo_name
1759 1760 )
1760 1761 )
1761 1762
1762 1763
1763 1764 @jsonrpc_method()
1764 1765 def pull(request, apiuser, repoid, remote_uri=Optional(None)):
1765 1766 """
1766 1767 Triggers a pull on the given repository from a remote location. You
1767 1768 can use this to keep remote repositories up-to-date.
1768 1769
1769 1770 This command can only be run using an |authtoken| with admin
1770 1771 rights to the specified repository. For more information,
1771 1772 see :ref:`config-token-ref`.
1772 1773
1773 1774 This command takes the following options:
1774 1775
1775 1776 :param apiuser: This is filled automatically from the |authtoken|.
1776 1777 :type apiuser: AuthUser
1777 1778 :param repoid: The repository name or repository ID.
1778 1779 :type repoid: str or int
1779 1780 :param remote_uri: Optional remote URI to pass in for pull
1780 1781 :type remote_uri: str
1781 1782
1782 1783 Example output:
1783 1784
1784 1785 .. code-block:: bash
1785 1786
1786 1787 id : <id_given_in_input>
1787 1788 result : {
1788 1789 "msg": "Pulled from url `<remote_url>` on repo `<repository name>`"
1789 1790 "repository": "<repository name>"
1790 1791 }
1791 1792 error : null
1792 1793
1793 1794 Example error output:
1794 1795
1795 1796 .. code-block:: bash
1796 1797
1797 1798 id : <id_given_in_input>
1798 1799 result : null
1799 1800 error : {
1800 1801 "Unable to push changes from `<remote_url>`"
1801 1802 }
1802 1803
1803 1804 """
1804 1805
1805 1806 repo = get_repo_or_error(repoid)
1806 1807 remote_uri = Optional.extract(remote_uri)
1807 1808 remote_uri_display = remote_uri or repo.clone_uri_hidden
1808 1809 if not has_superadmin_permission(apiuser):
1809 1810 _perms = ('repository.admin',)
1810 1811 validate_repo_permissions(apiuser, repoid, repo, _perms)
1811 1812
1812 1813 try:
1813 1814 ScmModel().pull_changes(
1814 1815 repo.repo_name, apiuser.username, remote_uri=remote_uri)
1815 1816 return {
1816 1817 'msg': 'Pulled from url `%s` on repo `%s`' % (
1817 1818 remote_uri_display, repo.repo_name),
1818 1819 'repository': repo.repo_name
1819 1820 }
1820 1821 except Exception:
1821 1822 log.exception("Exception occurred while trying to "
1822 1823 "pull changes from remote location")
1823 1824 raise JSONRPCError(
1824 1825 'Unable to pull changes from `%s`' % remote_uri_display
1825 1826 )
1826 1827
1827 1828
1828 1829 @jsonrpc_method()
1829 1830 def strip(request, apiuser, repoid, revision, branch):
1830 1831 """
1831 1832 Strips the given revision from the specified repository.
1832 1833
1833 1834 * This will remove the revision and all of its decendants.
1834 1835
1835 1836 This command can only be run using an |authtoken| with admin rights to
1836 1837 the specified repository.
1837 1838
1838 1839 This command takes the following options:
1839 1840
1840 1841 :param apiuser: This is filled automatically from the |authtoken|.
1841 1842 :type apiuser: AuthUser
1842 1843 :param repoid: The repository name or repository ID.
1843 1844 :type repoid: str or int
1844 1845 :param revision: The revision you wish to strip.
1845 1846 :type revision: str
1846 1847 :param branch: The branch from which to strip the revision.
1847 1848 :type branch: str
1848 1849
1849 1850 Example output:
1850 1851
1851 1852 .. code-block:: bash
1852 1853
1853 1854 id : <id_given_in_input>
1854 1855 result : {
1855 1856 "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'"
1856 1857 "repository": "<repository name>"
1857 1858 }
1858 1859 error : null
1859 1860
1860 1861 Example error output:
1861 1862
1862 1863 .. code-block:: bash
1863 1864
1864 1865 id : <id_given_in_input>
1865 1866 result : null
1866 1867 error : {
1867 1868 "Unable to strip commit <commit_hash> from repo `<repository name>`"
1868 1869 }
1869 1870
1870 1871 """
1871 1872
1872 1873 repo = get_repo_or_error(repoid)
1873 1874 if not has_superadmin_permission(apiuser):
1874 1875 _perms = ('repository.admin',)
1875 1876 validate_repo_permissions(apiuser, repoid, repo, _perms)
1876 1877
1877 1878 try:
1878 1879 ScmModel().strip(repo, revision, branch)
1879 1880 audit_logger.store_api(
1880 1881 'repo.commit.strip', action_data={'commit_id': revision},
1881 1882 repo=repo,
1882 1883 user=apiuser, commit=True)
1883 1884
1884 1885 return {
1885 1886 'msg': 'Stripped commit %s from repo `%s`' % (
1886 1887 revision, repo.repo_name),
1887 1888 'repository': repo.repo_name
1888 1889 }
1889 1890 except Exception:
1890 1891 log.exception("Exception while trying to strip")
1891 1892 raise JSONRPCError(
1892 1893 'Unable to strip commit %s from repo `%s`' % (
1893 1894 revision, repo.repo_name)
1894 1895 )
1895 1896
1896 1897
1897 1898 @jsonrpc_method()
1898 1899 def get_repo_settings(request, apiuser, repoid, key=Optional(None)):
1899 1900 """
1900 1901 Returns all settings for a repository. If key is given it only returns the
1901 1902 setting identified by the key or null.
1902 1903
1903 1904 :param apiuser: This is filled automatically from the |authtoken|.
1904 1905 :type apiuser: AuthUser
1905 1906 :param repoid: The repository name or repository id.
1906 1907 :type repoid: str or int
1907 1908 :param key: Key of the setting to return.
1908 1909 :type: key: Optional(str)
1909 1910
1910 1911 Example output:
1911 1912
1912 1913 .. code-block:: bash
1913 1914
1914 1915 {
1915 1916 "error": null,
1916 1917 "id": 237,
1917 1918 "result": {
1918 1919 "extensions_largefiles": true,
1919 1920 "extensions_evolve": true,
1920 1921 "hooks_changegroup_push_logger": true,
1921 1922 "hooks_changegroup_repo_size": false,
1922 1923 "hooks_outgoing_pull_logger": true,
1923 1924 "phases_publish": "True",
1924 1925 "rhodecode_hg_use_rebase_for_merging": true,
1925 1926 "rhodecode_pr_merge_enabled": true,
1926 1927 "rhodecode_use_outdated_comments": true
1927 1928 }
1928 1929 }
1929 1930 """
1930 1931
1931 1932 # Restrict access to this api method to admins only.
1932 1933 if not has_superadmin_permission(apiuser):
1933 1934 raise JSONRPCForbidden()
1934 1935
1935 1936 try:
1936 1937 repo = get_repo_or_error(repoid)
1937 1938 settings_model = VcsSettingsModel(repo=repo)
1938 1939 settings = settings_model.get_global_settings()
1939 1940 settings.update(settings_model.get_repo_settings())
1940 1941
1941 1942 # If only a single setting is requested fetch it from all settings.
1942 1943 key = Optional.extract(key)
1943 1944 if key is not None:
1944 1945 settings = settings.get(key, None)
1945 1946 except Exception:
1946 1947 msg = 'Failed to fetch settings for repository `{}`'.format(repoid)
1947 1948 log.exception(msg)
1948 1949 raise JSONRPCError(msg)
1949 1950
1950 1951 return settings
1951 1952
1952 1953
1953 1954 @jsonrpc_method()
1954 1955 def set_repo_settings(request, apiuser, repoid, settings):
1955 1956 """
1956 1957 Update repository settings. Returns true on success.
1957 1958
1958 1959 :param apiuser: This is filled automatically from the |authtoken|.
1959 1960 :type apiuser: AuthUser
1960 1961 :param repoid: The repository name or repository id.
1961 1962 :type repoid: str or int
1962 1963 :param settings: The new settings for the repository.
1963 1964 :type: settings: dict
1964 1965
1965 1966 Example output:
1966 1967
1967 1968 .. code-block:: bash
1968 1969
1969 1970 {
1970 1971 "error": null,
1971 1972 "id": 237,
1972 1973 "result": true
1973 1974 }
1974 1975 """
1975 1976 # Restrict access to this api method to admins only.
1976 1977 if not has_superadmin_permission(apiuser):
1977 1978 raise JSONRPCForbidden()
1978 1979
1979 1980 if type(settings) is not dict:
1980 1981 raise JSONRPCError('Settings have to be a JSON Object.')
1981 1982
1982 1983 try:
1983 1984 settings_model = VcsSettingsModel(repo=repoid)
1984 1985
1985 1986 # Merge global, repo and incoming settings.
1986 1987 new_settings = settings_model.get_global_settings()
1987 1988 new_settings.update(settings_model.get_repo_settings())
1988 1989 new_settings.update(settings)
1989 1990
1990 1991 # Update the settings.
1991 1992 inherit_global_settings = new_settings.get(
1992 1993 'inherit_global_settings', False)
1993 1994 settings_model.create_or_update_repo_settings(
1994 1995 new_settings, inherit_global_settings=inherit_global_settings)
1995 1996 Session().commit()
1996 1997 except Exception:
1997 1998 msg = 'Failed to update settings for repository `{}`'.format(repoid)
1998 1999 log.exception(msg)
1999 2000 raise JSONRPCError(msg)
2000 2001
2001 2002 # Indicate success.
2002 2003 return True
2003 2004
2004 2005
2005 2006 @jsonrpc_method()
2006 2007 def maintenance(request, apiuser, repoid):
2007 2008 """
2008 2009 Triggers a maintenance on the given repository.
2009 2010
2010 2011 This command can only be run using an |authtoken| with admin
2011 2012 rights to the specified repository. For more information,
2012 2013 see :ref:`config-token-ref`.
2013 2014
2014 2015 This command takes the following options:
2015 2016
2016 2017 :param apiuser: This is filled automatically from the |authtoken|.
2017 2018 :type apiuser: AuthUser
2018 2019 :param repoid: The repository name or repository ID.
2019 2020 :type repoid: str or int
2020 2021
2021 2022 Example output:
2022 2023
2023 2024 .. code-block:: bash
2024 2025
2025 2026 id : <id_given_in_input>
2026 2027 result : {
2027 2028 "msg": "executed maintenance command",
2028 2029 "executed_actions": [
2029 2030 <action_message>, <action_message2>...
2030 2031 ],
2031 2032 "repository": "<repository name>"
2032 2033 }
2033 2034 error : null
2034 2035
2035 2036 Example error output:
2036 2037
2037 2038 .. code-block:: bash
2038 2039
2039 2040 id : <id_given_in_input>
2040 2041 result : null
2041 2042 error : {
2042 2043 "Unable to execute maintenance on `<reponame>`"
2043 2044 }
2044 2045
2045 2046 """
2046 2047
2047 2048 repo = get_repo_or_error(repoid)
2048 2049 if not has_superadmin_permission(apiuser):
2049 2050 _perms = ('repository.admin',)
2050 2051 validate_repo_permissions(apiuser, repoid, repo, _perms)
2051 2052
2052 2053 try:
2053 2054 maintenance = repo_maintenance.RepoMaintenance()
2054 2055 executed_actions = maintenance.execute(repo)
2055 2056
2056 2057 return {
2057 2058 'msg': 'executed maintenance command',
2058 2059 'executed_actions': executed_actions,
2059 2060 'repository': repo.repo_name
2060 2061 }
2061 2062 except Exception:
2062 2063 log.exception("Exception occurred while trying to run maintenance")
2063 2064 raise JSONRPCError(
2064 2065 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -1,589 +1,590 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import RepoAppView
31 31
32 32 from rhodecode.lib import diffs, codeblocks
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 35
36 36 from rhodecode.lib.compat import OrderedDict
37 37 from rhodecode.lib.diffs import cache_diff, load_cached_diff, diff_cache_exist
38 38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
39 39 import rhodecode.lib.helpers as h
40 40 from rhodecode.lib.utils2 import safe_unicode, str2bool
41 41 from rhodecode.lib.vcs.backends.base import EmptyCommit
42 42 from rhodecode.lib.vcs.exceptions import (
43 43 RepositoryError, CommitDoesNotExistError)
44 44 from rhodecode.model.db import ChangesetComment, ChangesetStatus
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.settings import VcsSettingsModel
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 def _update_with_GET(params, request):
54 54 for k in ['diff1', 'diff2', 'diff']:
55 55 params[k] += request.GET.getall(k)
56 56
57 57
58 58 def get_ignore_ws(fid, request):
59 59 ig_ws_global = request.GET.get('ignorews')
60 60 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
61 61 if ig_ws:
62 62 try:
63 63 return int(ig_ws[0].split(':')[-1])
64 64 except Exception:
65 65 pass
66 66 return ig_ws_global
67 67
68 68
69 69 def _ignorews_url(request, fileid=None):
70 70 _ = request.translate
71 71 fileid = str(fileid) if fileid else None
72 72 params = collections.defaultdict(list)
73 73 _update_with_GET(params, request)
74 74 label = _('Show whitespace')
75 75 tooltiplbl = _('Show whitespace for all diffs')
76 76 ig_ws = get_ignore_ws(fileid, request)
77 77 ln_ctx = get_line_ctx(fileid, request)
78 78
79 79 if ig_ws is None:
80 80 params['ignorews'] += [1]
81 81 label = _('Ignore whitespace')
82 82 tooltiplbl = _('Ignore whitespace for all diffs')
83 83 ctx_key = 'context'
84 84 ctx_val = ln_ctx
85 85
86 86 # if we have passed in ln_ctx pass it along to our params
87 87 if ln_ctx:
88 88 params[ctx_key] += [ctx_val]
89 89
90 90 if fileid:
91 91 params['anchor'] = 'a_' + fileid
92 92 return h.link_to(label, request.current_route_path(_query=params),
93 93 title=tooltiplbl, class_='tooltip')
94 94
95 95
96 96 def get_line_ctx(fid, request):
97 97 ln_ctx_global = request.GET.get('context')
98 98 if fid:
99 99 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
100 100 else:
101 101 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
102 102 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
103 103 if ln_ctx:
104 104 ln_ctx = [ln_ctx]
105 105
106 106 if ln_ctx:
107 107 retval = ln_ctx[0].split(':')[-1]
108 108 else:
109 109 retval = ln_ctx_global
110 110
111 111 try:
112 112 return int(retval)
113 113 except Exception:
114 114 return 3
115 115
116 116
117 117 def _context_url(request, fileid=None):
118 118 """
119 119 Generates a url for context lines.
120 120
121 121 :param fileid:
122 122 """
123 123
124 124 _ = request.translate
125 125 fileid = str(fileid) if fileid else None
126 126 ig_ws = get_ignore_ws(fileid, request)
127 127 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
128 128
129 129 params = collections.defaultdict(list)
130 130 _update_with_GET(params, request)
131 131
132 132 if ln_ctx > 0:
133 133 params['context'] += [ln_ctx]
134 134
135 135 if ig_ws:
136 136 ig_ws_key = 'ignorews'
137 137 ig_ws_val = 1
138 138 params[ig_ws_key] += [ig_ws_val]
139 139
140 140 lbl = _('Increase context')
141 141 tooltiplbl = _('Increase context for all diffs')
142 142
143 143 if fileid:
144 144 params['anchor'] = 'a_' + fileid
145 145 return h.link_to(lbl, request.current_route_path(_query=params),
146 146 title=tooltiplbl, class_='tooltip')
147 147
148 148
149 149 class RepoCommitsView(RepoAppView):
150 150 def load_default_context(self):
151 151 c = self._get_local_tmpl_context(include_app_defaults=True)
152 152 c.rhodecode_repo = self.rhodecode_vcs_repo
153 153
154 154 return c
155 155
156 156 def _is_diff_cache_enabled(self, target_repo):
157 157 caching_enabled = self._get_general_setting(
158 158 target_repo, 'rhodecode_diff_cache')
159 159 log.debug('Diff caching enabled: %s', caching_enabled)
160 160 return caching_enabled
161 161
162 162 def _commit(self, commit_id_range, method):
163 163 _ = self.request.translate
164 164 c = self.load_default_context()
165 165 c.ignorews_url = _ignorews_url
166 166 c.context_url = _context_url
167 167 c.fulldiff = self.request.GET.get('fulldiff')
168 168
169 169 # fetch global flags of ignore ws or context lines
170 170 context_lcl = get_line_ctx('', self.request)
171 171 ign_whitespace_lcl = get_ignore_ws('', self.request)
172 172
173 173 # diff_limit will cut off the whole diff if the limit is applied
174 174 # otherwise it will just hide the big files from the front-end
175 175 diff_limit = c.visual.cut_off_limit_diff
176 176 file_limit = c.visual.cut_off_limit_file
177 177
178 178 # get ranges of commit ids if preset
179 179 commit_range = commit_id_range.split('...')[:2]
180 180
181 181 try:
182 182 pre_load = ['affected_files', 'author', 'branch', 'date',
183 183 'message', 'parents']
184 184
185 185 if len(commit_range) == 2:
186 186 commits = self.rhodecode_vcs_repo.get_commits(
187 187 start_id=commit_range[0], end_id=commit_range[1],
188 188 pre_load=pre_load)
189 189 commits = list(commits)
190 190 else:
191 191 commits = [self.rhodecode_vcs_repo.get_commit(
192 192 commit_id=commit_id_range, pre_load=pre_load)]
193 193
194 194 c.commit_ranges = commits
195 195 if not c.commit_ranges:
196 196 raise RepositoryError(
197 197 'The commit range returned an empty result')
198 198 except CommitDoesNotExistError:
199 199 msg = _('No such commit exists for this repository')
200 200 h.flash(msg, category='error')
201 201 raise HTTPNotFound()
202 202 except Exception:
203 203 log.exception("General failure")
204 204 raise HTTPNotFound()
205 205
206 206 c.changes = OrderedDict()
207 207 c.lines_added = 0
208 208 c.lines_deleted = 0
209 209
210 210 # auto collapse if we have more than limit
211 211 collapse_limit = diffs.DiffProcessor._collapse_commits_over
212 212 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
213 213
214 214 c.commit_statuses = ChangesetStatus.STATUSES
215 215 c.inline_comments = []
216 216 c.files = []
217 217
218 218 c.statuses = []
219 219 c.comments = []
220 220 c.unresolved_comments = []
221 221 if len(c.commit_ranges) == 1:
222 222 commit = c.commit_ranges[0]
223 223 c.comments = CommentsModel().get_comments(
224 224 self.db_repo.repo_id,
225 225 revision=commit.raw_id)
226 226 c.statuses.append(ChangesetStatusModel().get_status(
227 227 self.db_repo.repo_id, commit.raw_id))
228 228 # comments from PR
229 229 statuses = ChangesetStatusModel().get_statuses(
230 230 self.db_repo.repo_id, commit.raw_id,
231 231 with_revisions=True)
232 232 prs = set(st.pull_request for st in statuses
233 233 if st.pull_request is not None)
234 234 # from associated statuses, check the pull requests, and
235 235 # show comments from them
236 236 for pr in prs:
237 237 c.comments.extend(pr.comments)
238 238
239 239 c.unresolved_comments = CommentsModel()\
240 240 .get_commit_unresolved_todos(commit.raw_id)
241 241
242 242 diff = None
243 243 # Iterate over ranges (default commit view is always one commit)
244 244 for commit in c.commit_ranges:
245 245 c.changes[commit.raw_id] = []
246 246
247 247 commit2 = commit
248 248 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
249 249
250 250 if method == 'show':
251 251 inline_comments = CommentsModel().get_inline_comments(
252 252 self.db_repo.repo_id, revision=commit.raw_id)
253 253 c.inline_cnt = CommentsModel().get_inline_comments_count(
254 254 inline_comments)
255 255 c.inline_comments = inline_comments
256 256
257 257 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
258 258 self.db_repo)
259 259 cache_file_path = diff_cache_exist(
260 260 cache_path, 'diff', commit.raw_id,
261 261 ign_whitespace_lcl, context_lcl, c.fulldiff)
262 262
263 263 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
264 264 force_recache = str2bool(self.request.GET.get('force_recache'))
265 265
266 266 cached_diff = None
267 267 if caching_enabled:
268 268 cached_diff = load_cached_diff(cache_file_path)
269 269
270 270 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
271 271 if not force_recache and has_proper_diff_cache:
272 272 diffset = cached_diff['diff']
273 273 else:
274 274 vcs_diff = self.rhodecode_vcs_repo.get_diff(
275 275 commit1, commit2,
276 276 ignore_whitespace=ign_whitespace_lcl,
277 277 context=context_lcl)
278 278
279 279 diff_processor = diffs.DiffProcessor(
280 280 vcs_diff, format='newdiff', diff_limit=diff_limit,
281 281 file_limit=file_limit, show_full_diff=c.fulldiff)
282 282
283 283 _parsed = diff_processor.prepare()
284 284
285 285 diffset = codeblocks.DiffSet(
286 286 repo_name=self.db_repo_name,
287 287 source_node_getter=codeblocks.diffset_node_getter(commit1),
288 288 target_node_getter=codeblocks.diffset_node_getter(commit2))
289 289
290 290 diffset = self.path_filter.render_patchset_filtered(
291 291 diffset, _parsed, commit1.raw_id, commit2.raw_id)
292 292
293 293 # save cached diff
294 294 if caching_enabled:
295 295 cache_diff(cache_file_path, diffset, None)
296 296
297 297 c.limited_diff = diffset.limited_diff
298 298 c.changes[commit.raw_id] = diffset
299 299 else:
300 300 # TODO(marcink): no cache usage here...
301 301 _diff = self.rhodecode_vcs_repo.get_diff(
302 302 commit1, commit2,
303 303 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
304 304 diff_processor = diffs.DiffProcessor(
305 305 _diff, format='newdiff', diff_limit=diff_limit,
306 306 file_limit=file_limit, show_full_diff=c.fulldiff)
307 307 # downloads/raw we only need RAW diff nothing else
308 308 diff = self.path_filter.get_raw_patch(diff_processor)
309 309 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
310 310
311 311 # sort comments by how they were generated
312 312 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
313 313
314 314 if len(c.commit_ranges) == 1:
315 315 c.commit = c.commit_ranges[0]
316 316 c.parent_tmpl = ''.join(
317 317 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
318 318
319 319 if method == 'download':
320 320 response = Response(diff)
321 321 response.content_type = 'text/plain'
322 322 response.content_disposition = (
323 323 'attachment; filename=%s.diff' % commit_id_range[:12])
324 324 return response
325 325 elif method == 'patch':
326 326 c.diff = safe_unicode(diff)
327 327 patch = render(
328 328 'rhodecode:templates/changeset/patch_changeset.mako',
329 329 self._get_template_context(c), self.request)
330 330 response = Response(patch)
331 331 response.content_type = 'text/plain'
332 332 return response
333 333 elif method == 'raw':
334 334 response = Response(diff)
335 335 response.content_type = 'text/plain'
336 336 return response
337 337 elif method == 'show':
338 338 if len(c.commit_ranges) == 1:
339 339 html = render(
340 340 'rhodecode:templates/changeset/changeset.mako',
341 341 self._get_template_context(c), self.request)
342 342 return Response(html)
343 343 else:
344 344 c.ancestor = None
345 345 c.target_repo = self.db_repo
346 346 html = render(
347 347 'rhodecode:templates/changeset/changeset_range.mako',
348 348 self._get_template_context(c), self.request)
349 349 return Response(html)
350 350
351 351 raise HTTPBadRequest()
352 352
353 353 @LoginRequired()
354 354 @HasRepoPermissionAnyDecorator(
355 355 'repository.read', 'repository.write', 'repository.admin')
356 356 @view_config(
357 357 route_name='repo_commit', request_method='GET',
358 358 renderer=None)
359 359 def repo_commit_show(self):
360 360 commit_id = self.request.matchdict['commit_id']
361 361 return self._commit(commit_id, method='show')
362 362
363 363 @LoginRequired()
364 364 @HasRepoPermissionAnyDecorator(
365 365 'repository.read', 'repository.write', 'repository.admin')
366 366 @view_config(
367 367 route_name='repo_commit_raw', request_method='GET',
368 368 renderer=None)
369 369 @view_config(
370 370 route_name='repo_commit_raw_deprecated', request_method='GET',
371 371 renderer=None)
372 372 def repo_commit_raw(self):
373 373 commit_id = self.request.matchdict['commit_id']
374 374 return self._commit(commit_id, method='raw')
375 375
376 376 @LoginRequired()
377 377 @HasRepoPermissionAnyDecorator(
378 378 'repository.read', 'repository.write', 'repository.admin')
379 379 @view_config(
380 380 route_name='repo_commit_patch', request_method='GET',
381 381 renderer=None)
382 382 def repo_commit_patch(self):
383 383 commit_id = self.request.matchdict['commit_id']
384 384 return self._commit(commit_id, method='patch')
385 385
386 386 @LoginRequired()
387 387 @HasRepoPermissionAnyDecorator(
388 388 'repository.read', 'repository.write', 'repository.admin')
389 389 @view_config(
390 390 route_name='repo_commit_download', request_method='GET',
391 391 renderer=None)
392 392 def repo_commit_download(self):
393 393 commit_id = self.request.matchdict['commit_id']
394 394 return self._commit(commit_id, method='download')
395 395
396 396 @LoginRequired()
397 397 @NotAnonymous()
398 398 @HasRepoPermissionAnyDecorator(
399 399 'repository.read', 'repository.write', 'repository.admin')
400 400 @CSRFRequired()
401 401 @view_config(
402 402 route_name='repo_commit_comment_create', request_method='POST',
403 403 renderer='json_ext')
404 404 def repo_commit_comment_create(self):
405 405 _ = self.request.translate
406 406 commit_id = self.request.matchdict['commit_id']
407 407
408 408 c = self.load_default_context()
409 409 status = self.request.POST.get('changeset_status', None)
410 410 text = self.request.POST.get('text')
411 411 comment_type = self.request.POST.get('comment_type')
412 412 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
413 413
414 414 if status:
415 415 text = text or (_('Status change %(transition_icon)s %(status)s')
416 416 % {'transition_icon': '>',
417 417 'status': ChangesetStatus.get_status_lbl(status)})
418 418
419 419 multi_commit_ids = []
420 420 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
421 421 if _commit_id not in ['', None, EmptyCommit.raw_id]:
422 422 if _commit_id not in multi_commit_ids:
423 423 multi_commit_ids.append(_commit_id)
424 424
425 425 commit_ids = multi_commit_ids or [commit_id]
426 426
427 427 comment = None
428 428 for current_id in filter(None, commit_ids):
429 429 comment = CommentsModel().create(
430 430 text=text,
431 431 repo=self.db_repo.repo_id,
432 432 user=self._rhodecode_db_user.user_id,
433 433 commit_id=current_id,
434 434 f_path=self.request.POST.get('f_path'),
435 435 line_no=self.request.POST.get('line'),
436 436 status_change=(ChangesetStatus.get_status_lbl(status)
437 437 if status else None),
438 438 status_change_type=status,
439 439 comment_type=comment_type,
440 resolves_comment_id=resolves_comment_id
440 resolves_comment_id=resolves_comment_id,
441 auth_user=self._rhodecode_user
441 442 )
442 443
443 444 # get status if set !
444 445 if status:
445 446 # if latest status was from pull request and it's closed
446 447 # disallow changing status !
447 448 # dont_allow_on_closed_pull_request = True !
448 449
449 450 try:
450 451 ChangesetStatusModel().set_status(
451 452 self.db_repo.repo_id,
452 453 status,
453 454 self._rhodecode_db_user.user_id,
454 455 comment,
455 456 revision=current_id,
456 457 dont_allow_on_closed_pull_request=True
457 458 )
458 459 except StatusChangeOnClosedPullRequestError:
459 460 msg = _('Changing the status of a commit associated with '
460 461 'a closed pull request is not allowed')
461 462 log.exception(msg)
462 463 h.flash(msg, category='warning')
463 464 raise HTTPFound(h.route_path(
464 465 'repo_commit', repo_name=self.db_repo_name,
465 466 commit_id=current_id))
466 467
467 468 # finalize, commit and redirect
468 469 Session().commit()
469 470
470 471 data = {
471 472 'target_id': h.safeid(h.safe_unicode(
472 473 self.request.POST.get('f_path'))),
473 474 }
474 475 if comment:
475 476 c.co = comment
476 477 rendered_comment = render(
477 478 'rhodecode:templates/changeset/changeset_comment_block.mako',
478 479 self._get_template_context(c), self.request)
479 480
480 481 data.update(comment.get_dict())
481 482 data.update({'rendered_text': rendered_comment})
482 483
483 484 return data
484 485
485 486 @LoginRequired()
486 487 @NotAnonymous()
487 488 @HasRepoPermissionAnyDecorator(
488 489 'repository.read', 'repository.write', 'repository.admin')
489 490 @CSRFRequired()
490 491 @view_config(
491 492 route_name='repo_commit_comment_preview', request_method='POST',
492 493 renderer='string', xhr=True)
493 494 def repo_commit_comment_preview(self):
494 495 # Technically a CSRF token is not needed as no state changes with this
495 496 # call. However, as this is a POST is better to have it, so automated
496 497 # tools don't flag it as potential CSRF.
497 498 # Post is required because the payload could be bigger than the maximum
498 499 # allowed by GET.
499 500
500 501 text = self.request.POST.get('text')
501 502 renderer = self.request.POST.get('renderer') or 'rst'
502 503 if text:
503 504 return h.render(text, renderer=renderer, mentions=True)
504 505 return ''
505 506
506 507 @LoginRequired()
507 508 @NotAnonymous()
508 509 @HasRepoPermissionAnyDecorator(
509 510 'repository.read', 'repository.write', 'repository.admin')
510 511 @CSRFRequired()
511 512 @view_config(
512 513 route_name='repo_commit_comment_delete', request_method='POST',
513 514 renderer='json_ext')
514 515 def repo_commit_comment_delete(self):
515 516 commit_id = self.request.matchdict['commit_id']
516 517 comment_id = self.request.matchdict['comment_id']
517 518
518 519 comment = ChangesetComment.get_or_404(comment_id)
519 520 if not comment:
520 521 log.debug('Comment with id:%s not found, skipping', comment_id)
521 522 # comment already deleted in another call probably
522 523 return True
523 524
524 525 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
525 526 super_admin = h.HasPermissionAny('hg.admin')()
526 527 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
527 528 is_repo_comment = comment.repo.repo_name == self.db_repo_name
528 529 comment_repo_admin = is_repo_admin and is_repo_comment
529 530
530 531 if super_admin or comment_owner or comment_repo_admin:
531 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
532 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
532 533 Session().commit()
533 534 return True
534 535 else:
535 536 log.warning('No permissions for user %s to delete comment_id: %s',
536 537 self._rhodecode_db_user, comment_id)
537 538 raise HTTPNotFound()
538 539
539 540 @LoginRequired()
540 541 @HasRepoPermissionAnyDecorator(
541 542 'repository.read', 'repository.write', 'repository.admin')
542 543 @view_config(
543 544 route_name='repo_commit_data', request_method='GET',
544 545 renderer='json_ext', xhr=True)
545 546 def repo_commit_data(self):
546 547 commit_id = self.request.matchdict['commit_id']
547 548 self.load_default_context()
548 549
549 550 try:
550 551 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
551 552 except CommitDoesNotExistError as e:
552 553 return EmptyCommit(message=str(e))
553 554
554 555 @LoginRequired()
555 556 @HasRepoPermissionAnyDecorator(
556 557 'repository.read', 'repository.write', 'repository.admin')
557 558 @view_config(
558 559 route_name='repo_commit_children', request_method='GET',
559 560 renderer='json_ext', xhr=True)
560 561 def repo_commit_children(self):
561 562 commit_id = self.request.matchdict['commit_id']
562 563 self.load_default_context()
563 564
564 565 try:
565 566 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
566 567 children = commit.children
567 568 except CommitDoesNotExistError:
568 569 children = []
569 570
570 571 result = {"results": children}
571 572 return result
572 573
573 574 @LoginRequired()
574 575 @HasRepoPermissionAnyDecorator(
575 576 'repository.read', 'repository.write', 'repository.admin')
576 577 @view_config(
577 578 route_name='repo_commit_parents', request_method='GET',
578 579 renderer='json_ext')
579 580 def repo_commit_parents(self):
580 581 commit_id = self.request.matchdict['commit_id']
581 582 self.load_default_context()
582 583
583 584 try:
584 585 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
585 586 parents = commit.parents
586 587 except CommitDoesNotExistError:
587 588 parents = []
588 589 result = {"results": parents}
589 590 return result
@@ -1,1298 +1,1298 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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 collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
45 45 RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
49 49 ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64
65 65 return c
66 66
67 67 def _get_pull_requests_list(
68 68 self, repo_name, source, filter_type, opened_by, statuses):
69 69
70 70 draw, start, limit = self._extract_chunk(self.request)
71 71 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 72 _render = self.request.get_partial_renderer(
73 73 'rhodecode:templates/data_table/_dt_elements.mako')
74 74
75 75 # pagination
76 76
77 77 if filter_type == 'awaiting_review':
78 78 pull_requests = PullRequestModel().get_awaiting_review(
79 79 repo_name, source=source, opened_by=opened_by,
80 80 statuses=statuses, offset=start, length=limit,
81 81 order_by=order_by, order_dir=order_dir)
82 82 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 83 repo_name, source=source, statuses=statuses,
84 84 opened_by=opened_by)
85 85 elif filter_type == 'awaiting_my_review':
86 86 pull_requests = PullRequestModel().get_awaiting_my_review(
87 87 repo_name, source=source, opened_by=opened_by,
88 88 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 89 offset=start, length=limit, order_by=order_by,
90 90 order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, source=source, user_id=self._rhodecode_user.user_id,
93 93 statuses=statuses, opened_by=opened_by)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr)
108 108
109 109 data.append({
110 110 'name': _render('pullrequest_name',
111 111 pr.pull_request_id, pr.target_repo.repo_name),
112 112 'name_raw': pr.pull_request_id,
113 113 'status': _render('pullrequest_status',
114 114 pr.calculated_review_status()),
115 115 'title': _render(
116 116 'pullrequest_title', pr.title, pr.description),
117 117 'description': h.escape(pr.description),
118 118 'updated_on': _render('pullrequest_updated_on',
119 119 h.datetime_to_time(pr.updated_on)),
120 120 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 121 'created_on': _render('pullrequest_updated_on',
122 122 h.datetime_to_time(pr.created_on)),
123 123 'created_on_raw': h.datetime_to_time(pr.created_on),
124 124 'author': _render('pullrequest_author',
125 125 pr.author.full_contact, ),
126 126 'author_raw': pr.author.full_name,
127 127 'comments': _render('pullrequest_comments', len(comments)),
128 128 'comments_raw': len(comments),
129 129 'closed': pr.is_closed(),
130 130 })
131 131
132 132 data = ({
133 133 'draw': draw,
134 134 'data': data,
135 135 'recordsTotal': pull_requests_total_count,
136 136 'recordsFiltered': pull_requests_total_count,
137 137 })
138 138 return data
139 139
140 140 @LoginRequired()
141 141 @HasRepoPermissionAnyDecorator(
142 142 'repository.read', 'repository.write', 'repository.admin')
143 143 @view_config(
144 144 route_name='pullrequest_show_all', request_method='GET',
145 145 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
146 146 def pull_request_list(self):
147 147 c = self.load_default_context()
148 148
149 149 req_get = self.request.GET
150 150 c.source = str2bool(req_get.get('source'))
151 151 c.closed = str2bool(req_get.get('closed'))
152 152 c.my = str2bool(req_get.get('my'))
153 153 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
154 154 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
155 155
156 156 c.active = 'open'
157 157 if c.my:
158 158 c.active = 'my'
159 159 if c.closed:
160 160 c.active = 'closed'
161 161 if c.awaiting_review and not c.source:
162 162 c.active = 'awaiting'
163 163 if c.source and not c.awaiting_review:
164 164 c.active = 'source'
165 165 if c.awaiting_my_review:
166 166 c.active = 'awaiting_my'
167 167
168 168 return self._get_template_context(c)
169 169
170 170 @LoginRequired()
171 171 @HasRepoPermissionAnyDecorator(
172 172 'repository.read', 'repository.write', 'repository.admin')
173 173 @view_config(
174 174 route_name='pullrequest_show_all_data', request_method='GET',
175 175 renderer='json_ext', xhr=True)
176 176 def pull_request_list_data(self):
177 177 self.load_default_context()
178 178
179 179 # additional filters
180 180 req_get = self.request.GET
181 181 source = str2bool(req_get.get('source'))
182 182 closed = str2bool(req_get.get('closed'))
183 183 my = str2bool(req_get.get('my'))
184 184 awaiting_review = str2bool(req_get.get('awaiting_review'))
185 185 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
186 186
187 187 filter_type = 'awaiting_review' if awaiting_review \
188 188 else 'awaiting_my_review' if awaiting_my_review \
189 189 else None
190 190
191 191 opened_by = None
192 192 if my:
193 193 opened_by = [self._rhodecode_user.user_id]
194 194
195 195 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
196 196 if closed:
197 197 statuses = [PullRequest.STATUS_CLOSED]
198 198
199 199 data = self._get_pull_requests_list(
200 200 repo_name=self.db_repo_name, source=source,
201 201 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
202 202
203 203 return data
204 204
205 205 def _is_diff_cache_enabled(self, target_repo):
206 206 caching_enabled = self._get_general_setting(
207 207 target_repo, 'rhodecode_diff_cache')
208 208 log.debug('Diff caching enabled: %s', caching_enabled)
209 209 return caching_enabled
210 210
211 211 def _get_diffset(self, source_repo_name, source_repo,
212 212 source_ref_id, target_ref_id,
213 213 target_commit, source_commit, diff_limit, file_limit,
214 214 fulldiff):
215 215
216 216 vcs_diff = PullRequestModel().get_diff(
217 217 source_repo, source_ref_id, target_ref_id)
218 218
219 219 diff_processor = diffs.DiffProcessor(
220 220 vcs_diff, format='newdiff', diff_limit=diff_limit,
221 221 file_limit=file_limit, show_full_diff=fulldiff)
222 222
223 223 _parsed = diff_processor.prepare()
224 224
225 225 diffset = codeblocks.DiffSet(
226 226 repo_name=self.db_repo_name,
227 227 source_repo_name=source_repo_name,
228 228 source_node_getter=codeblocks.diffset_node_getter(target_commit),
229 229 target_node_getter=codeblocks.diffset_node_getter(source_commit),
230 230 )
231 231 diffset = self.path_filter.render_patchset_filtered(
232 232 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
233 233
234 234 return diffset
235 235
236 236 @LoginRequired()
237 237 @HasRepoPermissionAnyDecorator(
238 238 'repository.read', 'repository.write', 'repository.admin')
239 239 @view_config(
240 240 route_name='pullrequest_show', request_method='GET',
241 241 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
242 242 def pull_request_show(self):
243 243 pull_request_id = self.request.matchdict['pull_request_id']
244 244
245 245 c = self.load_default_context()
246 246
247 247 version = self.request.GET.get('version')
248 248 from_version = self.request.GET.get('from_version') or version
249 249 merge_checks = self.request.GET.get('merge_checks')
250 250 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
251 251
252 252 (pull_request_latest,
253 253 pull_request_at_ver,
254 254 pull_request_display_obj,
255 255 at_version) = PullRequestModel().get_pr_version(
256 256 pull_request_id, version=version)
257 257 pr_closed = pull_request_latest.is_closed()
258 258
259 259 if pr_closed and (version or from_version):
260 260 # not allow to browse versions
261 261 raise HTTPFound(h.route_path(
262 262 'pullrequest_show', repo_name=self.db_repo_name,
263 263 pull_request_id=pull_request_id))
264 264
265 265 versions = pull_request_display_obj.versions()
266 266
267 267 c.at_version = at_version
268 268 c.at_version_num = (at_version
269 269 if at_version and at_version != 'latest'
270 270 else None)
271 271 c.at_version_pos = ChangesetComment.get_index_from_version(
272 272 c.at_version_num, versions)
273 273
274 274 (prev_pull_request_latest,
275 275 prev_pull_request_at_ver,
276 276 prev_pull_request_display_obj,
277 277 prev_at_version) = PullRequestModel().get_pr_version(
278 278 pull_request_id, version=from_version)
279 279
280 280 c.from_version = prev_at_version
281 281 c.from_version_num = (prev_at_version
282 282 if prev_at_version and prev_at_version != 'latest'
283 283 else None)
284 284 c.from_version_pos = ChangesetComment.get_index_from_version(
285 285 c.from_version_num, versions)
286 286
287 287 # define if we're in COMPARE mode or VIEW at version mode
288 288 compare = at_version != prev_at_version
289 289
290 290 # pull_requests repo_name we opened it against
291 291 # ie. target_repo must match
292 292 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
293 293 raise HTTPNotFound()
294 294
295 295 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
296 296 pull_request_at_ver)
297 297
298 298 c.pull_request = pull_request_display_obj
299 299 c.pull_request_latest = pull_request_latest
300 300
301 301 if compare or (at_version and not at_version == 'latest'):
302 302 c.allowed_to_change_status = False
303 303 c.allowed_to_update = False
304 304 c.allowed_to_merge = False
305 305 c.allowed_to_delete = False
306 306 c.allowed_to_comment = False
307 307 c.allowed_to_close = False
308 308 else:
309 309 can_change_status = PullRequestModel().check_user_change_status(
310 310 pull_request_at_ver, self._rhodecode_user)
311 311 c.allowed_to_change_status = can_change_status and not pr_closed
312 312
313 313 c.allowed_to_update = PullRequestModel().check_user_update(
314 314 pull_request_latest, self._rhodecode_user) and not pr_closed
315 315 c.allowed_to_merge = PullRequestModel().check_user_merge(
316 316 pull_request_latest, self._rhodecode_user) and not pr_closed
317 317 c.allowed_to_delete = PullRequestModel().check_user_delete(
318 318 pull_request_latest, self._rhodecode_user) and not pr_closed
319 319 c.allowed_to_comment = not pr_closed
320 320 c.allowed_to_close = c.allowed_to_merge and not pr_closed
321 321
322 322 c.forbid_adding_reviewers = False
323 323 c.forbid_author_to_review = False
324 324 c.forbid_commit_author_to_review = False
325 325
326 326 if pull_request_latest.reviewer_data and \
327 327 'rules' in pull_request_latest.reviewer_data:
328 328 rules = pull_request_latest.reviewer_data['rules'] or {}
329 329 try:
330 330 c.forbid_adding_reviewers = rules.get(
331 331 'forbid_adding_reviewers')
332 332 c.forbid_author_to_review = rules.get(
333 333 'forbid_author_to_review')
334 334 c.forbid_commit_author_to_review = rules.get(
335 335 'forbid_commit_author_to_review')
336 336 except Exception:
337 337 pass
338 338
339 339 # check merge capabilities
340 340 _merge_check = MergeCheck.validate(
341 341 pull_request_latest, user=self._rhodecode_user,
342 342 translator=self.request.translate)
343 343 c.pr_merge_errors = _merge_check.error_details
344 344 c.pr_merge_possible = not _merge_check.failed
345 345 c.pr_merge_message = _merge_check.merge_msg
346 346
347 347 c.pr_merge_info = MergeCheck.get_merge_conditions(
348 348 pull_request_latest, translator=self.request.translate)
349 349
350 350 c.pull_request_review_status = _merge_check.review_status
351 351 if merge_checks:
352 352 self.request.override_renderer = \
353 353 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
354 354 return self._get_template_context(c)
355 355
356 356 comments_model = CommentsModel()
357 357
358 358 # reviewers and statuses
359 359 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
360 360 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
361 361
362 362 # GENERAL COMMENTS with versions #
363 363 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
364 364 q = q.order_by(ChangesetComment.comment_id.asc())
365 365 general_comments = q
366 366
367 367 # pick comments we want to render at current version
368 368 c.comment_versions = comments_model.aggregate_comments(
369 369 general_comments, versions, c.at_version_num)
370 370 c.comments = c.comment_versions[c.at_version_num]['until']
371 371
372 372 # INLINE COMMENTS with versions #
373 373 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
374 374 q = q.order_by(ChangesetComment.comment_id.asc())
375 375 inline_comments = q
376 376
377 377 c.inline_versions = comments_model.aggregate_comments(
378 378 inline_comments, versions, c.at_version_num, inline=True)
379 379
380 380 # inject latest version
381 381 latest_ver = PullRequest.get_pr_display_object(
382 382 pull_request_latest, pull_request_latest)
383 383
384 384 c.versions = versions + [latest_ver]
385 385
386 386 # if we use version, then do not show later comments
387 387 # than current version
388 388 display_inline_comments = collections.defaultdict(
389 389 lambda: collections.defaultdict(list))
390 390 for co in inline_comments:
391 391 if c.at_version_num:
392 392 # pick comments that are at least UPTO given version, so we
393 393 # don't render comments for higher version
394 394 should_render = co.pull_request_version_id and \
395 395 co.pull_request_version_id <= c.at_version_num
396 396 else:
397 397 # showing all, for 'latest'
398 398 should_render = True
399 399
400 400 if should_render:
401 401 display_inline_comments[co.f_path][co.line_no].append(co)
402 402
403 403 # load diff data into template context, if we use compare mode then
404 404 # diff is calculated based on changes between versions of PR
405 405
406 406 source_repo = pull_request_at_ver.source_repo
407 407 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
408 408
409 409 target_repo = pull_request_at_ver.target_repo
410 410 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
411 411
412 412 if compare:
413 413 # in compare switch the diff base to latest commit from prev version
414 414 target_ref_id = prev_pull_request_display_obj.revisions[0]
415 415
416 416 # despite opening commits for bookmarks/branches/tags, we always
417 417 # convert this to rev to prevent changes after bookmark or branch change
418 418 c.source_ref_type = 'rev'
419 419 c.source_ref = source_ref_id
420 420
421 421 c.target_ref_type = 'rev'
422 422 c.target_ref = target_ref_id
423 423
424 424 c.source_repo = source_repo
425 425 c.target_repo = target_repo
426 426
427 427 c.commit_ranges = []
428 428 source_commit = EmptyCommit()
429 429 target_commit = EmptyCommit()
430 430 c.missing_requirements = False
431 431
432 432 source_scm = source_repo.scm_instance()
433 433 target_scm = target_repo.scm_instance()
434 434
435 435 # try first shadow repo, fallback to regular repo
436 436 try:
437 437 commits_source_repo = pull_request_latest.get_shadow_repo()
438 438 except Exception:
439 439 log.debug('Failed to get shadow repo', exc_info=True)
440 440 commits_source_repo = source_scm
441 441
442 442 c.commits_source_repo = commits_source_repo
443 443 c.ancestor = None # set it to None, to hide it from PR view
444 444
445 445 # empty version means latest, so we keep this to prevent
446 446 # double caching
447 447 version_normalized = version or 'latest'
448 448 from_version_normalized = from_version or 'latest'
449 449
450 450 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
451 451 target_repo)
452 452 cache_file_path = diff_cache_exist(
453 453 cache_path, 'pull_request', pull_request_id, version_normalized,
454 454 from_version_normalized, source_ref_id, target_ref_id, c.fulldiff)
455 455
456 456 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
457 457 force_recache = str2bool(self.request.GET.get('force_recache'))
458 458
459 459 cached_diff = None
460 460 if caching_enabled:
461 461 cached_diff = load_cached_diff(cache_file_path)
462 462
463 463 has_proper_commit_cache = (
464 464 cached_diff and cached_diff.get('commits')
465 465 and len(cached_diff.get('commits', [])) == 5
466 466 and cached_diff.get('commits')[0]
467 467 and cached_diff.get('commits')[3])
468 468 if not force_recache and has_proper_commit_cache:
469 469 diff_commit_cache = \
470 470 (ancestor_commit, commit_cache, missing_requirements,
471 471 source_commit, target_commit) = cached_diff['commits']
472 472 else:
473 473 diff_commit_cache = \
474 474 (ancestor_commit, commit_cache, missing_requirements,
475 475 source_commit, target_commit) = self.get_commits(
476 476 commits_source_repo,
477 477 pull_request_at_ver,
478 478 source_commit,
479 479 source_ref_id,
480 480 source_scm,
481 481 target_commit,
482 482 target_ref_id,
483 483 target_scm)
484 484
485 485 # register our commit range
486 486 for comm in commit_cache.values():
487 487 c.commit_ranges.append(comm)
488 488
489 489 c.missing_requirements = missing_requirements
490 490 c.ancestor_commit = ancestor_commit
491 491 c.statuses = source_repo.statuses(
492 492 [x.raw_id for x in c.commit_ranges])
493 493
494 494 # auto collapse if we have more than limit
495 495 collapse_limit = diffs.DiffProcessor._collapse_commits_over
496 496 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
497 497 c.compare_mode = compare
498 498
499 499 # diff_limit is the old behavior, will cut off the whole diff
500 500 # if the limit is applied otherwise will just hide the
501 501 # big files from the front-end
502 502 diff_limit = c.visual.cut_off_limit_diff
503 503 file_limit = c.visual.cut_off_limit_file
504 504
505 505 c.missing_commits = False
506 506 if (c.missing_requirements
507 507 or isinstance(source_commit, EmptyCommit)
508 508 or source_commit == target_commit):
509 509
510 510 c.missing_commits = True
511 511 else:
512 512 c.inline_comments = display_inline_comments
513 513
514 514 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
515 515 if not force_recache and has_proper_diff_cache:
516 516 c.diffset = cached_diff['diff']
517 517 (ancestor_commit, commit_cache, missing_requirements,
518 518 source_commit, target_commit) = cached_diff['commits']
519 519 else:
520 520 c.diffset = self._get_diffset(
521 521 c.source_repo.repo_name, commits_source_repo,
522 522 source_ref_id, target_ref_id,
523 523 target_commit, source_commit,
524 524 diff_limit, file_limit, c.fulldiff)
525 525
526 526 # save cached diff
527 527 if caching_enabled:
528 528 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
529 529
530 530 c.limited_diff = c.diffset.limited_diff
531 531
532 532 # calculate removed files that are bound to comments
533 533 comment_deleted_files = [
534 534 fname for fname in display_inline_comments
535 535 if fname not in c.diffset.file_stats]
536 536
537 537 c.deleted_files_comments = collections.defaultdict(dict)
538 538 for fname, per_line_comments in display_inline_comments.items():
539 539 if fname in comment_deleted_files:
540 540 c.deleted_files_comments[fname]['stats'] = 0
541 541 c.deleted_files_comments[fname]['comments'] = list()
542 542 for lno, comments in per_line_comments.items():
543 543 c.deleted_files_comments[fname]['comments'].extend(
544 544 comments)
545 545
546 546 # this is a hack to properly display links, when creating PR, the
547 547 # compare view and others uses different notation, and
548 548 # compare_commits.mako renders links based on the target_repo.
549 549 # We need to swap that here to generate it properly on the html side
550 550 c.target_repo = c.source_repo
551 551
552 552 c.commit_statuses = ChangesetStatus.STATUSES
553 553
554 554 c.show_version_changes = not pr_closed
555 555 if c.show_version_changes:
556 556 cur_obj = pull_request_at_ver
557 557 prev_obj = prev_pull_request_at_ver
558 558
559 559 old_commit_ids = prev_obj.revisions
560 560 new_commit_ids = cur_obj.revisions
561 561 commit_changes = PullRequestModel()._calculate_commit_id_changes(
562 562 old_commit_ids, new_commit_ids)
563 563 c.commit_changes_summary = commit_changes
564 564
565 565 # calculate the diff for commits between versions
566 566 c.commit_changes = []
567 567 mark = lambda cs, fw: list(
568 568 h.itertools.izip_longest([], cs, fillvalue=fw))
569 569 for c_type, raw_id in mark(commit_changes.added, 'a') \
570 570 + mark(commit_changes.removed, 'r') \
571 571 + mark(commit_changes.common, 'c'):
572 572
573 573 if raw_id in commit_cache:
574 574 commit = commit_cache[raw_id]
575 575 else:
576 576 try:
577 577 commit = commits_source_repo.get_commit(raw_id)
578 578 except CommitDoesNotExistError:
579 579 # in case we fail extracting still use "dummy" commit
580 580 # for display in commit diff
581 581 commit = h.AttributeDict(
582 582 {'raw_id': raw_id,
583 583 'message': 'EMPTY or MISSING COMMIT'})
584 584 c.commit_changes.append([c_type, commit])
585 585
586 586 # current user review statuses for each version
587 587 c.review_versions = {}
588 588 if self._rhodecode_user.user_id in allowed_reviewers:
589 589 for co in general_comments:
590 590 if co.author.user_id == self._rhodecode_user.user_id:
591 591 status = co.status_change
592 592 if status:
593 593 _ver_pr = status[0].comment.pull_request_version_id
594 594 c.review_versions[_ver_pr] = status[0]
595 595
596 596 return self._get_template_context(c)
597 597
598 598 def get_commits(
599 599 self, commits_source_repo, pull_request_at_ver, source_commit,
600 600 source_ref_id, source_scm, target_commit, target_ref_id, target_scm):
601 601 commit_cache = collections.OrderedDict()
602 602 missing_requirements = False
603 603 try:
604 604 pre_load = ["author", "branch", "date", "message"]
605 605 show_revs = pull_request_at_ver.revisions
606 606 for rev in show_revs:
607 607 comm = commits_source_repo.get_commit(
608 608 commit_id=rev, pre_load=pre_load)
609 609 commit_cache[comm.raw_id] = comm
610 610
611 611 # Order here matters, we first need to get target, and then
612 612 # the source
613 613 target_commit = commits_source_repo.get_commit(
614 614 commit_id=safe_str(target_ref_id))
615 615
616 616 source_commit = commits_source_repo.get_commit(
617 617 commit_id=safe_str(source_ref_id))
618 618 except CommitDoesNotExistError:
619 619 log.warning(
620 620 'Failed to get commit from `{}` repo'.format(
621 621 commits_source_repo), exc_info=True)
622 622 except RepositoryRequirementError:
623 623 log.warning(
624 624 'Failed to get all required data from repo', exc_info=True)
625 625 missing_requirements = True
626 626 ancestor_commit = None
627 627 try:
628 628 ancestor_id = source_scm.get_common_ancestor(
629 629 source_commit.raw_id, target_commit.raw_id, target_scm)
630 630 ancestor_commit = source_scm.get_commit(ancestor_id)
631 631 except Exception:
632 632 ancestor_commit = None
633 633 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
634 634
635 635 def assure_not_empty_repo(self):
636 636 _ = self.request.translate
637 637
638 638 try:
639 639 self.db_repo.scm_instance().get_commit()
640 640 except EmptyRepositoryError:
641 641 h.flash(h.literal(_('There are no commits yet')),
642 642 category='warning')
643 643 raise HTTPFound(
644 644 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
645 645
646 646 @LoginRequired()
647 647 @NotAnonymous()
648 648 @HasRepoPermissionAnyDecorator(
649 649 'repository.read', 'repository.write', 'repository.admin')
650 650 @view_config(
651 651 route_name='pullrequest_new', request_method='GET',
652 652 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
653 653 def pull_request_new(self):
654 654 _ = self.request.translate
655 655 c = self.load_default_context()
656 656
657 657 self.assure_not_empty_repo()
658 658 source_repo = self.db_repo
659 659
660 660 commit_id = self.request.GET.get('commit')
661 661 branch_ref = self.request.GET.get('branch')
662 662 bookmark_ref = self.request.GET.get('bookmark')
663 663
664 664 try:
665 665 source_repo_data = PullRequestModel().generate_repo_data(
666 666 source_repo, commit_id=commit_id,
667 667 branch=branch_ref, bookmark=bookmark_ref,
668 668 translator=self.request.translate)
669 669 except CommitDoesNotExistError as e:
670 670 log.exception(e)
671 671 h.flash(_('Commit does not exist'), 'error')
672 672 raise HTTPFound(
673 673 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
674 674
675 675 default_target_repo = source_repo
676 676
677 677 if source_repo.parent:
678 678 parent_vcs_obj = source_repo.parent.scm_instance()
679 679 if parent_vcs_obj and not parent_vcs_obj.is_empty():
680 680 # change default if we have a parent repo
681 681 default_target_repo = source_repo.parent
682 682
683 683 target_repo_data = PullRequestModel().generate_repo_data(
684 684 default_target_repo, translator=self.request.translate)
685 685
686 686 selected_source_ref = source_repo_data['refs']['selected_ref']
687 687 title_source_ref = ''
688 688 if selected_source_ref:
689 689 title_source_ref = selected_source_ref.split(':', 2)[1]
690 690 c.default_title = PullRequestModel().generate_pullrequest_title(
691 691 source=source_repo.repo_name,
692 692 source_ref=title_source_ref,
693 693 target=default_target_repo.repo_name
694 694 )
695 695
696 696 c.default_repo_data = {
697 697 'source_repo_name': source_repo.repo_name,
698 698 'source_refs_json': json.dumps(source_repo_data),
699 699 'target_repo_name': default_target_repo.repo_name,
700 700 'target_refs_json': json.dumps(target_repo_data),
701 701 }
702 702 c.default_source_ref = selected_source_ref
703 703
704 704 return self._get_template_context(c)
705 705
706 706 @LoginRequired()
707 707 @NotAnonymous()
708 708 @HasRepoPermissionAnyDecorator(
709 709 'repository.read', 'repository.write', 'repository.admin')
710 710 @view_config(
711 711 route_name='pullrequest_repo_refs', request_method='GET',
712 712 renderer='json_ext', xhr=True)
713 713 def pull_request_repo_refs(self):
714 714 self.load_default_context()
715 715 target_repo_name = self.request.matchdict['target_repo_name']
716 716 repo = Repository.get_by_repo_name(target_repo_name)
717 717 if not repo:
718 718 raise HTTPNotFound()
719 719
720 720 target_perm = HasRepoPermissionAny(
721 721 'repository.read', 'repository.write', 'repository.admin')(
722 722 target_repo_name)
723 723 if not target_perm:
724 724 raise HTTPNotFound()
725 725
726 726 return PullRequestModel().generate_repo_data(
727 727 repo, translator=self.request.translate)
728 728
729 729 @LoginRequired()
730 730 @NotAnonymous()
731 731 @HasRepoPermissionAnyDecorator(
732 732 'repository.read', 'repository.write', 'repository.admin')
733 733 @view_config(
734 734 route_name='pullrequest_repo_destinations', request_method='GET',
735 735 renderer='json_ext', xhr=True)
736 736 def pull_request_repo_destinations(self):
737 737 _ = self.request.translate
738 738 filter_query = self.request.GET.get('query')
739 739
740 740 query = Repository.query() \
741 741 .order_by(func.length(Repository.repo_name)) \
742 742 .filter(
743 743 or_(Repository.repo_name == self.db_repo.repo_name,
744 744 Repository.fork_id == self.db_repo.repo_id))
745 745
746 746 if filter_query:
747 747 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
748 748 query = query.filter(
749 749 Repository.repo_name.ilike(ilike_expression))
750 750
751 751 add_parent = False
752 752 if self.db_repo.parent:
753 753 if filter_query in self.db_repo.parent.repo_name:
754 754 parent_vcs_obj = self.db_repo.parent.scm_instance()
755 755 if parent_vcs_obj and not parent_vcs_obj.is_empty():
756 756 add_parent = True
757 757
758 758 limit = 20 - 1 if add_parent else 20
759 759 all_repos = query.limit(limit).all()
760 760 if add_parent:
761 761 all_repos += [self.db_repo.parent]
762 762
763 763 repos = []
764 764 for obj in ScmModel().get_repos(all_repos):
765 765 repos.append({
766 766 'id': obj['name'],
767 767 'text': obj['name'],
768 768 'type': 'repo',
769 769 'obj': obj['dbrepo']
770 770 })
771 771
772 772 data = {
773 773 'more': False,
774 774 'results': [{
775 775 'text': _('Repositories'),
776 776 'children': repos
777 777 }] if repos else []
778 778 }
779 779 return data
780 780
781 781 @LoginRequired()
782 782 @NotAnonymous()
783 783 @HasRepoPermissionAnyDecorator(
784 784 'repository.read', 'repository.write', 'repository.admin')
785 785 @CSRFRequired()
786 786 @view_config(
787 787 route_name='pullrequest_create', request_method='POST',
788 788 renderer=None)
789 789 def pull_request_create(self):
790 790 _ = self.request.translate
791 791 self.assure_not_empty_repo()
792 792 self.load_default_context()
793 793
794 794 controls = peppercorn.parse(self.request.POST.items())
795 795
796 796 try:
797 797 form = PullRequestForm(
798 798 self.request.translate, self.db_repo.repo_id)()
799 799 _form = form.to_python(controls)
800 800 except formencode.Invalid as errors:
801 801 if errors.error_dict.get('revisions'):
802 802 msg = 'Revisions: %s' % errors.error_dict['revisions']
803 803 elif errors.error_dict.get('pullrequest_title'):
804 804 msg = errors.error_dict.get('pullrequest_title')
805 805 else:
806 806 msg = _('Error creating pull request: {}').format(errors)
807 807 log.exception(msg)
808 808 h.flash(msg, 'error')
809 809
810 810 # would rather just go back to form ...
811 811 raise HTTPFound(
812 812 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
813 813
814 814 source_repo = _form['source_repo']
815 815 source_ref = _form['source_ref']
816 816 target_repo = _form['target_repo']
817 817 target_ref = _form['target_ref']
818 818 commit_ids = _form['revisions'][::-1]
819 819
820 820 # find the ancestor for this pr
821 821 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
822 822 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
823 823
824 824 # re-check permissions again here
825 825 # source_repo we must have read permissions
826 826
827 827 source_perm = HasRepoPermissionAny(
828 828 'repository.read',
829 829 'repository.write', 'repository.admin')(source_db_repo.repo_name)
830 830 if not source_perm:
831 831 msg = _('Not Enough permissions to source repo `{}`.'.format(
832 832 source_db_repo.repo_name))
833 833 h.flash(msg, category='error')
834 834 # copy the args back to redirect
835 835 org_query = self.request.GET.mixed()
836 836 raise HTTPFound(
837 837 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
838 838 _query=org_query))
839 839
840 840 # target repo we must have read permissions, and also later on
841 841 # we want to check branch permissions here
842 842 target_perm = HasRepoPermissionAny(
843 843 'repository.read',
844 844 'repository.write', 'repository.admin')(target_db_repo.repo_name)
845 845 if not target_perm:
846 846 msg = _('Not Enough permissions to target repo `{}`.'.format(
847 847 target_db_repo.repo_name))
848 848 h.flash(msg, category='error')
849 849 # copy the args back to redirect
850 850 org_query = self.request.GET.mixed()
851 851 raise HTTPFound(
852 852 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
853 853 _query=org_query))
854 854
855 855 source_scm = source_db_repo.scm_instance()
856 856 target_scm = target_db_repo.scm_instance()
857 857
858 858 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
859 859 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
860 860
861 861 ancestor = source_scm.get_common_ancestor(
862 862 source_commit.raw_id, target_commit.raw_id, target_scm)
863 863
864 864 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
865 865 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
866 866
867 867 pullrequest_title = _form['pullrequest_title']
868 868 title_source_ref = source_ref.split(':', 2)[1]
869 869 if not pullrequest_title:
870 870 pullrequest_title = PullRequestModel().generate_pullrequest_title(
871 871 source=source_repo,
872 872 source_ref=title_source_ref,
873 873 target=target_repo
874 874 )
875 875
876 876 description = _form['pullrequest_desc']
877 877
878 878 get_default_reviewers_data, validate_default_reviewers = \
879 879 PullRequestModel().get_reviewer_functions()
880 880
881 881 # recalculate reviewers logic, to make sure we can validate this
882 882 reviewer_rules = get_default_reviewers_data(
883 883 self._rhodecode_db_user, source_db_repo,
884 884 source_commit, target_db_repo, target_commit)
885 885
886 886 given_reviewers = _form['review_members']
887 887 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
888 888
889 889 try:
890 890 pull_request = PullRequestModel().create(
891 891 self._rhodecode_user.user_id, source_repo, source_ref,
892 892 target_repo, target_ref, commit_ids, reviewers,
893 893 pullrequest_title, description, reviewer_rules
894 894 )
895 895 Session().commit()
896 896
897 897 h.flash(_('Successfully opened new pull request'),
898 898 category='success')
899 899 except Exception:
900 900 msg = _('Error occurred during creation of this pull request.')
901 901 log.exception(msg)
902 902 h.flash(msg, category='error')
903 903
904 904 # copy the args back to redirect
905 905 org_query = self.request.GET.mixed()
906 906 raise HTTPFound(
907 907 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
908 908 _query=org_query))
909 909
910 910 raise HTTPFound(
911 911 h.route_path('pullrequest_show', repo_name=target_repo,
912 912 pull_request_id=pull_request.pull_request_id))
913 913
914 914 @LoginRequired()
915 915 @NotAnonymous()
916 916 @HasRepoPermissionAnyDecorator(
917 917 'repository.read', 'repository.write', 'repository.admin')
918 918 @CSRFRequired()
919 919 @view_config(
920 920 route_name='pullrequest_update', request_method='POST',
921 921 renderer='json_ext')
922 922 def pull_request_update(self):
923 923 pull_request = PullRequest.get_or_404(
924 924 self.request.matchdict['pull_request_id'])
925 925 _ = self.request.translate
926 926
927 927 self.load_default_context()
928 928
929 929 if pull_request.is_closed():
930 930 log.debug('update: forbidden because pull request is closed')
931 931 msg = _(u'Cannot update closed pull requests.')
932 932 h.flash(msg, category='error')
933 933 return True
934 934
935 935 # only owner or admin can update it
936 936 allowed_to_update = PullRequestModel().check_user_update(
937 937 pull_request, self._rhodecode_user)
938 938 if allowed_to_update:
939 939 controls = peppercorn.parse(self.request.POST.items())
940 940
941 941 if 'review_members' in controls:
942 942 self._update_reviewers(
943 943 pull_request, controls['review_members'],
944 944 pull_request.reviewer_data)
945 945 elif str2bool(self.request.POST.get('update_commits', 'false')):
946 946 self._update_commits(pull_request)
947 947 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
948 948 self._edit_pull_request(pull_request)
949 949 else:
950 950 raise HTTPBadRequest()
951 951 return True
952 952 raise HTTPForbidden()
953 953
954 954 def _edit_pull_request(self, pull_request):
955 955 _ = self.request.translate
956 956 try:
957 957 PullRequestModel().edit(
958 958 pull_request, self.request.POST.get('title'),
959 959 self.request.POST.get('description'), self._rhodecode_user)
960 960 except ValueError:
961 961 msg = _(u'Cannot update closed pull requests.')
962 962 h.flash(msg, category='error')
963 963 return
964 964 else:
965 965 Session().commit()
966 966
967 967 msg = _(u'Pull request title & description updated.')
968 968 h.flash(msg, category='success')
969 969 return
970 970
971 971 def _update_commits(self, pull_request):
972 972 _ = self.request.translate
973 973 resp = PullRequestModel().update_commits(pull_request)
974 974
975 975 if resp.executed:
976 976
977 977 if resp.target_changed and resp.source_changed:
978 978 changed = 'target and source repositories'
979 979 elif resp.target_changed and not resp.source_changed:
980 980 changed = 'target repository'
981 981 elif not resp.target_changed and resp.source_changed:
982 982 changed = 'source repository'
983 983 else:
984 984 changed = 'nothing'
985 985
986 986 msg = _(
987 987 u'Pull request updated to "{source_commit_id}" with '
988 988 u'{count_added} added, {count_removed} removed commits. '
989 989 u'Source of changes: {change_source}')
990 990 msg = msg.format(
991 991 source_commit_id=pull_request.source_ref_parts.commit_id,
992 992 count_added=len(resp.changes.added),
993 993 count_removed=len(resp.changes.removed),
994 994 change_source=changed)
995 995 h.flash(msg, category='success')
996 996
997 997 channel = '/repo${}$/pr/{}'.format(
998 998 pull_request.target_repo.repo_name,
999 999 pull_request.pull_request_id)
1000 1000 message = msg + (
1001 1001 ' - <a onclick="window.location.reload()">'
1002 1002 '<strong>{}</strong></a>'.format(_('Reload page')))
1003 1003 channelstream.post_message(
1004 1004 channel, message, self._rhodecode_user.username,
1005 1005 registry=self.request.registry)
1006 1006 else:
1007 1007 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1008 1008 warning_reasons = [
1009 1009 UpdateFailureReason.NO_CHANGE,
1010 1010 UpdateFailureReason.WRONG_REF_TYPE,
1011 1011 ]
1012 1012 category = 'warning' if resp.reason in warning_reasons else 'error'
1013 1013 h.flash(msg, category=category)
1014 1014
1015 1015 @LoginRequired()
1016 1016 @NotAnonymous()
1017 1017 @HasRepoPermissionAnyDecorator(
1018 1018 'repository.read', 'repository.write', 'repository.admin')
1019 1019 @CSRFRequired()
1020 1020 @view_config(
1021 1021 route_name='pullrequest_merge', request_method='POST',
1022 1022 renderer='json_ext')
1023 1023 def pull_request_merge(self):
1024 1024 """
1025 1025 Merge will perform a server-side merge of the specified
1026 1026 pull request, if the pull request is approved and mergeable.
1027 1027 After successful merging, the pull request is automatically
1028 1028 closed, with a relevant comment.
1029 1029 """
1030 1030 pull_request = PullRequest.get_or_404(
1031 1031 self.request.matchdict['pull_request_id'])
1032 1032
1033 1033 self.load_default_context()
1034 1034 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
1035 1035 translator=self.request.translate)
1036 1036 merge_possible = not check.failed
1037 1037
1038 1038 for err_type, error_msg in check.errors:
1039 1039 h.flash(error_msg, category=err_type)
1040 1040
1041 1041 if merge_possible:
1042 1042 log.debug("Pre-conditions checked, trying to merge.")
1043 1043 extras = vcs_operation_context(
1044 1044 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1045 1045 username=self._rhodecode_db_user.username, action='push',
1046 1046 scm=pull_request.target_repo.repo_type)
1047 1047 self._merge_pull_request(
1048 1048 pull_request, self._rhodecode_db_user, extras)
1049 1049 else:
1050 1050 log.debug("Pre-conditions failed, NOT merging.")
1051 1051
1052 1052 raise HTTPFound(
1053 1053 h.route_path('pullrequest_show',
1054 1054 repo_name=pull_request.target_repo.repo_name,
1055 1055 pull_request_id=pull_request.pull_request_id))
1056 1056
1057 1057 def _merge_pull_request(self, pull_request, user, extras):
1058 1058 _ = self.request.translate
1059 1059 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1060 1060
1061 1061 if merge_resp.executed:
1062 1062 log.debug("The merge was successful, closing the pull request.")
1063 1063 PullRequestModel().close_pull_request(
1064 1064 pull_request.pull_request_id, user)
1065 1065 Session().commit()
1066 1066 msg = _('Pull request was successfully merged and closed.')
1067 1067 h.flash(msg, category='success')
1068 1068 else:
1069 1069 log.debug(
1070 1070 "The merge was not successful. Merge response: %s",
1071 1071 merge_resp)
1072 1072 msg = PullRequestModel().merge_status_message(
1073 1073 merge_resp.failure_reason)
1074 1074 h.flash(msg, category='error')
1075 1075
1076 1076 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1077 1077 _ = self.request.translate
1078 1078 get_default_reviewers_data, validate_default_reviewers = \
1079 1079 PullRequestModel().get_reviewer_functions()
1080 1080
1081 1081 try:
1082 1082 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1083 1083 except ValueError as e:
1084 1084 log.error('Reviewers Validation: {}'.format(e))
1085 1085 h.flash(e, category='error')
1086 1086 return
1087 1087
1088 1088 PullRequestModel().update_reviewers(
1089 1089 pull_request, reviewers, self._rhodecode_user)
1090 1090 h.flash(_('Pull request reviewers updated.'), category='success')
1091 1091 Session().commit()
1092 1092
1093 1093 @LoginRequired()
1094 1094 @NotAnonymous()
1095 1095 @HasRepoPermissionAnyDecorator(
1096 1096 'repository.read', 'repository.write', 'repository.admin')
1097 1097 @CSRFRequired()
1098 1098 @view_config(
1099 1099 route_name='pullrequest_delete', request_method='POST',
1100 1100 renderer='json_ext')
1101 1101 def pull_request_delete(self):
1102 1102 _ = self.request.translate
1103 1103
1104 1104 pull_request = PullRequest.get_or_404(
1105 1105 self.request.matchdict['pull_request_id'])
1106 1106 self.load_default_context()
1107 1107
1108 1108 pr_closed = pull_request.is_closed()
1109 1109 allowed_to_delete = PullRequestModel().check_user_delete(
1110 1110 pull_request, self._rhodecode_user) and not pr_closed
1111 1111
1112 1112 # only owner can delete it !
1113 1113 if allowed_to_delete:
1114 1114 PullRequestModel().delete(pull_request, self._rhodecode_user)
1115 1115 Session().commit()
1116 1116 h.flash(_('Successfully deleted pull request'),
1117 1117 category='success')
1118 1118 raise HTTPFound(h.route_path('pullrequest_show_all',
1119 1119 repo_name=self.db_repo_name))
1120 1120
1121 1121 log.warning('user %s tried to delete pull request without access',
1122 1122 self._rhodecode_user)
1123 1123 raise HTTPNotFound()
1124 1124
1125 1125 @LoginRequired()
1126 1126 @NotAnonymous()
1127 1127 @HasRepoPermissionAnyDecorator(
1128 1128 'repository.read', 'repository.write', 'repository.admin')
1129 1129 @CSRFRequired()
1130 1130 @view_config(
1131 1131 route_name='pullrequest_comment_create', request_method='POST',
1132 1132 renderer='json_ext')
1133 1133 def pull_request_comment_create(self):
1134 1134 _ = self.request.translate
1135 1135
1136 1136 pull_request = PullRequest.get_or_404(
1137 1137 self.request.matchdict['pull_request_id'])
1138 1138 pull_request_id = pull_request.pull_request_id
1139 1139
1140 1140 if pull_request.is_closed():
1141 1141 log.debug('comment: forbidden because pull request is closed')
1142 1142 raise HTTPForbidden()
1143 1143
1144 1144 allowed_to_comment = PullRequestModel().check_user_comment(
1145 1145 pull_request, self._rhodecode_user)
1146 1146 if not allowed_to_comment:
1147 1147 log.debug(
1148 1148 'comment: forbidden because pull request is from forbidden repo')
1149 1149 raise HTTPForbidden()
1150 1150
1151 1151 c = self.load_default_context()
1152 1152
1153 1153 status = self.request.POST.get('changeset_status', None)
1154 1154 text = self.request.POST.get('text')
1155 1155 comment_type = self.request.POST.get('comment_type')
1156 1156 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1157 1157 close_pull_request = self.request.POST.get('close_pull_request')
1158 1158
1159 1159 # the logic here should work like following, if we submit close
1160 1160 # pr comment, use `close_pull_request_with_comment` function
1161 1161 # else handle regular comment logic
1162 1162
1163 1163 if close_pull_request:
1164 1164 # only owner or admin or person with write permissions
1165 1165 allowed_to_close = PullRequestModel().check_user_update(
1166 1166 pull_request, self._rhodecode_user)
1167 1167 if not allowed_to_close:
1168 1168 log.debug('comment: forbidden because not allowed to close '
1169 1169 'pull request %s', pull_request_id)
1170 1170 raise HTTPForbidden()
1171 1171 comment, status = PullRequestModel().close_pull_request_with_comment(
1172 1172 pull_request, self._rhodecode_user, self.db_repo, message=text)
1173 1173 Session().flush()
1174 1174 events.trigger(
1175 1175 events.PullRequestCommentEvent(pull_request, comment))
1176 1176
1177 1177 else:
1178 1178 # regular comment case, could be inline, or one with status.
1179 1179 # for that one we check also permissions
1180 1180
1181 1181 allowed_to_change_status = PullRequestModel().check_user_change_status(
1182 1182 pull_request, self._rhodecode_user)
1183 1183
1184 1184 if status and allowed_to_change_status:
1185 1185 message = (_('Status change %(transition_icon)s %(status)s')
1186 1186 % {'transition_icon': '>',
1187 1187 'status': ChangesetStatus.get_status_lbl(status)})
1188 1188 text = text or message
1189 1189
1190 1190 comment = CommentsModel().create(
1191 1191 text=text,
1192 1192 repo=self.db_repo.repo_id,
1193 1193 user=self._rhodecode_user.user_id,
1194 1194 pull_request=pull_request,
1195 1195 f_path=self.request.POST.get('f_path'),
1196 1196 line_no=self.request.POST.get('line'),
1197 1197 status_change=(ChangesetStatus.get_status_lbl(status)
1198 1198 if status and allowed_to_change_status else None),
1199 1199 status_change_type=(status
1200 1200 if status and allowed_to_change_status else None),
1201 1201 comment_type=comment_type,
1202 1202 resolves_comment_id=resolves_comment_id
1203 1203 )
1204 1204
1205 1205 if allowed_to_change_status:
1206 1206 # calculate old status before we change it
1207 1207 old_calculated_status = pull_request.calculated_review_status()
1208 1208
1209 1209 # get status if set !
1210 1210 if status:
1211 1211 ChangesetStatusModel().set_status(
1212 1212 self.db_repo.repo_id,
1213 1213 status,
1214 1214 self._rhodecode_user.user_id,
1215 1215 comment,
1216 1216 pull_request=pull_request
1217 1217 )
1218 1218
1219 1219 Session().flush()
1220 1220 # this is somehow required to get access to some relationship
1221 1221 # loaded on comment
1222 1222 Session().refresh(comment)
1223 1223
1224 1224 events.trigger(
1225 1225 events.PullRequestCommentEvent(pull_request, comment))
1226 1226
1227 1227 # we now calculate the status of pull request, and based on that
1228 1228 # calculation we set the commits status
1229 1229 calculated_status = pull_request.calculated_review_status()
1230 1230 if old_calculated_status != calculated_status:
1231 1231 PullRequestModel()._trigger_pull_request_hook(
1232 1232 pull_request, self._rhodecode_user, 'review_status_change')
1233 1233
1234 1234 Session().commit()
1235 1235
1236 1236 data = {
1237 1237 'target_id': h.safeid(h.safe_unicode(
1238 1238 self.request.POST.get('f_path'))),
1239 1239 }
1240 1240 if comment:
1241 1241 c.co = comment
1242 1242 rendered_comment = render(
1243 1243 'rhodecode:templates/changeset/changeset_comment_block.mako',
1244 1244 self._get_template_context(c), self.request)
1245 1245
1246 1246 data.update(comment.get_dict())
1247 1247 data.update({'rendered_text': rendered_comment})
1248 1248
1249 1249 return data
1250 1250
1251 1251 @LoginRequired()
1252 1252 @NotAnonymous()
1253 1253 @HasRepoPermissionAnyDecorator(
1254 1254 'repository.read', 'repository.write', 'repository.admin')
1255 1255 @CSRFRequired()
1256 1256 @view_config(
1257 1257 route_name='pullrequest_comment_delete', request_method='POST',
1258 1258 renderer='json_ext')
1259 1259 def pull_request_comment_delete(self):
1260 1260 pull_request = PullRequest.get_or_404(
1261 1261 self.request.matchdict['pull_request_id'])
1262 1262
1263 1263 comment = ChangesetComment.get_or_404(
1264 1264 self.request.matchdict['comment_id'])
1265 1265 comment_id = comment.comment_id
1266 1266
1267 1267 if pull_request.is_closed():
1268 1268 log.debug('comment: forbidden because pull request is closed')
1269 1269 raise HTTPForbidden()
1270 1270
1271 1271 if not comment:
1272 1272 log.debug('Comment with id:%s not found, skipping', comment_id)
1273 1273 # comment already deleted in another call probably
1274 1274 return True
1275 1275
1276 1276 if comment.pull_request.is_closed():
1277 1277 # don't allow deleting comments on closed pull request
1278 1278 raise HTTPForbidden()
1279 1279
1280 1280 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1281 1281 super_admin = h.HasPermissionAny('hg.admin')()
1282 1282 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1283 1283 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1284 1284 comment_repo_admin = is_repo_admin and is_repo_comment
1285 1285
1286 1286 if super_admin or comment_owner or comment_repo_admin:
1287 1287 old_calculated_status = comment.pull_request.calculated_review_status()
1288 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1288 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1289 1289 Session().commit()
1290 1290 calculated_status = comment.pull_request.calculated_review_status()
1291 1291 if old_calculated_status != calculated_status:
1292 1292 PullRequestModel()._trigger_pull_request_hook(
1293 1293 comment.pull_request, self._rhodecode_user, 'review_status_change')
1294 1294 return True
1295 1295 else:
1296 1296 log.warning('No permissions for user %s to delete comment_id: %s',
1297 1297 self._rhodecode_db_user, comment_id)
1298 1298 raise HTTPNotFound()
@@ -1,660 +1,662 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 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 comments model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import collections
28 28
29 29 from pyramid.threadlocal import get_current_registry, get_current_request
30 30 from sqlalchemy.sql.expression import null
31 31 from sqlalchemy.sql.functions import coalesce
32 32
33 33 from rhodecode.lib import helpers as h, diffs, channelstream
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 36 from rhodecode.model import BaseModel
37 37 from rhodecode.model.db import (
38 38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
39 39 from rhodecode.model.notification import NotificationModel
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.settings import VcsSettingsModel
42 42 from rhodecode.model.notification import EmailNotificationModel
43 43 from rhodecode.model.validation_schema.schemas import comment_schema
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class CommentsModel(BaseModel):
50 50
51 51 cls = ChangesetComment
52 52
53 53 DIFF_CONTEXT_BEFORE = 3
54 54 DIFF_CONTEXT_AFTER = 3
55 55
56 56 def __get_commit_comment(self, changeset_comment):
57 57 return self._get_instance(ChangesetComment, changeset_comment)
58 58
59 59 def __get_pull_request(self, pull_request):
60 60 return self._get_instance(PullRequest, pull_request)
61 61
62 62 def _extract_mentions(self, s):
63 63 user_objects = []
64 64 for username in extract_mentioned_users(s):
65 65 user_obj = User.get_by_username(username, case_insensitive=True)
66 66 if user_obj:
67 67 user_objects.append(user_obj)
68 68 return user_objects
69 69
70 70 def _get_renderer(self, global_renderer='rst', request=None):
71 71 request = request or get_current_request()
72 72
73 73 try:
74 74 global_renderer = request.call_context.visual.default_renderer
75 75 except AttributeError:
76 76 log.debug("Renderer not set, falling back "
77 77 "to default renderer '%s'", global_renderer)
78 78 except Exception:
79 79 log.error(traceback.format_exc())
80 80 return global_renderer
81 81
82 82 def aggregate_comments(self, comments, versions, show_version, inline=False):
83 83 # group by versions, and count until, and display objects
84 84
85 85 comment_groups = collections.defaultdict(list)
86 86 [comment_groups[
87 87 _co.pull_request_version_id].append(_co) for _co in comments]
88 88
89 89 def yield_comments(pos):
90 90 for co in comment_groups[pos]:
91 91 yield co
92 92
93 93 comment_versions = collections.defaultdict(
94 94 lambda: collections.defaultdict(list))
95 95 prev_prvid = -1
96 96 # fake last entry with None, to aggregate on "latest" version which
97 97 # doesn't have an pull_request_version_id
98 98 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
99 99 prvid = ver.pull_request_version_id
100 100 if prev_prvid == -1:
101 101 prev_prvid = prvid
102 102
103 103 for co in yield_comments(prvid):
104 104 comment_versions[prvid]['at'].append(co)
105 105
106 106 # save until
107 107 current = comment_versions[prvid]['at']
108 108 prev_until = comment_versions[prev_prvid]['until']
109 109 cur_until = prev_until + current
110 110 comment_versions[prvid]['until'].extend(cur_until)
111 111
112 112 # save outdated
113 113 if inline:
114 114 outdated = [x for x in cur_until
115 115 if x.outdated_at_version(show_version)]
116 116 else:
117 117 outdated = [x for x in cur_until
118 118 if x.older_than_version(show_version)]
119 119 display = [x for x in cur_until if x not in outdated]
120 120
121 121 comment_versions[prvid]['outdated'] = outdated
122 122 comment_versions[prvid]['display'] = display
123 123
124 124 prev_prvid = prvid
125 125
126 126 return comment_versions
127 127
128 128 def get_unresolved_todos(self, pull_request, show_outdated=True):
129 129
130 130 todos = Session().query(ChangesetComment) \
131 131 .filter(ChangesetComment.pull_request == pull_request) \
132 132 .filter(ChangesetComment.resolved_by == None) \
133 133 .filter(ChangesetComment.comment_type
134 134 == ChangesetComment.COMMENT_TYPE_TODO)
135 135
136 136 if not show_outdated:
137 137 todos = todos.filter(
138 138 coalesce(ChangesetComment.display_state, '') !=
139 139 ChangesetComment.COMMENT_OUTDATED)
140 140
141 141 todos = todos.all()
142 142
143 143 return todos
144 144
145 145 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
146 146
147 147 todos = Session().query(ChangesetComment) \
148 148 .filter(ChangesetComment.revision == commit_id) \
149 149 .filter(ChangesetComment.resolved_by == None) \
150 150 .filter(ChangesetComment.comment_type
151 151 == ChangesetComment.COMMENT_TYPE_TODO)
152 152
153 153 if not show_outdated:
154 154 todos = todos.filter(
155 155 coalesce(ChangesetComment.display_state, '') !=
156 156 ChangesetComment.COMMENT_OUTDATED)
157 157
158 158 todos = todos.all()
159 159
160 160 return todos
161 161
162 def _log_audit_action(self, action, action_data, user, comment):
162 def _log_audit_action(self, action, action_data, auth_user, comment):
163 163 audit_logger.store(
164 164 action=action,
165 165 action_data=action_data,
166 user=user,
166 user=auth_user,
167 167 repo=comment.repo)
168 168
169 169 def create(self, text, repo, user, commit_id=None, pull_request=None,
170 170 f_path=None, line_no=None, status_change=None,
171 171 status_change_type=None, comment_type=None,
172 172 resolves_comment_id=None, closing_pr=False, send_email=True,
173 renderer=None):
173 renderer=None, auth_user=None):
174 174 """
175 175 Creates new comment for commit or pull request.
176 176 IF status_change is not none this comment is associated with a
177 177 status change of commit or commit associated with pull request
178 178
179 179 :param text:
180 180 :param repo:
181 181 :param user:
182 182 :param commit_id:
183 183 :param pull_request:
184 184 :param f_path:
185 185 :param line_no:
186 186 :param status_change: Label for status change
187 187 :param comment_type: Type of comment
188 188 :param status_change_type: type of status change
189 189 :param closing_pr:
190 190 :param send_email:
191 191 :param renderer: pick renderer for this comment
192 192 """
193
194 auth_user = auth_user or user
193 195 if not text:
194 196 log.warning('Missing text for comment, skipping...')
195 197 return
196 198 request = get_current_request()
197 199 _ = request.translate
198 200
199 201 if not renderer:
200 202 renderer = self._get_renderer(request=request)
201 203
202 204 repo = self._get_repo(repo)
203 205 user = self._get_user(user)
204 206
205 207 schema = comment_schema.CommentSchema()
206 208 validated_kwargs = schema.deserialize(dict(
207 209 comment_body=text,
208 210 comment_type=comment_type,
209 211 comment_file=f_path,
210 212 comment_line=line_no,
211 213 renderer_type=renderer,
212 214 status_change=status_change_type,
213 215 resolves_comment_id=resolves_comment_id,
214 216 repo=repo.repo_id,
215 217 user=user.user_id,
216 218 ))
217 219
218 220 comment = ChangesetComment()
219 221 comment.renderer = validated_kwargs['renderer_type']
220 222 comment.text = validated_kwargs['comment_body']
221 223 comment.f_path = validated_kwargs['comment_file']
222 224 comment.line_no = validated_kwargs['comment_line']
223 225 comment.comment_type = validated_kwargs['comment_type']
224 226
225 227 comment.repo = repo
226 228 comment.author = user
227 229 resolved_comment = self.__get_commit_comment(
228 230 validated_kwargs['resolves_comment_id'])
229 231 # check if the comment actually belongs to this PR
230 232 if resolved_comment and resolved_comment.pull_request and \
231 233 resolved_comment.pull_request != pull_request:
232 234 # comment not bound to this pull request, forbid
233 235 resolved_comment = None
234 236 comment.resolved_comment = resolved_comment
235 237
236 238 pull_request_id = pull_request
237 239
238 240 commit_obj = None
239 241 pull_request_obj = None
240 242
241 243 if commit_id:
242 244 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
243 245 # do a lookup, so we don't pass something bad here
244 246 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
245 247 comment.revision = commit_obj.raw_id
246 248
247 249 elif pull_request_id:
248 250 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
249 251 pull_request_obj = self.__get_pull_request(pull_request_id)
250 252 comment.pull_request = pull_request_obj
251 253 else:
252 254 raise Exception('Please specify commit or pull_request_id')
253 255
254 256 Session().add(comment)
255 257 Session().flush()
256 258 kwargs = {
257 259 'user': user,
258 260 'renderer_type': renderer,
259 261 'repo_name': repo.repo_name,
260 262 'status_change': status_change,
261 263 'status_change_type': status_change_type,
262 264 'comment_body': text,
263 265 'comment_file': f_path,
264 266 'comment_line': line_no,
265 267 'comment_type': comment_type or 'note'
266 268 }
267 269
268 270 if commit_obj:
269 271 recipients = ChangesetComment.get_users(
270 272 revision=commit_obj.raw_id)
271 273 # add commit author if it's in RhodeCode system
272 274 cs_author = User.get_from_cs_author(commit_obj.author)
273 275 if not cs_author:
274 276 # use repo owner if we cannot extract the author correctly
275 277 cs_author = repo.user
276 278 recipients += [cs_author]
277 279
278 280 commit_comment_url = self.get_url(comment, request=request)
279 281
280 282 target_repo_url = h.link_to(
281 283 repo.repo_name,
282 284 h.route_url('repo_summary', repo_name=repo.repo_name))
283 285
284 286 # commit specifics
285 287 kwargs.update({
286 288 'commit': commit_obj,
287 289 'commit_message': commit_obj.message,
288 290 'commit_target_repo': target_repo_url,
289 291 'commit_comment_url': commit_comment_url,
290 292 })
291 293
292 294 elif pull_request_obj:
293 295 # get the current participants of this pull request
294 296 recipients = ChangesetComment.get_users(
295 297 pull_request_id=pull_request_obj.pull_request_id)
296 298 # add pull request author
297 299 recipients += [pull_request_obj.author]
298 300
299 301 # add the reviewers to notification
300 302 recipients += [x.user for x in pull_request_obj.reviewers]
301 303
302 304 pr_target_repo = pull_request_obj.target_repo
303 305 pr_source_repo = pull_request_obj.source_repo
304 306
305 307 pr_comment_url = h.route_url(
306 308 'pullrequest_show',
307 309 repo_name=pr_target_repo.repo_name,
308 310 pull_request_id=pull_request_obj.pull_request_id,
309 311 _anchor='comment-%s' % comment.comment_id)
310 312
311 313 # set some variables for email notification
312 314 pr_target_repo_url = h.route_url(
313 315 'repo_summary', repo_name=pr_target_repo.repo_name)
314 316
315 317 pr_source_repo_url = h.route_url(
316 318 'repo_summary', repo_name=pr_source_repo.repo_name)
317 319
318 320 # pull request specifics
319 321 kwargs.update({
320 322 'pull_request': pull_request_obj,
321 323 'pr_id': pull_request_obj.pull_request_id,
322 324 'pr_target_repo': pr_target_repo,
323 325 'pr_target_repo_url': pr_target_repo_url,
324 326 'pr_source_repo': pr_source_repo,
325 327 'pr_source_repo_url': pr_source_repo_url,
326 328 'pr_comment_url': pr_comment_url,
327 329 'pr_closing': closing_pr,
328 330 })
329 331 if send_email:
330 332 # pre-generate the subject for notification itself
331 333 (subject,
332 334 _h, _e, # we don't care about those
333 335 body_plaintext) = EmailNotificationModel().render_email(
334 336 notification_type, **kwargs)
335 337
336 338 mention_recipients = set(
337 339 self._extract_mentions(text)).difference(recipients)
338 340
339 341 # create notification objects, and emails
340 342 NotificationModel().create(
341 343 created_by=user,
342 344 notification_subject=subject,
343 345 notification_body=body_plaintext,
344 346 notification_type=notification_type,
345 347 recipients=recipients,
346 348 mention_recipients=mention_recipients,
347 349 email_kwargs=kwargs,
348 350 )
349 351
350 352 Session().flush()
351 353 if comment.pull_request:
352 354 action = 'repo.pull_request.comment.create'
353 355 else:
354 356 action = 'repo.commit.comment.create'
355 357
356 358 comment_data = comment.get_api_data()
357 359 self._log_audit_action(
358 action, {'data': comment_data}, user, comment)
360 action, {'data': comment_data}, auth_user, comment)
359 361
360 362 msg_url = ''
361 363 channel = None
362 364 if commit_obj:
363 365 msg_url = commit_comment_url
364 366 repo_name = repo.repo_name
365 367 channel = u'/repo${}$/commit/{}'.format(
366 368 repo_name,
367 369 commit_obj.raw_id
368 370 )
369 371 elif pull_request_obj:
370 372 msg_url = pr_comment_url
371 373 repo_name = pr_target_repo.repo_name
372 374 channel = u'/repo${}$/pr/{}'.format(
373 375 repo_name,
374 376 pull_request_id
375 377 )
376 378
377 379 message = '<strong>{}</strong> {} - ' \
378 380 '<a onclick="window.location=\'{}\';' \
379 381 'window.location.reload()">' \
380 382 '<strong>{}</strong></a>'
381 383 message = message.format(
382 384 user.username, _('made a comment'), msg_url,
383 385 _('Show it now'))
384 386
385 387 channelstream.post_message(
386 388 channel, message, user.username,
387 389 registry=get_current_registry())
388 390
389 391 return comment
390 392
391 def delete(self, comment, user):
393 def delete(self, comment, auth_user):
392 394 """
393 395 Deletes given comment
394 396 """
395 397 comment = self.__get_commit_comment(comment)
396 398 old_data = comment.get_api_data()
397 399 Session().delete(comment)
398 400
399 401 if comment.pull_request:
400 402 action = 'repo.pull_request.comment.delete'
401 403 else:
402 404 action = 'repo.commit.comment.delete'
403 405
404 406 self._log_audit_action(
405 action, {'old_data': old_data}, user, comment)
407 action, {'old_data': old_data}, auth_user, comment)
406 408
407 409 return comment
408 410
409 411 def get_all_comments(self, repo_id, revision=None, pull_request=None):
410 412 q = ChangesetComment.query()\
411 413 .filter(ChangesetComment.repo_id == repo_id)
412 414 if revision:
413 415 q = q.filter(ChangesetComment.revision == revision)
414 416 elif pull_request:
415 417 pull_request = self.__get_pull_request(pull_request)
416 418 q = q.filter(ChangesetComment.pull_request == pull_request)
417 419 else:
418 420 raise Exception('Please specify commit or pull_request')
419 421 q = q.order_by(ChangesetComment.created_on)
420 422 return q.all()
421 423
422 424 def get_url(self, comment, request=None, permalink=False):
423 425 if not request:
424 426 request = get_current_request()
425 427
426 428 comment = self.__get_commit_comment(comment)
427 429 if comment.pull_request:
428 430 pull_request = comment.pull_request
429 431 if permalink:
430 432 return request.route_url(
431 433 'pull_requests_global',
432 434 pull_request_id=pull_request.pull_request_id,
433 435 _anchor='comment-%s' % comment.comment_id)
434 436 else:
435 437 return request.route_url(
436 438 'pullrequest_show',
437 439 repo_name=safe_str(pull_request.target_repo.repo_name),
438 440 pull_request_id=pull_request.pull_request_id,
439 441 _anchor='comment-%s' % comment.comment_id)
440 442
441 443 else:
442 444 repo = comment.repo
443 445 commit_id = comment.revision
444 446
445 447 if permalink:
446 448 return request.route_url(
447 449 'repo_commit', repo_name=safe_str(repo.repo_id),
448 450 commit_id=commit_id,
449 451 _anchor='comment-%s' % comment.comment_id)
450 452
451 453 else:
452 454 return request.route_url(
453 455 'repo_commit', repo_name=safe_str(repo.repo_name),
454 456 commit_id=commit_id,
455 457 _anchor='comment-%s' % comment.comment_id)
456 458
457 459 def get_comments(self, repo_id, revision=None, pull_request=None):
458 460 """
459 461 Gets main comments based on revision or pull_request_id
460 462
461 463 :param repo_id:
462 464 :param revision:
463 465 :param pull_request:
464 466 """
465 467
466 468 q = ChangesetComment.query()\
467 469 .filter(ChangesetComment.repo_id == repo_id)\
468 470 .filter(ChangesetComment.line_no == None)\
469 471 .filter(ChangesetComment.f_path == None)
470 472 if revision:
471 473 q = q.filter(ChangesetComment.revision == revision)
472 474 elif pull_request:
473 475 pull_request = self.__get_pull_request(pull_request)
474 476 q = q.filter(ChangesetComment.pull_request == pull_request)
475 477 else:
476 478 raise Exception('Please specify commit or pull_request')
477 479 q = q.order_by(ChangesetComment.created_on)
478 480 return q.all()
479 481
480 482 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
481 483 q = self._get_inline_comments_query(repo_id, revision, pull_request)
482 484 return self._group_comments_by_path_and_line_number(q)
483 485
484 486 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
485 487 version=None):
486 488 inline_cnt = 0
487 489 for fname, per_line_comments in inline_comments.iteritems():
488 490 for lno, comments in per_line_comments.iteritems():
489 491 for comm in comments:
490 492 if not comm.outdated_at_version(version) and skip_outdated:
491 493 inline_cnt += 1
492 494
493 495 return inline_cnt
494 496
495 497 def get_outdated_comments(self, repo_id, pull_request):
496 498 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
497 499 # of a pull request.
498 500 q = self._all_inline_comments_of_pull_request(pull_request)
499 501 q = q.filter(
500 502 ChangesetComment.display_state ==
501 503 ChangesetComment.COMMENT_OUTDATED
502 504 ).order_by(ChangesetComment.comment_id.asc())
503 505
504 506 return self._group_comments_by_path_and_line_number(q)
505 507
506 508 def _get_inline_comments_query(self, repo_id, revision, pull_request):
507 509 # TODO: johbo: Split this into two methods: One for PR and one for
508 510 # commit.
509 511 if revision:
510 512 q = Session().query(ChangesetComment).filter(
511 513 ChangesetComment.repo_id == repo_id,
512 514 ChangesetComment.line_no != null(),
513 515 ChangesetComment.f_path != null(),
514 516 ChangesetComment.revision == revision)
515 517
516 518 elif pull_request:
517 519 pull_request = self.__get_pull_request(pull_request)
518 520 if not CommentsModel.use_outdated_comments(pull_request):
519 521 q = self._visible_inline_comments_of_pull_request(pull_request)
520 522 else:
521 523 q = self._all_inline_comments_of_pull_request(pull_request)
522 524
523 525 else:
524 526 raise Exception('Please specify commit or pull_request_id')
525 527 q = q.order_by(ChangesetComment.comment_id.asc())
526 528 return q
527 529
528 530 def _group_comments_by_path_and_line_number(self, q):
529 531 comments = q.all()
530 532 paths = collections.defaultdict(lambda: collections.defaultdict(list))
531 533 for co in comments:
532 534 paths[co.f_path][co.line_no].append(co)
533 535 return paths
534 536
535 537 @classmethod
536 538 def needed_extra_diff_context(cls):
537 539 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
538 540
539 541 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
540 542 if not CommentsModel.use_outdated_comments(pull_request):
541 543 return
542 544
543 545 comments = self._visible_inline_comments_of_pull_request(pull_request)
544 546 comments_to_outdate = comments.all()
545 547
546 548 for comment in comments_to_outdate:
547 549 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
548 550
549 551 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
550 552 diff_line = _parse_comment_line_number(comment.line_no)
551 553
552 554 try:
553 555 old_context = old_diff_proc.get_context_of_line(
554 556 path=comment.f_path, diff_line=diff_line)
555 557 new_context = new_diff_proc.get_context_of_line(
556 558 path=comment.f_path, diff_line=diff_line)
557 559 except (diffs.LineNotInDiffException,
558 560 diffs.FileNotInDiffException):
559 561 comment.display_state = ChangesetComment.COMMENT_OUTDATED
560 562 return
561 563
562 564 if old_context == new_context:
563 565 return
564 566
565 567 if self._should_relocate_diff_line(diff_line):
566 568 new_diff_lines = new_diff_proc.find_context(
567 569 path=comment.f_path, context=old_context,
568 570 offset=self.DIFF_CONTEXT_BEFORE)
569 571 if not new_diff_lines:
570 572 comment.display_state = ChangesetComment.COMMENT_OUTDATED
571 573 else:
572 574 new_diff_line = self._choose_closest_diff_line(
573 575 diff_line, new_diff_lines)
574 576 comment.line_no = _diff_to_comment_line_number(new_diff_line)
575 577 else:
576 578 comment.display_state = ChangesetComment.COMMENT_OUTDATED
577 579
578 580 def _should_relocate_diff_line(self, diff_line):
579 581 """
580 582 Checks if relocation shall be tried for the given `diff_line`.
581 583
582 584 If a comment points into the first lines, then we can have a situation
583 585 that after an update another line has been added on top. In this case
584 586 we would find the context still and move the comment around. This
585 587 would be wrong.
586 588 """
587 589 should_relocate = (
588 590 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
589 591 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
590 592 return should_relocate
591 593
592 594 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
593 595 candidate = new_diff_lines[0]
594 596 best_delta = _diff_line_delta(diff_line, candidate)
595 597 for new_diff_line in new_diff_lines[1:]:
596 598 delta = _diff_line_delta(diff_line, new_diff_line)
597 599 if delta < best_delta:
598 600 candidate = new_diff_line
599 601 best_delta = delta
600 602 return candidate
601 603
602 604 def _visible_inline_comments_of_pull_request(self, pull_request):
603 605 comments = self._all_inline_comments_of_pull_request(pull_request)
604 606 comments = comments.filter(
605 607 coalesce(ChangesetComment.display_state, '') !=
606 608 ChangesetComment.COMMENT_OUTDATED)
607 609 return comments
608 610
609 611 def _all_inline_comments_of_pull_request(self, pull_request):
610 612 comments = Session().query(ChangesetComment)\
611 613 .filter(ChangesetComment.line_no != None)\
612 614 .filter(ChangesetComment.f_path != None)\
613 615 .filter(ChangesetComment.pull_request == pull_request)
614 616 return comments
615 617
616 618 def _all_general_comments_of_pull_request(self, pull_request):
617 619 comments = Session().query(ChangesetComment)\
618 620 .filter(ChangesetComment.line_no == None)\
619 621 .filter(ChangesetComment.f_path == None)\
620 622 .filter(ChangesetComment.pull_request == pull_request)
621 623 return comments
622 624
623 625 @staticmethod
624 626 def use_outdated_comments(pull_request):
625 627 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
626 628 settings = settings_model.get_general_settings()
627 629 return settings.get('rhodecode_use_outdated_comments', False)
628 630
629 631
630 632 def _parse_comment_line_number(line_no):
631 633 """
632 634 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
633 635 """
634 636 old_line = None
635 637 new_line = None
636 638 if line_no.startswith('o'):
637 639 old_line = int(line_no[1:])
638 640 elif line_no.startswith('n'):
639 641 new_line = int(line_no[1:])
640 642 else:
641 643 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
642 644 return diffs.DiffLineNumber(old_line, new_line)
643 645
644 646
645 647 def _diff_to_comment_line_number(diff_line):
646 648 if diff_line.new is not None:
647 649 return u'n{}'.format(diff_line.new)
648 650 elif diff_line.old is not None:
649 651 return u'o{}'.format(diff_line.old)
650 652 return u''
651 653
652 654
653 655 def _diff_line_delta(a, b):
654 656 if None not in (a.new, b.new):
655 657 return abs(a.new - b.new)
656 658 elif None not in (a.old, b.old):
657 659 return abs(a.old - b.old)
658 660 else:
659 661 raise ValueError(
660 662 "Cannot compute delta between {} and {}".format(a, b))
General Comments 0
You need to be logged in to leave comments. Login now