Show More
@@ -43,6 +43,7 b' class TestClosePullRequest(object):' | |||||
43 | response = api_call(self.app, params) |
|
43 | response = api_call(self.app, params) | |
44 | expected = { |
|
44 | expected = { | |
45 | 'pull_request_id': pull_request_id, |
|
45 | 'pull_request_id': pull_request_id, | |
|
46 | 'close_status': 'Rejected', | |||
46 | 'closed': True, |
|
47 | 'closed': True, | |
47 | } |
|
48 | } | |
48 | assert_ok(id_, expected, response.body) |
|
49 | assert_ok(id_, expected, response.body) |
@@ -21,6 +21,7 b'' | |||||
21 |
|
21 | |||
22 | import logging |
|
22 | import logging | |
23 |
|
23 | |||
|
24 | from rhodecode import events | |||
24 | from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError |
|
25 | from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError | |
25 | from rhodecode.api.utils import ( |
|
26 | from rhodecode.api.utils import ( | |
26 | has_superadmin_permission, Optional, OAttr, get_repo_or_error, |
|
27 | has_superadmin_permission, Optional, OAttr, get_repo_or_error, | |
@@ -35,8 +36,8 b' from rhodecode.model.db import Session, ' | |||||
35 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck |
|
36 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck | |
36 | from rhodecode.model.settings import SettingsModel |
|
37 | from rhodecode.model.settings import SettingsModel | |
37 | from rhodecode.model.validation_schema import Invalid |
|
38 | from rhodecode.model.validation_schema import Invalid | |
38 |
from rhodecode.model.validation_schema.schemas.reviewer_schema import |
|
39 | from rhodecode.model.validation_schema.schemas.reviewer_schema import( | |
39 | ReviewerListSchema |
|
40 | ReviewerListSchema) | |
40 |
|
41 | |||
41 | log = logging.getLogger(__name__) |
|
42 | log = logging.getLogger(__name__) | |
42 |
|
43 | |||
@@ -227,7 +228,8 b' def get_pull_requests(request, apiuser, ' | |||||
227 |
|
228 | |||
228 |
|
229 | |||
229 | @jsonrpc_method() |
|
230 | @jsonrpc_method() | |
230 | def merge_pull_request(request, apiuser, repoid, pullrequestid, |
|
231 | def merge_pull_request( | |
|
232 | request, apiuser, repoid, pullrequestid, | |||
231 |
|
|
233 | userid=Optional(OAttr('apiuser'))): | |
232 | """ |
|
234 | """ | |
233 | Merge the pull request specified by `pullrequestid` into its target |
|
235 | Merge the pull request specified by `pullrequestid` into its target | |
@@ -308,63 +310,6 b' def merge_pull_request(request, apiuser,' | |||||
308 |
|
310 | |||
309 |
|
311 | |||
310 | @jsonrpc_method() |
|
312 | @jsonrpc_method() | |
311 | def close_pull_request(request, apiuser, repoid, pullrequestid, |
|
|||
312 | userid=Optional(OAttr('apiuser'))): |
|
|||
313 | """ |
|
|||
314 | Close the pull request specified by `pullrequestid`. |
|
|||
315 |
|
||||
316 | :param apiuser: This is filled automatically from the |authtoken|. |
|
|||
317 | :type apiuser: AuthUser |
|
|||
318 | :param repoid: Repository name or repository ID to which the pull |
|
|||
319 | request belongs. |
|
|||
320 | :type repoid: str or int |
|
|||
321 | :param pullrequestid: ID of the pull request to be closed. |
|
|||
322 | :type pullrequestid: int |
|
|||
323 | :param userid: Close the pull request as this user. |
|
|||
324 | :type userid: Optional(str or int) |
|
|||
325 |
|
||||
326 | Example output: |
|
|||
327 |
|
||||
328 | .. code-block:: bash |
|
|||
329 |
|
||||
330 | "id": <id_given_in_input>, |
|
|||
331 | "result": { |
|
|||
332 | "pull_request_id": "<int>", |
|
|||
333 | "closed": "<bool>" |
|
|||
334 | }, |
|
|||
335 | "error": null |
|
|||
336 |
|
||||
337 | """ |
|
|||
338 | repo = get_repo_or_error(repoid) |
|
|||
339 | if not isinstance(userid, Optional): |
|
|||
340 | if (has_superadmin_permission(apiuser) or |
|
|||
341 | HasRepoPermissionAnyApi('repository.admin')( |
|
|||
342 | user=apiuser, repo_name=repo.repo_name)): |
|
|||
343 | apiuser = get_user_or_error(userid) |
|
|||
344 | else: |
|
|||
345 | raise JSONRPCError('userid is not the same as your user') |
|
|||
346 |
|
||||
347 | pull_request = get_pull_request_or_error(pullrequestid) |
|
|||
348 | if not PullRequestModel().check_user_update( |
|
|||
349 | pull_request, apiuser, api=True): |
|
|||
350 | raise JSONRPCError( |
|
|||
351 | 'pull request `%s` close failed, no permission to close.' % ( |
|
|||
352 | pullrequestid,)) |
|
|||
353 | if pull_request.is_closed(): |
|
|||
354 | raise JSONRPCError( |
|
|||
355 | 'pull request `%s` is already closed' % (pullrequestid,)) |
|
|||
356 |
|
||||
357 | PullRequestModel().close_pull_request( |
|
|||
358 | pull_request.pull_request_id, apiuser) |
|
|||
359 | Session().commit() |
|
|||
360 | data = { |
|
|||
361 | 'pull_request_id': pull_request.pull_request_id, |
|
|||
362 | 'closed': True, |
|
|||
363 | } |
|
|||
364 | return data |
|
|||
365 |
|
||||
366 |
|
||||
367 | @jsonrpc_method() |
|
|||
368 | def comment_pull_request( |
|
313 | def comment_pull_request( | |
369 | request, apiuser, repoid, pullrequestid, message=Optional(None), |
|
314 | request, apiuser, repoid, pullrequestid, message=Optional(None), | |
370 | commit_id=Optional(None), status=Optional(None), |
|
315 | commit_id=Optional(None), status=Optional(None), | |
@@ -610,7 +555,7 b' def create_pull_request(' | |||||
610 | def update_pull_request( |
|
555 | def update_pull_request( | |
611 | request, apiuser, repoid, pullrequestid, title=Optional(''), |
|
556 | request, apiuser, repoid, pullrequestid, title=Optional(''), | |
612 | description=Optional(''), reviewers=Optional(None), |
|
557 | description=Optional(''), reviewers=Optional(None), | |
613 |
update_commits=Optional(None) |
|
558 | update_commits=Optional(None)): | |
614 | """ |
|
559 | """ | |
615 | Updates a pull request. |
|
560 | Updates a pull request. | |
616 |
|
561 | |||
@@ -632,8 +577,6 b' def update_pull_request(' | |||||
632 |
|
577 | |||
633 | :param update_commits: Trigger update of commits for this pull request |
|
578 | :param update_commits: Trigger update of commits for this pull request | |
634 | :type: update_commits: Optional(bool) |
|
579 | :type: update_commits: Optional(bool) | |
635 | :param close_pull_request: Close this pull request with rejected state |
|
|||
636 | :type: close_pull_request: Optional(bool) |
|
|||
637 |
|
580 | |||
638 | Example output: |
|
581 | Example output: | |
639 |
|
582 | |||
@@ -675,7 +618,6 b' def update_pull_request(' | |||||
675 | 'pull request `%s` update failed, pull request is closed' % ( |
|
618 | 'pull request `%s` update failed, pull request is closed' % ( | |
676 | pullrequestid,)) |
|
619 | pullrequestid,)) | |
677 |
|
620 | |||
678 |
|
||||
679 | reviewer_objects = Optional.extract(reviewers) or [] |
|
621 | reviewer_objects = Optional.extract(reviewers) or [] | |
680 | if reviewer_objects: |
|
622 | if reviewer_objects: | |
681 | schema = ReviewerListSchema() |
|
623 | schema = ReviewerListSchema() | |
@@ -718,11 +660,6 b' def update_pull_request(' | |||||
718 | [get_user_or_error(n).username for n in removed_reviewers]) |
|
660 | [get_user_or_error(n).username for n in removed_reviewers]) | |
719 | Session().commit() |
|
661 | Session().commit() | |
720 |
|
662 | |||
721 | if str2bool(Optional.extract(close_pull_request)): |
|
|||
722 | PullRequestModel().close_pull_request_with_comment( |
|
|||
723 | pull_request, apiuser, repo) |
|
|||
724 | Session().commit() |
|
|||
725 |
|
||||
726 | data = { |
|
663 | data = { | |
727 | 'msg': 'Updated pull request `{}`'.format( |
|
664 | 'msg': 'Updated pull request `{}`'.format( | |
728 | pull_request.pull_request_id), |
|
665 | pull_request.pull_request_id), | |
@@ -732,3 +669,80 b' def update_pull_request(' | |||||
732 | } |
|
669 | } | |
733 |
|
670 | |||
734 | return data |
|
671 | return data | |
|
672 | ||||
|
673 | ||||
|
674 | @jsonrpc_method() | |||
|
675 | def close_pull_request( | |||
|
676 | request, apiuser, repoid, pullrequestid, | |||
|
677 | userid=Optional(OAttr('apiuser')), message=Optional('')): | |||
|
678 | """ | |||
|
679 | Close the pull request specified by `pullrequestid`. | |||
|
680 | ||||
|
681 | :param apiuser: This is filled automatically from the |authtoken|. | |||
|
682 | :type apiuser: AuthUser | |||
|
683 | :param repoid: Repository name or repository ID to which the pull | |||
|
684 | request belongs. | |||
|
685 | :type repoid: str or int | |||
|
686 | :param pullrequestid: ID of the pull request to be closed. | |||
|
687 | :type pullrequestid: int | |||
|
688 | :param userid: Close the pull request as this user. | |||
|
689 | :type userid: Optional(str or int) | |||
|
690 | :param message: Optional message to close the Pull Request with. If not | |||
|
691 | specified it will be generated automatically. | |||
|
692 | :type message: Optional(str) | |||
|
693 | ||||
|
694 | Example output: | |||
|
695 | ||||
|
696 | .. code-block:: bash | |||
|
697 | ||||
|
698 | "id": <id_given_in_input>, | |||
|
699 | "result": { | |||
|
700 | "pull_request_id": "<int>", | |||
|
701 | "close_status": "<str:status_lbl>, | |||
|
702 | "closed": "<bool>" | |||
|
703 | }, | |||
|
704 | "error": null | |||
|
705 | ||||
|
706 | """ | |||
|
707 | _ = request.translate | |||
|
708 | ||||
|
709 | repo = get_repo_or_error(repoid) | |||
|
710 | if not isinstance(userid, Optional): | |||
|
711 | if (has_superadmin_permission(apiuser) or | |||
|
712 | HasRepoPermissionAnyApi('repository.admin')( | |||
|
713 | user=apiuser, repo_name=repo.repo_name)): | |||
|
714 | apiuser = get_user_or_error(userid) | |||
|
715 | else: | |||
|
716 | raise JSONRPCError('userid is not the same as your user') | |||
|
717 | ||||
|
718 | pull_request = get_pull_request_or_error(pullrequestid) | |||
|
719 | ||||
|
720 | if pull_request.is_closed(): | |||
|
721 | raise JSONRPCError( | |||
|
722 | 'pull request `%s` is already closed' % (pullrequestid,)) | |||
|
723 | ||||
|
724 | # only owner or admin or person with write permissions | |||
|
725 | allowed_to_close = PullRequestModel().check_user_update( | |||
|
726 | pull_request, apiuser, api=True) | |||
|
727 | ||||
|
728 | if not allowed_to_close: | |||
|
729 | raise JSONRPCError( | |||
|
730 | 'pull request `%s` close failed, no permission to close.' % ( | |||
|
731 | pullrequestid,)) | |||
|
732 | ||||
|
733 | # message we're using to close the PR, else it's automatically generated | |||
|
734 | message = Optional.extract(message) | |||
|
735 | ||||
|
736 | # finally close the PR, with proper message comment | |||
|
737 | comment, status = PullRequestModel().close_pull_request_with_comment( | |||
|
738 | pull_request, apiuser, repo, message=message) | |||
|
739 | status_lbl = ChangesetStatus.get_status_lbl(status) | |||
|
740 | ||||
|
741 | Session().commit() | |||
|
742 | ||||
|
743 | data = { | |||
|
744 | 'pull_request_id': pull_request.pull_request_id, | |||
|
745 | 'close_status': status_lbl, | |||
|
746 | 'closed': True, | |||
|
747 | } | |||
|
748 | return data |
@@ -368,7 +368,6 b' class ChangesetController(BaseRepoContro' | |||||
368 | comment_type=comment_type, |
|
368 | comment_type=comment_type, | |
369 | resolves_comment_id=resolves_comment_id |
|
369 | resolves_comment_id=resolves_comment_id | |
370 | ) |
|
370 | ) | |
371 | c.inline_comment = True if comment.line_no else False |
|
|||
372 |
|
371 | |||
373 | # get status if set ! |
|
372 | # get status if set ! | |
374 | if status: |
|
373 | if status: |
@@ -306,8 +306,6 b' class PullrequestsController(BaseRepoCon' | |||||
306 | pull_request.reviewer_data) |
|
306 | pull_request.reviewer_data) | |
307 | elif str2bool(request.POST.get('update_commits', 'false')): |
|
307 | elif str2bool(request.POST.get('update_commits', 'false')): | |
308 | self._update_commits(pull_request) |
|
308 | self._update_commits(pull_request) | |
309 | elif str2bool(request.POST.get('close_pull_request', 'false')): |
|
|||
310 | self._reject_close(pull_request) |
|
|||
311 | elif str2bool(request.POST.get('edit_pull_request', 'false')): |
|
309 | elif str2bool(request.POST.get('edit_pull_request', 'false')): | |
312 | self._edit_pull_request(pull_request) |
|
310 | self._edit_pull_request(pull_request) | |
313 | else: |
|
311 | else: | |
@@ -462,14 +460,6 b' class PullrequestsController(BaseRepoCon' | |||||
462 | h.flash(_('Pull request reviewers updated.'), category='success') |
|
460 | h.flash(_('Pull request reviewers updated.'), category='success') | |
463 | Session().commit() |
|
461 | Session().commit() | |
464 |
|
462 | |||
465 | def _reject_close(self, pull_request): |
|
|||
466 | if pull_request.is_closed(): |
|
|||
467 | raise HTTPForbidden() |
|
|||
468 |
|
||||
469 | PullRequestModel().close_pull_request_with_comment( |
|
|||
470 | pull_request, c.rhodecode_user, c.rhodecode_db_repo) |
|
|||
471 | Session().commit() |
|
|||
472 |
|
||||
473 | @LoginRequired() |
|
463 | @LoginRequired() | |
474 | @NotAnonymous() |
|
464 | @NotAnonymous() | |
475 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', |
|
465 | @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', | |
@@ -888,6 +878,7 b' class PullrequestsController(BaseRepoCon' | |||||
888 | pull_request_id = safe_int(pull_request_id) |
|
878 | pull_request_id = safe_int(pull_request_id) | |
889 | pull_request = PullRequest.get_or_404(pull_request_id) |
|
879 | pull_request = PullRequest.get_or_404(pull_request_id) | |
890 | if pull_request.is_closed(): |
|
880 | if pull_request.is_closed(): | |
|
881 | log.debug('comment: forbidden because pull request is closed') | |||
891 | raise HTTPForbidden() |
|
882 | raise HTTPForbidden() | |
892 |
|
883 | |||
893 | status = request.POST.get('changeset_status', None) |
|
884 | status = request.POST.get('changeset_status', None) | |
@@ -896,19 +887,29 b' class PullrequestsController(BaseRepoCon' | |||||
896 | resolves_comment_id = request.POST.get('resolves_comment_id', None) |
|
887 | resolves_comment_id = request.POST.get('resolves_comment_id', None) | |
897 | close_pull_request = request.POST.get('close_pull_request') |
|
888 | close_pull_request = request.POST.get('close_pull_request') | |
898 |
|
889 | |||
899 | close_pr = False |
|
890 | # the logic here should work like following, if we submit close | |
|
891 | # pr comment, use `close_pull_request_with_comment` function | |||
|
892 | # else handle regular comment logic | |||
|
893 | user = c.rhodecode_user | |||
|
894 | repo = c.rhodecode_db_repo | |||
|
895 | ||||
|
896 | if close_pull_request: | |||
900 | # only owner or admin or person with write permissions |
|
897 | # only owner or admin or person with write permissions | |
901 | allowed_to_close = PullRequestModel().check_user_update( |
|
898 | allowed_to_close = PullRequestModel().check_user_update( | |
902 | pull_request, c.rhodecode_user) |
|
899 | pull_request, c.rhodecode_user) | |
|
900 | if not allowed_to_close: | |||
|
901 | log.debug('comment: forbidden because not allowed to close ' | |||
|
902 | 'pull request %s', pull_request_id) | |||
|
903 | raise HTTPForbidden() | |||
|
904 | comment, status = PullRequestModel().close_pull_request_with_comment( | |||
|
905 | pull_request, user, repo, message=text) | |||
|
906 | Session().flush() | |||
|
907 | events.trigger( | |||
|
908 | events.PullRequestCommentEvent(pull_request, comment)) | |||
903 |
|
909 | |||
904 | if close_pull_request and allowed_to_close: |
|
|||
905 | close_pr = True |
|
|||
906 | pull_request_review_status = pull_request.calculated_review_status() |
|
|||
907 | if pull_request_review_status == ChangesetStatus.STATUS_APPROVED: |
|
|||
908 | # approved only if we have voting consent |
|
|||
909 | status = ChangesetStatus.STATUS_APPROVED |
|
|||
910 |
|
|
910 | else: | |
911 | status = ChangesetStatus.STATUS_REJECTED |
|
911 | # regular comment case, could be inline, or one with status. | |
|
912 | # for that one we check also permissions | |||
912 |
|
913 | |||
913 | allowed_to_change_status = PullRequestModel().check_user_change_status( |
|
914 | allowed_to_change_status = PullRequestModel().check_user_change_status( | |
914 | pull_request, c.rhodecode_user) |
|
915 | pull_request, c.rhodecode_user) | |
@@ -917,10 +918,9 b' class PullrequestsController(BaseRepoCon' | |||||
917 | message = (_('Status change %(transition_icon)s %(status)s') |
|
918 | message = (_('Status change %(transition_icon)s %(status)s') | |
918 | % {'transition_icon': '>', |
|
919 | % {'transition_icon': '>', | |
919 | 'status': ChangesetStatus.get_status_lbl(status)}) |
|
920 | 'status': ChangesetStatus.get_status_lbl(status)}) | |
920 | if close_pr: |
|
|||
921 | message = _('Closing with') + ' ' + message |
|
|||
922 | text = text or message |
|
921 | text = text or message | |
923 | comm = CommentsModel().create( |
|
922 | ||
|
923 | comment = CommentsModel().create( | |||
924 | text=text, |
|
924 | text=text, | |
925 | repo=c.rhodecode_db_repo.repo_id, |
|
925 | repo=c.rhodecode_db_repo.repo_id, | |
926 | user=c.rhodecode_user.user_id, |
|
926 | user=c.rhodecode_user.user_id, | |
@@ -931,25 +931,28 b' class PullrequestsController(BaseRepoCon' | |||||
931 | if status and allowed_to_change_status else None), |
|
931 | if status and allowed_to_change_status else None), | |
932 | status_change_type=(status |
|
932 | status_change_type=(status | |
933 | if status and allowed_to_change_status else None), |
|
933 | if status and allowed_to_change_status else None), | |
934 | closing_pr=close_pr, |
|
|||
935 | comment_type=comment_type, |
|
934 | comment_type=comment_type, | |
936 | resolves_comment_id=resolves_comment_id |
|
935 | resolves_comment_id=resolves_comment_id | |
937 | ) |
|
936 | ) | |
938 |
|
937 | |||
939 | if allowed_to_change_status: |
|
938 | if allowed_to_change_status: | |
|
939 | # calculate old status before we change it | |||
940 | old_calculated_status = pull_request.calculated_review_status() |
|
940 | old_calculated_status = pull_request.calculated_review_status() | |
|
941 | ||||
941 | # get status if set ! |
|
942 | # get status if set ! | |
942 | if status: |
|
943 | if status: | |
943 | ChangesetStatusModel().set_status( |
|
944 | ChangesetStatusModel().set_status( | |
944 | c.rhodecode_db_repo.repo_id, |
|
945 | c.rhodecode_db_repo.repo_id, | |
945 | status, |
|
946 | status, | |
946 | c.rhodecode_user.user_id, |
|
947 | c.rhodecode_user.user_id, | |
947 | comm, |
|
948 | comment, | |
948 | pull_request=pull_request_id |
|
949 | pull_request=pull_request_id | |
949 | ) |
|
950 | ) | |
950 |
|
951 | |||
951 | Session().flush() |
|
952 | Session().flush() | |
952 | events.trigger(events.PullRequestCommentEvent(pull_request, comm)) |
|
953 | events.trigger( | |
|
954 | events.PullRequestCommentEvent(pull_request, comment)) | |||
|
955 | ||||
953 | # we now calculate the status of pull request, and based on that |
|
956 | # we now calculate the status of pull request, and based on that | |
954 | # calculation we set the commits status |
|
957 | # calculation we set the commits status | |
955 | calculated_status = pull_request.calculated_review_status() |
|
958 | calculated_status = pull_request.calculated_review_status() | |
@@ -957,23 +960,6 b' class PullrequestsController(BaseRepoCon' | |||||
957 | PullRequestModel()._trigger_pull_request_hook( |
|
960 | PullRequestModel()._trigger_pull_request_hook( | |
958 | pull_request, c.rhodecode_user, 'review_status_change') |
|
961 | pull_request, c.rhodecode_user, 'review_status_change') | |
959 |
|
962 | |||
960 | calculated_status_lbl = ChangesetStatus.get_status_lbl( |
|
|||
961 | calculated_status) |
|
|||
962 |
|
||||
963 | if close_pr: |
|
|||
964 | status_completed = ( |
|
|||
965 | calculated_status in [ChangesetStatus.STATUS_APPROVED, |
|
|||
966 | ChangesetStatus.STATUS_REJECTED]) |
|
|||
967 | if close_pull_request or status_completed: |
|
|||
968 | PullRequestModel().close_pull_request( |
|
|||
969 | pull_request_id, c.rhodecode_user) |
|
|||
970 | else: |
|
|||
971 | h.flash(_('Closing pull request on other statuses than ' |
|
|||
972 | 'rejected or approved is forbidden. ' |
|
|||
973 | 'Calculated status from all reviewers ' |
|
|||
974 | 'is currently: %s') % calculated_status_lbl, |
|
|||
975 | category='warning') |
|
|||
976 |
|
||||
977 | Session().commit() |
|
963 | Session().commit() | |
978 |
|
964 | |||
979 | if not request.is_xhr: |
|
965 | if not request.is_xhr: | |
@@ -983,12 +969,11 b' class PullrequestsController(BaseRepoCon' | |||||
983 | data = { |
|
969 | data = { | |
984 | 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), |
|
970 | 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), | |
985 | } |
|
971 | } | |
986 | if comm: |
|
972 | if comment: | |
987 | c.co = comm |
|
973 | c.co = comment | |
988 | c.inline_comment = True if comm.line_no else False |
|
974 | rendered_comment = render('changeset/changeset_comment_block.mako') | |
989 | data.update(comm.get_dict()) |
|
975 | data.update(comment.get_dict()) | |
990 | data.update({'rendered_text': |
|
976 | data.update({'rendered_text': rendered_comment}) | |
991 | render('changeset/changeset_comment_block.mako')}) |
|
|||
992 |
|
977 | |||
993 | return data |
|
978 | return data | |
994 |
|
979 |
@@ -3107,6 +3107,10 b' class ChangesetComment(Base, BaseModel):' | |||||
3107 | def is_todo(self): |
|
3107 | def is_todo(self): | |
3108 | return self.comment_type == self.COMMENT_TYPE_TODO |
|
3108 | return self.comment_type == self.COMMENT_TYPE_TODO | |
3109 |
|
3109 | |||
|
3110 | @property | |||
|
3111 | def is_inline(self): | |||
|
3112 | return self.line_no and self.f_path | |||
|
3113 | ||||
3110 | def get_index_version(self, versions): |
|
3114 | def get_index_version(self, versions): | |
3111 | return self.get_index_from_version( |
|
3115 | return self.get_index_from_version( | |
3112 | self.pull_request_version_id, versions) |
|
3116 | self.pull_request_version_id, versions) |
@@ -34,6 +34,7 b' from pylons.i18n.translation import lazy' | |||||
34 | from pyramid.threadlocal import get_current_request |
|
34 | from pyramid.threadlocal import get_current_request | |
35 | from sqlalchemy import or_ |
|
35 | from sqlalchemy import or_ | |
36 |
|
36 | |||
|
37 | from rhodecode import events | |||
37 | from rhodecode.lib import helpers as h, hooks_utils, diffs |
|
38 | from rhodecode.lib import helpers as h, hooks_utils, diffs | |
38 | from rhodecode.lib.compat import OrderedDict |
|
39 | from rhodecode.lib.compat import OrderedDict | |
39 | from rhodecode.lib.hooks_daemon import prepare_callback_daemon |
|
40 | from rhodecode.lib.hooks_daemon import prepare_callback_daemon | |
@@ -1064,42 +1065,61 b' class PullRequestModel(BaseModel):' | |||||
1064 | pull_request, pull_request.author, 'close') |
|
1065 | pull_request, pull_request.author, 'close') | |
1065 | self._log_action('user_closed_pull_request', user, pull_request) |
|
1066 | self._log_action('user_closed_pull_request', user, pull_request) | |
1066 |
|
1067 | |||
1067 |
def close_pull_request_with_comment( |
|
1068 | def close_pull_request_with_comment( | |
1068 | message=None): |
|
1069 | self, pull_request, user, repo, message=None): | |
1069 | status = ChangesetStatus.STATUS_REJECTED |
|
1070 | ||
|
1071 | pull_request_review_status = pull_request.calculated_review_status() | |||
1070 |
|
1072 | |||
1071 | if not message: |
|
1073 | if pull_request_review_status == ChangesetStatus.STATUS_APPROVED: | |
1072 | message = ( |
|
1074 | # approved only if we have voting consent | |
1073 | _('Status change %(transition_icon)s %(status)s') % { |
|
1075 | status = ChangesetStatus.STATUS_APPROVED | |
1074 | 'transition_icon': '>', |
|
1076 | else: | |
1075 | 'status': ChangesetStatus.get_status_lbl(status)}) |
|
1077 | status = ChangesetStatus.STATUS_REJECTED | |
|
1078 | status_lbl = ChangesetStatus.get_status_lbl(status) | |||
1076 |
|
1079 | |||
1077 | internal_message = _('Closing with') + ' ' + message |
|
1080 | default_message = ( | |
|
1081 | _('Closing with status change {transition_icon} {status}.') | |||
|
1082 | ).format(transition_icon='>', status=status_lbl) | |||
|
1083 | text = message or default_message | |||
1078 |
|
1084 | |||
1079 | comm = CommentsModel().create( |
|
1085 | # create a comment, and link it to new status | |
1080 | text=internal_message, |
|
1086 | comment = CommentsModel().create( | |
|
1087 | text=text, | |||
1081 | repo=repo.repo_id, |
|
1088 | repo=repo.repo_id, | |
1082 | user=user.user_id, |
|
1089 | user=user.user_id, | |
1083 | pull_request=pull_request.pull_request_id, |
|
1090 | pull_request=pull_request.pull_request_id, | |
1084 | f_path=None, |
|
1091 | status_change=status_lbl, | |
1085 | line_no=None, |
|
|||
1086 | status_change=ChangesetStatus.get_status_lbl(status), |
|
|||
1087 | status_change_type=status, |
|
1092 | status_change_type=status, | |
1088 | closing_pr=True |
|
1093 | closing_pr=True | |
1089 | ) |
|
1094 | ) | |
1090 |
|
1095 | |||
|
1096 | # calculate old status before we change it | |||
|
1097 | old_calculated_status = pull_request.calculated_review_status() | |||
1091 | ChangesetStatusModel().set_status( |
|
1098 | ChangesetStatusModel().set_status( | |
1092 | repo.repo_id, |
|
1099 | repo.repo_id, | |
1093 | status, |
|
1100 | status, | |
1094 | user.user_id, |
|
1101 | user.user_id, | |
1095 | comm, |
|
1102 | comment=comment, | |
1096 | pull_request=pull_request.pull_request_id |
|
1103 | pull_request=pull_request.pull_request_id | |
1097 | ) |
|
1104 | ) | |
|
1105 | ||||
1098 | Session().flush() |
|
1106 | Session().flush() | |
|
1107 | events.trigger(events.PullRequestCommentEvent(pull_request, comment)) | |||
|
1108 | # we now calculate the status of pull request again, and based on that | |||
|
1109 | # calculation trigger status change. This might happen in cases | |||
|
1110 | # that non-reviewer admin closes a pr, which means his vote doesn't | |||
|
1111 | # change the status, while if he's a reviewer this might change it. | |||
|
1112 | calculated_status = pull_request.calculated_review_status() | |||
|
1113 | if old_calculated_status != calculated_status: | |||
|
1114 | self._trigger_pull_request_hook( | |||
|
1115 | pull_request, user, 'review_status_change') | |||
1099 |
|
1116 | |||
|
1117 | # finally close the PR | |||
1100 | PullRequestModel().close_pull_request( |
|
1118 | PullRequestModel().close_pull_request( | |
1101 | pull_request.pull_request_id, user) |
|
1119 | pull_request.pull_request_id, user) | |
1102 |
|
1120 | |||
|
1121 | return comment, status | |||
|
1122 | ||||
1103 | def merge_status(self, pull_request): |
|
1123 | def merge_status(self, pull_request): | |
1104 | if not self._is_merge_enabled(pull_request): |
|
1124 | if not self._is_merge_enabled(pull_request): | |
1105 | return False, _('Server-side pull request merging is disabled.') |
|
1125 | return False, _('Server-side pull request merging is disabled.') |
@@ -96,6 +96,7 b' function registerRCRoutes() {' | |||||
96 | pyroutes.register('goto_switcher_data', '/_goto_data', []); |
|
96 | pyroutes.register('goto_switcher_data', '/_goto_data', []); | |
97 | pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']); |
|
97 | pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']); | |
98 | pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']); |
|
98 | pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']); | |
|
99 | pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']); | |||
99 | pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']); |
|
100 | pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']); | |
100 | pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']); |
|
101 | pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']); | |
101 | pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']); |
|
102 | pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']); |
@@ -368,16 +368,6 b' var _updatePullRequest = function(repo_n' | |||||
368 | }; |
|
368 | }; | |
369 |
|
369 | |||
370 | /** |
|
370 | /** | |
371 | * PULL REQUEST reject & close |
|
|||
372 | */ |
|
|||
373 | var closePullRequest = function(repo_name, pull_request_id) { |
|
|||
374 | var postData = { |
|
|||
375 | '_method': 'put', |
|
|||
376 | 'close_pull_request': true}; |
|
|||
377 | _updatePullRequest(repo_name, pull_request_id, postData); |
|
|||
378 | }; |
|
|||
379 |
|
||||
380 | /** |
|
|||
381 | * PULL REQUEST update commits |
|
371 | * PULL REQUEST update commits | |
382 | */ |
|
372 | */ | |
383 | var updateCommits = function(repo_name, pull_request_id) { |
|
373 | var updateCommits = function(repo_name, pull_request_id) { |
@@ -1,4 +1,4 b'' | |||||
1 | ## this is a dummy html file for partial rendering on server and sending |
|
1 | ## this is a dummy html file for partial rendering on server and sending | |
2 | ## generated output via ajax after comment submit |
|
2 | ## generated output via ajax after comment submit | |
3 | <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/> |
|
3 | <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/> | |
4 |
${comment.comment_block(c.co, inline=c.inline |
|
4 | ${comment.comment_block(c.co, inline=c.co.is_inline)} |
@@ -949,9 +949,6 b'' | |||||
949 | updateCommits("rhodecode-momentum", "720"); |
|
949 | updateCommits("rhodecode-momentum", "720"); | |
950 | }); |
|
950 | }); | |
951 |
|
951 | |||
952 | $('#close_pull_request').on('click', function(e){ |
|
|||
953 | closePullRequest("rhodecode-momentum", "720"); |
|
|||
954 | }); |
|
|||
955 | }) |
|
952 | }) | |
956 | </script> |
|
953 | </script> | |
957 |
|
954 |
@@ -825,11 +825,6 b'' | |||||
825 | // fixing issue with caches on firefox |
|
825 | // fixing issue with caches on firefox | |
826 | $('#update_commits').removeAttr("disabled"); |
|
826 | $('#update_commits').removeAttr("disabled"); | |
827 |
|
827 | |||
828 | $('#close_pull_request').on('click', function(e){ |
|
|||
829 | closePullRequest( |
|
|||
830 | "${c.repo_name}", "${c.pull_request.pull_request_id}"); |
|
|||
831 | }); |
|
|||
832 |
|
||||
833 | $('.show-inline-comments').on('click', function(e){ |
|
828 | $('.show-inline-comments').on('click', function(e){ | |
834 | var boxid = $(this).attr('data-comment-id'); |
|
829 | var boxid = $(this).attr('data-comment-id'); | |
835 | var button = $(this); |
|
830 | var button = $(this); |
@@ -237,7 +237,9 b' class TestPullrequestsController(object)' | |||||
237 | assertr.element_contains( |
|
237 | assertr.element_contains( | |
238 | 'span[data-role="merge-message"]', str(expected_msg)) |
|
238 | 'span[data-role="merge-message"]', str(expected_msg)) | |
239 |
|
239 | |||
240 |
def test_comment_and_close_pull_request( |
|
240 | def test_comment_and_close_pull_request_custom_message_approved( | |
|
241 | self, pr_util, csrf_token, xhr_header): | |||
|
242 | ||||
241 | pull_request = pr_util.create_pull_request(approved=True) |
|
243 | pull_request = pr_util.create_pull_request(approved=True) | |
242 | pull_request_id = pull_request.pull_request_id |
|
244 | pull_request_id = pull_request.pull_request_id | |
243 | author = pull_request.user_id |
|
245 | author = pull_request.user_id | |
@@ -249,11 +251,10 b' class TestPullrequestsController(object)' | |||||
249 | repo_name=pull_request.target_repo.scm_instance().name, |
|
251 | repo_name=pull_request.target_repo.scm_instance().name, | |
250 | pull_request_id=str(pull_request_id)), |
|
252 | pull_request_id=str(pull_request_id)), | |
251 | params={ |
|
253 | params={ | |
252 | 'changeset_status': ChangesetStatus.STATUS_APPROVED, |
|
|||
253 | 'close_pull_request': '1', |
|
254 | 'close_pull_request': '1', | |
254 | 'text': 'Closing a PR', |
|
255 | 'text': 'Closing a PR', | |
255 | 'csrf_token': csrf_token}, |
|
256 | 'csrf_token': csrf_token}, | |
256 | status=302) |
|
257 | extra_environ=xhr_header,) | |
257 |
|
258 | |||
258 | action = 'user_closed_pull_request:%d' % pull_request_id |
|
259 | action = 'user_closed_pull_request:%d' % pull_request_id | |
259 | journal = UserLog.query()\ |
|
260 | journal = UserLog.query()\ | |
@@ -270,45 +271,26 b' class TestPullrequestsController(object)' | |||||
270 | status = ChangesetStatusModel().get_status( |
|
271 | status = ChangesetStatusModel().get_status( | |
271 | pull_request.source_repo, pull_request=pull_request) |
|
272 | pull_request.source_repo, pull_request=pull_request) | |
272 | assert status == ChangesetStatus.STATUS_APPROVED |
|
273 | assert status == ChangesetStatus.STATUS_APPROVED | |
273 |
|
274 | assert pull_request.comments[-1].text == 'Closing a PR' | ||
274 | def test_reject_and_close_pull_request(self, pr_util, csrf_token): |
|
|||
275 | pull_request = pr_util.create_pull_request() |
|
|||
276 | pull_request_id = pull_request.pull_request_id |
|
|||
277 | response = self.app.post( |
|
|||
278 | url(controller='pullrequests', |
|
|||
279 | action='update', |
|
|||
280 | repo_name=pull_request.target_repo.scm_instance().name, |
|
|||
281 | pull_request_id=str(pull_request.pull_request_id)), |
|
|||
282 | params={'close_pull_request': 'true', '_method': 'put', |
|
|||
283 | 'csrf_token': csrf_token}) |
|
|||
284 |
|
275 | |||
285 | pull_request = PullRequest.get(pull_request_id) |
|
276 | def test_comment_force_close_pull_request_rejected( | |
286 |
|
277 | self, pr_util, csrf_token, xhr_header): | ||
287 | assert response.json is True |
|
|||
288 | assert pull_request.is_closed() |
|
|||
289 |
|
||||
290 | # check only the latest status, not the review status |
|
|||
291 | status = ChangesetStatusModel().get_status( |
|
|||
292 | pull_request.source_repo, pull_request=pull_request) |
|
|||
293 | assert status == ChangesetStatus.STATUS_REJECTED |
|
|||
294 |
|
||||
295 | def test_comment_force_close_pull_request(self, pr_util, csrf_token): |
|
|||
296 | pull_request = pr_util.create_pull_request() |
|
278 | pull_request = pr_util.create_pull_request() | |
297 | pull_request_id = pull_request.pull_request_id |
|
279 | pull_request_id = pull_request.pull_request_id | |
298 | PullRequestModel().update_reviewers( |
|
280 | PullRequestModel().update_reviewers( | |
299 | pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)]) |
|
281 | pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)]) | |
300 | author = pull_request.user_id |
|
282 | author = pull_request.user_id | |
301 | repo = pull_request.target_repo.repo_id |
|
283 | repo = pull_request.target_repo.repo_id | |
|
284 | ||||
302 | self.app.post( |
|
285 | self.app.post( | |
303 | url(controller='pullrequests', |
|
286 | url(controller='pullrequests', | |
304 | action='comment', |
|
287 | action='comment', | |
305 | repo_name=pull_request.target_repo.scm_instance().name, |
|
288 | repo_name=pull_request.target_repo.scm_instance().name, | |
306 | pull_request_id=str(pull_request_id)), |
|
289 | pull_request_id=str(pull_request_id)), | |
307 | params={ |
|
290 | params={ | |
308 | 'changeset_status': 'rejected', |
|
|||
309 | 'close_pull_request': '1', |
|
291 | 'close_pull_request': '1', | |
310 | 'csrf_token': csrf_token}, |
|
292 | 'csrf_token': csrf_token}, | |
311 | status=302) |
|
293 | extra_environ=xhr_header) | |
312 |
|
294 | |||
313 | pull_request = PullRequest.get(pull_request_id) |
|
295 | pull_request = PullRequest.get(pull_request_id) | |
314 |
|
296 | |||
@@ -324,6 +306,31 b' class TestPullrequestsController(object)' | |||||
324 | pull_request.source_repo, pull_request=pull_request) |
|
306 | pull_request.source_repo, pull_request=pull_request) | |
325 | assert status == ChangesetStatus.STATUS_REJECTED |
|
307 | assert status == ChangesetStatus.STATUS_REJECTED | |
326 |
|
308 | |||
|
309 | def test_comment_and_close_pull_request( | |||
|
310 | self, pr_util, csrf_token, xhr_header): | |||
|
311 | pull_request = pr_util.create_pull_request() | |||
|
312 | pull_request_id = pull_request.pull_request_id | |||
|
313 | ||||
|
314 | response = self.app.post( | |||
|
315 | url(controller='pullrequests', | |||
|
316 | action='comment', | |||
|
317 | repo_name=pull_request.target_repo.scm_instance().name, | |||
|
318 | pull_request_id=str(pull_request.pull_request_id)), | |||
|
319 | params={ | |||
|
320 | 'close_pull_request': 'true', | |||
|
321 | 'csrf_token': csrf_token}, | |||
|
322 | extra_environ=xhr_header) | |||
|
323 | ||||
|
324 | assert response.json | |||
|
325 | ||||
|
326 | pull_request = PullRequest.get(pull_request_id) | |||
|
327 | assert pull_request.is_closed() | |||
|
328 | ||||
|
329 | # check only the latest status, not the review status | |||
|
330 | status = ChangesetStatusModel().get_status( | |||
|
331 | pull_request.source_repo, pull_request=pull_request) | |||
|
332 | assert status == ChangesetStatus.STATUS_REJECTED | |||
|
333 | ||||
327 | def test_create_pull_request(self, backend, csrf_token): |
|
334 | def test_create_pull_request(self, backend, csrf_token): | |
328 | commits = [ |
|
335 | commits = [ | |
329 | {'message': 'ancestor'}, |
|
336 | {'message': 'ancestor'}, |
General Comments 0
You need to be logged in to leave comments.
Login now