##// END OF EJS Templates
api: added edit comment api
wuboo -
r4429:d4450498 default
parent child Browse files
Show More
@@ -1,133 +1,133 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.api.utils import Optional, OAttr
23 from rhodecode.api.utils import Optional, OAttr
24 from rhodecode.api.tests.utils import (
24 from rhodecode.api.tests.utils import (
25 build_data, api_call, assert_error, assert_ok)
25 build_data, api_call, assert_error, assert_ok)
26
26
27
27
28 @pytest.mark.usefixtures("testuser_api", "app")
28 @pytest.mark.usefixtures("testuser_api", "app")
29 class TestApi(object):
29 class TestApi(object):
30 maxDiff = None
30 maxDiff = None
31
31
32 def test_Optional_object(self):
32 def test_Optional_object(self):
33
33
34 option1 = Optional(None)
34 option1 = Optional(None)
35 assert '<Optional:%s>' % (None,) == repr(option1)
35 assert '<Optional:%s>' % (None,) == repr(option1)
36 assert option1() is None
36 assert option1() is None
37
37
38 assert 1 == Optional.extract(Optional(1))
38 assert 1 == Optional.extract(Optional(1))
39 assert 'example' == Optional.extract('example')
39 assert 'example' == Optional.extract('example')
40
40
41 def test_Optional_OAttr(self):
41 def test_Optional_OAttr(self):
42 option1 = Optional(OAttr('apiuser'))
42 option1 = Optional(OAttr('apiuser'))
43 assert 'apiuser' == Optional.extract(option1)
43 assert 'apiuser' == Optional.extract(option1)
44
44
45 def test_OAttr_object(self):
45 def test_OAttr_object(self):
46 oattr1 = OAttr('apiuser')
46 oattr1 = OAttr('apiuser')
47 assert '<OptionalAttr:apiuser>' == repr(oattr1)
47 assert '<OptionalAttr:apiuser>' == repr(oattr1)
48 assert oattr1() == oattr1
48 assert oattr1() == oattr1
49
49
50 def test_api_wrong_key(self):
50 def test_api_wrong_key(self):
51 id_, params = build_data('trololo', 'get_user')
51 id_, params = build_data('trololo', 'get_user')
52 response = api_call(self.app, params)
52 response = api_call(self.app, params)
53
53
54 expected = 'Invalid API KEY'
54 expected = 'Invalid API KEY'
55 assert_error(id_, expected, given=response.body)
55 assert_error(id_, expected, given=response.body)
56
56
57 def test_api_missing_non_optional_param(self):
57 def test_api_missing_non_optional_param(self):
58 id_, params = build_data(self.apikey, 'get_repo')
58 id_, params = build_data(self.apikey, 'get_repo')
59 response = api_call(self.app, params)
59 response = api_call(self.app, params)
60
60
61 expected = 'Missing non optional `repoid` arg in JSON DATA'
61 expected = 'Missing non optional `repoid` arg in JSON DATA'
62 assert_error(id_, expected, given=response.body)
62 assert_error(id_, expected, given=response.body)
63
63
64 def test_api_missing_non_optional_param_args_null(self):
64 def test_api_missing_non_optional_param_args_null(self):
65 id_, params = build_data(self.apikey, 'get_repo')
65 id_, params = build_data(self.apikey, 'get_repo')
66 params = params.replace('"args": {}', '"args": null')
66 params = params.replace('"args": {}', '"args": null')
67 response = api_call(self.app, params)
67 response = api_call(self.app, params)
68
68
69 expected = 'Missing non optional `repoid` arg in JSON DATA'
69 expected = 'Missing non optional `repoid` arg in JSON DATA'
70 assert_error(id_, expected, given=response.body)
70 assert_error(id_, expected, given=response.body)
71
71
72 def test_api_missing_non_optional_param_args_bad(self):
72 def test_api_missing_non_optional_param_args_bad(self):
73 id_, params = build_data(self.apikey, 'get_repo')
73 id_, params = build_data(self.apikey, 'get_repo')
74 params = params.replace('"args": {}', '"args": 1')
74 params = params.replace('"args": {}', '"args": 1')
75 response = api_call(self.app, params)
75 response = api_call(self.app, params)
76
76
77 expected = 'Missing non optional `repoid` arg in JSON DATA'
77 expected = 'Missing non optional `repoid` arg in JSON DATA'
78 assert_error(id_, expected, given=response.body)
78 assert_error(id_, expected, given=response.body)
79
79
80 def test_api_non_existing_method(self, request):
80 def test_api_non_existing_method(self, request):
81 id_, params = build_data(self.apikey, 'not_existing', args='xx')
81 id_, params = build_data(self.apikey, 'not_existing', args='xx')
82 response = api_call(self.app, params)
82 response = api_call(self.app, params)
83 expected = 'No such method: not_existing. Similar methods: none'
83 expected = 'No such method: not_existing. Similar methods: none'
84 assert_error(id_, expected, given=response.body)
84 assert_error(id_, expected, given=response.body)
85
85
86 def test_api_non_existing_method_have_similar(self, request):
86 def test_api_non_existing_method_have_similar(self, request):
87 id_, params = build_data(self.apikey, 'comment', args='xx')
87 id_, params = build_data(self.apikey, 'comment', args='xx')
88 response = api_call(self.app, params)
88 response = api_call(self.app, params)
89 expected = 'No such method: comment. ' \
89 expected = 'No such method: comment. ' \
90 'Similar methods: changeset_comment, comment_pull_request, ' \
90 'Similar methods: changeset_comment, comment_pull_request, edit_comment, ' \
91 'get_pull_request_comments, comment_commit, get_repo_comments'
91 'get_pull_request_comments, comment_commit, get_repo_comments'
92 assert_error(id_, expected, given=response.body)
92 assert_error(id_, expected, given=response.body)
93
93
94 def test_api_disabled_user(self, request):
94 def test_api_disabled_user(self, request):
95
95
96 def set_active(active):
96 def set_active(active):
97 from rhodecode.model.db import Session, User
97 from rhodecode.model.db import Session, User
98 user = User.get_by_auth_token(self.apikey)
98 user = User.get_by_auth_token(self.apikey)
99 user.active = active
99 user.active = active
100 Session().add(user)
100 Session().add(user)
101 Session().commit()
101 Session().commit()
102
102
103 request.addfinalizer(lambda: set_active(True))
103 request.addfinalizer(lambda: set_active(True))
104
104
105 set_active(False)
105 set_active(False)
106 id_, params = build_data(self.apikey, 'test', args='xx')
106 id_, params = build_data(self.apikey, 'test', args='xx')
107 response = api_call(self.app, params)
107 response = api_call(self.app, params)
108 expected = 'Request from this user not allowed'
108 expected = 'Request from this user not allowed'
109 assert_error(id_, expected, given=response.body)
109 assert_error(id_, expected, given=response.body)
110
110
111 def test_api_args_is_null(self):
111 def test_api_args_is_null(self):
112 __, params = build_data(self.apikey, 'get_users', )
112 __, params = build_data(self.apikey, 'get_users', )
113 params = params.replace('"args": {}', '"args": null')
113 params = params.replace('"args": {}', '"args": null')
114 response = api_call(self.app, params)
114 response = api_call(self.app, params)
115 assert response.status == '200 OK'
115 assert response.status == '200 OK'
116
116
117 def test_api_args_is_bad(self):
117 def test_api_args_is_bad(self):
118 __, params = build_data(self.apikey, 'get_users', )
118 __, params = build_data(self.apikey, 'get_users', )
119 params = params.replace('"args": {}', '"args": 1')
119 params = params.replace('"args": {}', '"args": 1')
120 response = api_call(self.app, params)
120 response = api_call(self.app, params)
121 assert response.status == '200 OK'
121 assert response.status == '200 OK'
122
122
123 def test_api_args_different_args(self):
123 def test_api_args_different_args(self):
124 import string
124 import string
125 expected = {
125 expected = {
126 'ascii_letters': string.ascii_letters,
126 'ascii_letters': string.ascii_letters,
127 'ws': string.whitespace,
127 'ws': string.whitespace,
128 'printables': string.printable
128 'printables': string.printable
129 }
129 }
130 id_, params = build_data(self.apikey, 'test', args=expected)
130 id_, params = build_data(self.apikey, 'test', args=expected)
131 response = api_call(self.app, params)
131 response = api_call(self.app, params)
132 assert response.status == '200 OK'
132 assert response.status == '200 OK'
133 assert_ok(id_, expected, response.body)
133 assert_ok(id_, expected, response.body)
@@ -1,246 +1,390 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.comment import CommentsModel
23 from rhodecode.model.comment import CommentsModel
24 from rhodecode.model.db import UserLog, User
24 from rhodecode.model.db import UserLog, User, ChangesetComment
25 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.pull_request import PullRequestModel
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 from rhodecode.api.tests.utils import (
27 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_ok)
28 build_data, api_call, assert_error, assert_ok)
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestCommentPullRequest(object):
32 class TestCommentPullRequest(object):
33 finalizers = []
33 finalizers = []
34
34
35 def teardown_method(self, method):
35 def teardown_method(self, method):
36 if self.finalizers:
36 if self.finalizers:
37 for finalizer in self.finalizers:
37 for finalizer in self.finalizers:
38 finalizer()
38 finalizer()
39 self.finalizers = []
39 self.finalizers = []
40
40
41 @pytest.mark.backends("git", "hg")
41 @pytest.mark.backends("git", "hg")
42 def test_api_comment_pull_request(self, pr_util, no_notifications):
42 def test_api_comment_pull_request(self, pr_util, no_notifications):
43 pull_request = pr_util.create_pull_request()
43 pull_request = pr_util.create_pull_request()
44 pull_request_id = pull_request.pull_request_id
44 pull_request_id = pull_request.pull_request_id
45 author = pull_request.user_id
45 author = pull_request.user_id
46 repo = pull_request.target_repo.repo_id
46 repo = pull_request.target_repo.repo_id
47 id_, params = build_data(
47 id_, params = build_data(
48 self.apikey, 'comment_pull_request',
48 self.apikey, 'comment_pull_request',
49 repoid=pull_request.target_repo.repo_name,
49 repoid=pull_request.target_repo.repo_name,
50 pullrequestid=pull_request.pull_request_id,
50 pullrequestid=pull_request.pull_request_id,
51 message='test message')
51 message='test message')
52 response = api_call(self.app, params)
52 response = api_call(self.app, params)
53 pull_request = PullRequestModel().get(pull_request.pull_request_id)
53 pull_request = PullRequestModel().get(pull_request.pull_request_id)
54
54
55 comments = CommentsModel().get_comments(
55 comments = CommentsModel().get_comments(
56 pull_request.target_repo.repo_id, pull_request=pull_request)
56 pull_request.target_repo.repo_id, pull_request=pull_request)
57
57
58 expected = {
58 expected = {
59 'pull_request_id': pull_request.pull_request_id,
59 'pull_request_id': pull_request.pull_request_id,
60 'comment_id': comments[-1].comment_id,
60 'comment_id': comments[-1].comment_id,
61 'status': {'given': None, 'was_changed': None}
61 'status': {'given': None, 'was_changed': None}
62 }
62 }
63 assert_ok(id_, expected, response.body)
63 assert_ok(id_, expected, response.body)
64
64
65 journal = UserLog.query()\
65 journal = UserLog.query()\
66 .filter(UserLog.user_id == author)\
66 .filter(UserLog.user_id == author)\
67 .filter(UserLog.repository_id == repo) \
67 .filter(UserLog.repository_id == repo) \
68 .order_by(UserLog.user_log_id.asc()) \
68 .order_by(UserLog.user_log_id.asc()) \
69 .all()
69 .all()
70 assert journal[-1].action == 'repo.pull_request.comment.create'
70 assert journal[-1].action == 'repo.pull_request.comment.create'
71
71
72 @pytest.mark.backends("git", "hg")
72 @pytest.mark.backends("git", "hg")
73 def test_api_comment_pull_request_with_extra_recipients(self, pr_util, user_util):
73 def test_api_comment_pull_request_with_extra_recipients(self, pr_util, user_util):
74 pull_request = pr_util.create_pull_request()
74 pull_request = pr_util.create_pull_request()
75
75
76 user1 = user_util.create_user()
76 user1 = user_util.create_user()
77 user1_id = user1.user_id
77 user1_id = user1.user_id
78 user2 = user_util.create_user()
78 user2 = user_util.create_user()
79 user2_id = user2.user_id
79 user2_id = user2.user_id
80
80
81 id_, params = build_data(
81 id_, params = build_data(
82 self.apikey, 'comment_pull_request',
82 self.apikey, 'comment_pull_request',
83 repoid=pull_request.target_repo.repo_name,
83 repoid=pull_request.target_repo.repo_name,
84 pullrequestid=pull_request.pull_request_id,
84 pullrequestid=pull_request.pull_request_id,
85 message='test message',
85 message='test message',
86 extra_recipients=[user1.user_id, user2.username]
86 extra_recipients=[user1.user_id, user2.username]
87 )
87 )
88 response = api_call(self.app, params)
88 response = api_call(self.app, params)
89 pull_request = PullRequestModel().get(pull_request.pull_request_id)
89 pull_request = PullRequestModel().get(pull_request.pull_request_id)
90
90
91 comments = CommentsModel().get_comments(
91 comments = CommentsModel().get_comments(
92 pull_request.target_repo.repo_id, pull_request=pull_request)
92 pull_request.target_repo.repo_id, pull_request=pull_request)
93
93
94 expected = {
94 expected = {
95 'pull_request_id': pull_request.pull_request_id,
95 'pull_request_id': pull_request.pull_request_id,
96 'comment_id': comments[-1].comment_id,
96 'comment_id': comments[-1].comment_id,
97 'status': {'given': None, 'was_changed': None}
97 'status': {'given': None, 'was_changed': None}
98 }
98 }
99 assert_ok(id_, expected, response.body)
99 assert_ok(id_, expected, response.body)
100 # check user1/user2 inbox for notification
100 # check user1/user2 inbox for notification
101 user1 = User.get(user1_id)
101 user1 = User.get(user1_id)
102 assert 1 == len(user1.notifications)
102 assert 1 == len(user1.notifications)
103 assert 'test message' in user1.notifications[0].notification.body
103 assert 'test message' in user1.notifications[0].notification.body
104
104
105 user2 = User.get(user2_id)
105 user2 = User.get(user2_id)
106 assert 1 == len(user2.notifications)
106 assert 1 == len(user2.notifications)
107 assert 'test message' in user2.notifications[0].notification.body
107 assert 'test message' in user2.notifications[0].notification.body
108
108
109 @pytest.mark.backends("git", "hg")
109 @pytest.mark.backends("git", "hg")
110 def test_api_comment_pull_request_change_status(
110 def test_api_comment_pull_request_change_status(
111 self, pr_util, no_notifications):
111 self, pr_util, no_notifications):
112 pull_request = pr_util.create_pull_request()
112 pull_request = pr_util.create_pull_request()
113 pull_request_id = pull_request.pull_request_id
113 pull_request_id = pull_request.pull_request_id
114 id_, params = build_data(
114 id_, params = build_data(
115 self.apikey, 'comment_pull_request',
115 self.apikey, 'comment_pull_request',
116 repoid=pull_request.target_repo.repo_name,
116 repoid=pull_request.target_repo.repo_name,
117 pullrequestid=pull_request.pull_request_id,
117 pullrequestid=pull_request.pull_request_id,
118 status='rejected')
118 status='rejected')
119 response = api_call(self.app, params)
119 response = api_call(self.app, params)
120 pull_request = PullRequestModel().get(pull_request_id)
120 pull_request = PullRequestModel().get(pull_request_id)
121
121
122 comments = CommentsModel().get_comments(
122 comments = CommentsModel().get_comments(
123 pull_request.target_repo.repo_id, pull_request=pull_request)
123 pull_request.target_repo.repo_id, pull_request=pull_request)
124 expected = {
124 expected = {
125 'pull_request_id': pull_request.pull_request_id,
125 'pull_request_id': pull_request.pull_request_id,
126 'comment_id': comments[-1].comment_id,
126 'comment_id': comments[-1].comment_id,
127 'status': {'given': 'rejected', 'was_changed': True}
127 'status': {'given': 'rejected', 'was_changed': True}
128 }
128 }
129 assert_ok(id_, expected, response.body)
129 assert_ok(id_, expected, response.body)
130
130
131 @pytest.mark.backends("git", "hg")
131 @pytest.mark.backends("git", "hg")
132 def test_api_comment_pull_request_change_status_with_specific_commit_id(
132 def test_api_comment_pull_request_change_status_with_specific_commit_id(
133 self, pr_util, no_notifications):
133 self, pr_util, no_notifications):
134 pull_request = pr_util.create_pull_request()
134 pull_request = pr_util.create_pull_request()
135 pull_request_id = pull_request.pull_request_id
135 pull_request_id = pull_request.pull_request_id
136 latest_commit_id = 'test_commit'
136 latest_commit_id = 'test_commit'
137 # inject additional revision, to fail test the status change on
137 # inject additional revision, to fail test the status change on
138 # non-latest commit
138 # non-latest commit
139 pull_request.revisions = pull_request.revisions + ['test_commit']
139 pull_request.revisions = pull_request.revisions + ['test_commit']
140
140
141 id_, params = build_data(
141 id_, params = build_data(
142 self.apikey, 'comment_pull_request',
142 self.apikey, 'comment_pull_request',
143 repoid=pull_request.target_repo.repo_name,
143 repoid=pull_request.target_repo.repo_name,
144 pullrequestid=pull_request.pull_request_id,
144 pullrequestid=pull_request.pull_request_id,
145 status='approved', commit_id=latest_commit_id)
145 status='approved', commit_id=latest_commit_id)
146 response = api_call(self.app, params)
146 response = api_call(self.app, params)
147 pull_request = PullRequestModel().get(pull_request_id)
147 pull_request = PullRequestModel().get(pull_request_id)
148
148
149 expected = {
149 expected = {
150 'pull_request_id': pull_request.pull_request_id,
150 'pull_request_id': pull_request.pull_request_id,
151 'comment_id': None,
151 'comment_id': None,
152 'status': {'given': 'approved', 'was_changed': False}
152 'status': {'given': 'approved', 'was_changed': False}
153 }
153 }
154 assert_ok(id_, expected, response.body)
154 assert_ok(id_, expected, response.body)
155
155
156 @pytest.mark.backends("git", "hg")
156 @pytest.mark.backends("git", "hg")
157 def test_api_comment_pull_request_change_status_with_specific_commit_id(
157 def test_api_comment_pull_request_change_status_with_specific_commit_id(
158 self, pr_util, no_notifications):
158 self, pr_util, no_notifications):
159 pull_request = pr_util.create_pull_request()
159 pull_request = pr_util.create_pull_request()
160 pull_request_id = pull_request.pull_request_id
160 pull_request_id = pull_request.pull_request_id
161 latest_commit_id = pull_request.revisions[0]
161 latest_commit_id = pull_request.revisions[0]
162
162
163 id_, params = build_data(
163 id_, params = build_data(
164 self.apikey, 'comment_pull_request',
164 self.apikey, 'comment_pull_request',
165 repoid=pull_request.target_repo.repo_name,
165 repoid=pull_request.target_repo.repo_name,
166 pullrequestid=pull_request.pull_request_id,
166 pullrequestid=pull_request.pull_request_id,
167 status='approved', commit_id=latest_commit_id)
167 status='approved', commit_id=latest_commit_id)
168 response = api_call(self.app, params)
168 response = api_call(self.app, params)
169 pull_request = PullRequestModel().get(pull_request_id)
169 pull_request = PullRequestModel().get(pull_request_id)
170
170
171 comments = CommentsModel().get_comments(
171 comments = CommentsModel().get_comments(
172 pull_request.target_repo.repo_id, pull_request=pull_request)
172 pull_request.target_repo.repo_id, pull_request=pull_request)
173 expected = {
173 expected = {
174 'pull_request_id': pull_request.pull_request_id,
174 'pull_request_id': pull_request.pull_request_id,
175 'comment_id': comments[-1].comment_id,
175 'comment_id': comments[-1].comment_id,
176 'status': {'given': 'approved', 'was_changed': True}
176 'status': {'given': 'approved', 'was_changed': True}
177 }
177 }
178 assert_ok(id_, expected, response.body)
178 assert_ok(id_, expected, response.body)
179
179
180 @pytest.mark.backends("git", "hg")
180 @pytest.mark.backends("git", "hg")
181 def test_api_comment_pull_request_missing_params_error(self, pr_util):
181 def test_api_comment_pull_request_missing_params_error(self, pr_util):
182 pull_request = pr_util.create_pull_request()
182 pull_request = pr_util.create_pull_request()
183 pull_request_id = pull_request.pull_request_id
183 pull_request_id = pull_request.pull_request_id
184 pull_request_repo = pull_request.target_repo.repo_name
184 pull_request_repo = pull_request.target_repo.repo_name
185 id_, params = build_data(
185 id_, params = build_data(
186 self.apikey, 'comment_pull_request',
186 self.apikey, 'comment_pull_request',
187 repoid=pull_request_repo,
187 repoid=pull_request_repo,
188 pullrequestid=pull_request_id)
188 pullrequestid=pull_request_id)
189 response = api_call(self.app, params)
189 response = api_call(self.app, params)
190
190
191 expected = 'Both message and status parameters are missing. At least one is required.'
191 expected = 'Both message and status parameters are missing. At least one is required.'
192 assert_error(id_, expected, given=response.body)
192 assert_error(id_, expected, given=response.body)
193
193
194 @pytest.mark.backends("git", "hg")
194 @pytest.mark.backends("git", "hg")
195 def test_api_comment_pull_request_unknown_status_error(self, pr_util):
195 def test_api_comment_pull_request_unknown_status_error(self, pr_util):
196 pull_request = pr_util.create_pull_request()
196 pull_request = pr_util.create_pull_request()
197 pull_request_id = pull_request.pull_request_id
197 pull_request_id = pull_request.pull_request_id
198 pull_request_repo = pull_request.target_repo.repo_name
198 pull_request_repo = pull_request.target_repo.repo_name
199 id_, params = build_data(
199 id_, params = build_data(
200 self.apikey, 'comment_pull_request',
200 self.apikey, 'comment_pull_request',
201 repoid=pull_request_repo,
201 repoid=pull_request_repo,
202 pullrequestid=pull_request_id,
202 pullrequestid=pull_request_id,
203 status='42')
203 status='42')
204 response = api_call(self.app, params)
204 response = api_call(self.app, params)
205
205
206 expected = 'Unknown comment status: `42`'
206 expected = 'Unknown comment status: `42`'
207 assert_error(id_, expected, given=response.body)
207 assert_error(id_, expected, given=response.body)
208
208
209 @pytest.mark.backends("git", "hg")
209 @pytest.mark.backends("git", "hg")
210 def test_api_comment_pull_request_repo_error(self, pr_util):
210 def test_api_comment_pull_request_repo_error(self, pr_util):
211 pull_request = pr_util.create_pull_request()
211 pull_request = pr_util.create_pull_request()
212 id_, params = build_data(
212 id_, params = build_data(
213 self.apikey, 'comment_pull_request',
213 self.apikey, 'comment_pull_request',
214 repoid=666, pullrequestid=pull_request.pull_request_id)
214 repoid=666, pullrequestid=pull_request.pull_request_id)
215 response = api_call(self.app, params)
215 response = api_call(self.app, params)
216
216
217 expected = 'repository `666` does not exist'
217 expected = 'repository `666` does not exist'
218 assert_error(id_, expected, given=response.body)
218 assert_error(id_, expected, given=response.body)
219
219
220 @pytest.mark.backends("git", "hg")
220 @pytest.mark.backends("git", "hg")
221 def test_api_comment_pull_request_non_admin_with_userid_error(
221 def test_api_comment_pull_request_non_admin_with_userid_error(self, pr_util):
222 self, pr_util):
222 pull_request = pr_util.create_pull_request()
223 id_, params = build_data(
224 self.apikey_regular, 'comment_pull_request',
225 repoid=pull_request.target_repo.repo_name,
226 pullrequestid=pull_request.pull_request_id,
227 userid=TEST_USER_ADMIN_LOGIN)
228 response = api_call(self.app, params)
229
230 expected = 'userid is not the same as your user'
231 assert_error(id_, expected, given=response.body)
232
233 @pytest.mark.backends("git", "hg")
234 def test_api_comment_pull_request_non_admin_with_userid_error(self, pr_util):
223 pull_request = pr_util.create_pull_request()
235 pull_request = pr_util.create_pull_request()
224 id_, params = build_data(
236 id_, params = build_data(
225 self.apikey_regular, 'comment_pull_request',
237 self.apikey_regular, 'comment_pull_request',
226 repoid=pull_request.target_repo.repo_name,
238 repoid=pull_request.target_repo.repo_name,
227 pullrequestid=pull_request.pull_request_id,
239 pullrequestid=pull_request.pull_request_id,
228 userid=TEST_USER_ADMIN_LOGIN)
240 userid=TEST_USER_ADMIN_LOGIN)
229 response = api_call(self.app, params)
241 response = api_call(self.app, params)
230
242
231 expected = 'userid is not the same as your user'
243 expected = 'userid is not the same as your user'
232 assert_error(id_, expected, given=response.body)
244 assert_error(id_, expected, given=response.body)
233
245
234 @pytest.mark.backends("git", "hg")
246 @pytest.mark.backends("git", "hg")
235 def test_api_comment_pull_request_wrong_commit_id_error(self, pr_util):
247 def test_api_comment_pull_request_wrong_commit_id_error(self, pr_util):
236 pull_request = pr_util.create_pull_request()
248 pull_request = pr_util.create_pull_request()
237 id_, params = build_data(
249 id_, params = build_data(
238 self.apikey_regular, 'comment_pull_request',
250 self.apikey_regular, 'comment_pull_request',
239 repoid=pull_request.target_repo.repo_name,
251 repoid=pull_request.target_repo.repo_name,
240 status='approved',
252 status='approved',
241 pullrequestid=pull_request.pull_request_id,
253 pullrequestid=pull_request.pull_request_id,
242 commit_id='XXX')
254 commit_id='XXX')
243 response = api_call(self.app, params)
255 response = api_call(self.app, params)
244
256
245 expected = 'Invalid commit_id `XXX` for this pull request.'
257 expected = 'Invalid commit_id `XXX` for this pull request.'
246 assert_error(id_, expected, given=response.body)
258 assert_error(id_, expected, given=response.body)
259
260 @pytest.mark.backends("git", "hg")
261 def test_api_edit_comment(self, pr_util):
262 pull_request = pr_util.create_pull_request()
263
264 id_, params = build_data(
265 self.apikey,
266 'comment_pull_request',
267 repoid=pull_request.target_repo.repo_name,
268 pullrequestid=pull_request.pull_request_id,
269 message='test message',
270 )
271 response = api_call(self.app, params)
272 json_response = response.json
273 comment_id = json_response['result']['comment_id']
274
275 message_after_edit = 'just message'
276 id_, params = build_data(
277 self.apikey,
278 'edit_comment',
279 comment_id=comment_id,
280 message=message_after_edit,
281 version=0,
282 )
283 response = api_call(self.app, params)
284 json_response = response.json
285 assert json_response['result']['version'] == 1
286
287 text_form_db = ChangesetComment.get(comment_id).text
288 assert message_after_edit == text_form_db
289
290 @pytest.mark.backends("git", "hg")
291 def test_api_edit_comment_wrong_version(self, pr_util):
292 pull_request = pr_util.create_pull_request()
293
294 id_, params = build_data(
295 self.apikey, 'comment_pull_request',
296 repoid=pull_request.target_repo.repo_name,
297 pullrequestid=pull_request.pull_request_id,
298 message='test message')
299 response = api_call(self.app, params)
300 json_response = response.json
301 comment_id = json_response['result']['comment_id']
302
303 message_after_edit = 'just message'
304 id_, params = build_data(
305 self.apikey_regular,
306 'edit_comment',
307 comment_id=comment_id,
308 message=message_after_edit,
309 version=1,
310 )
311 response = api_call(self.app, params)
312 expected = 'comment ({}) version ({}) mismatch'.format(comment_id, 1)
313 assert_error(id_, expected, given=response.body)
314
315 @pytest.mark.backends("git", "hg")
316 def test_api_edit_comment_wrong_version(self, pr_util):
317 pull_request = pr_util.create_pull_request()
318
319 id_, params = build_data(
320 self.apikey, 'comment_pull_request',
321 repoid=pull_request.target_repo.repo_name,
322 pullrequestid=pull_request.pull_request_id,
323 message='test message')
324 response = api_call(self.app, params)
325 json_response = response.json
326 comment_id = json_response['result']['comment_id']
327
328 id_, params = build_data(
329 self.apikey,
330 'edit_comment',
331 comment_id=comment_id,
332 message='',
333 version=0,
334 )
335 response = api_call(self.app, params)
336 expected = "comment ({}) can't be changed with empty string".format(comment_id, 1)
337 assert_error(id_, expected, given=response.body)
338
339 @pytest.mark.backends("git", "hg")
340 def test_api_edit_comment_wrong_user_set_by_non_admin(self, pr_util):
341 pull_request = pr_util.create_pull_request()
342 pull_request_id = pull_request.pull_request_id
343 id_, params = build_data(
344 self.apikey,
345 'comment_pull_request',
346 repoid=pull_request.target_repo.repo_name,
347 pullrequestid=pull_request_id,
348 message='test message'
349 )
350 response = api_call(self.app, params)
351 json_response = response.json
352 comment_id = json_response['result']['comment_id']
353
354 id_, params = build_data(
355 self.apikey_regular,
356 'edit_comment',
357 comment_id=comment_id,
358 message='just message',
359 version=0,
360 userid=TEST_USER_ADMIN_LOGIN
361 )
362 response = api_call(self.app, params)
363 expected = 'userid is not the same as your user'
364 assert_error(id_, expected, given=response.body)
365
366 @pytest.mark.backends("git", "hg")
367 def test_api_edit_comment_wrong_user_with_permissions_to_edit_comment(self, pr_util):
368 pull_request = pr_util.create_pull_request()
369 pull_request_id = pull_request.pull_request_id
370 id_, params = build_data(
371 self.apikey,
372 'comment_pull_request',
373 repoid=pull_request.target_repo.repo_name,
374 pullrequestid=pull_request_id,
375 message='test message'
376 )
377 response = api_call(self.app, params)
378 json_response = response.json
379 comment_id = json_response['result']['comment_id']
380
381 id_, params = build_data(
382 self.apikey_regular,
383 'edit_comment',
384 comment_id=comment_id,
385 message='just message',
386 version=0,
387 )
388 response = api_call(self.app, params)
389 expected = "you don't have access to edit this comment"
390 assert_error(id_, expected, given=response.body)
@@ -1,61 +1,61 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23
23
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
24 from rhodecode.api.tests.utils import build_data, api_call, assert_ok
25
25
26
26
27 @pytest.mark.usefixtures("testuser_api", "app")
27 @pytest.mark.usefixtures("testuser_api", "app")
28 class TestGetMethod(object):
28 class TestGetMethod(object):
29 def test_get_methods_no_matches(self):
29 def test_get_methods_no_matches(self):
30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
30 id_, params = build_data(self.apikey, 'get_method', pattern='hello')
31 response = api_call(self.app, params)
31 response = api_call(self.app, params)
32
32
33 expected = []
33 expected = []
34 assert_ok(id_, expected, given=response.body)
34 assert_ok(id_, expected, given=response.body)
35
35
36 def test_get_methods(self):
36 def test_get_methods(self):
37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
38 response = api_call(self.app, params)
38 response = api_call(self.app, params)
39
39
40 expected = ['changeset_comment', 'comment_pull_request',
40 expected = ['changeset_comment', 'comment_pull_request', 'edit_comment',
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
42 assert_ok(id_, expected, given=response.body)
42 assert_ok(id_, expected, given=response.body)
43
43
44 def test_get_methods_on_single_match(self):
44 def test_get_methods_on_single_match(self):
45 id_, params = build_data(self.apikey, 'get_method',
45 id_, params = build_data(self.apikey, 'get_method',
46 pattern='*comment_commit*')
46 pattern='*comment_commit*')
47 response = api_call(self.app, params)
47 response = api_call(self.app, params)
48
48
49 expected = ['comment_commit',
49 expected = ['comment_commit',
50 {'apiuser': '<RequiredType>',
50 {'apiuser': '<RequiredType>',
51 'comment_type': "<Optional:u'note'>",
51 'comment_type': "<Optional:u'note'>",
52 'commit_id': '<RequiredType>',
52 'commit_id': '<RequiredType>',
53 'extra_recipients': '<Optional:[]>',
53 'extra_recipients': '<Optional:[]>',
54 'message': '<RequiredType>',
54 'message': '<RequiredType>',
55 'repoid': '<RequiredType>',
55 'repoid': '<RequiredType>',
56 'request': '<RequiredType>',
56 'request': '<RequiredType>',
57 'resolves_comment_id': '<Optional:None>',
57 'resolves_comment_id': '<Optional:None>',
58 'status': '<Optional:None>',
58 'status': '<Optional:None>',
59 'userid': '<Optional:<OptionalAttr:apiuser>>',
59 'userid': '<Optional:<OptionalAttr:apiuser>>',
60 'send_email': '<Optional:True>'}]
60 'send_email': '<Optional:True>'}]
61 assert_ok(id_, expected, given=response.body)
61 assert_ok(id_, expected, given=response.body)
@@ -1,1018 +1,1092 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.exceptions import CommentVersionMismatch
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
47 merge_state=Optional(False)):
47 merge_state=Optional(False)):
48 """
48 """
49 Get a pull request based on the given ID.
49 Get a pull request based on the given ID.
50
50
51 :param apiuser: This is filled automatically from the |authtoken|.
51 :param apiuser: This is filled automatically from the |authtoken|.
52 :type apiuser: AuthUser
52 :type apiuser: AuthUser
53 :param repoid: Optional, repository name or repository ID from where
53 :param repoid: Optional, repository name or repository ID from where
54 the pull request was opened.
54 the pull request was opened.
55 :type repoid: str or int
55 :type repoid: str or int
56 :param pullrequestid: ID of the requested pull request.
56 :param pullrequestid: ID of the requested pull request.
57 :type pullrequestid: int
57 :type pullrequestid: int
58 :param merge_state: Optional calculate merge state for each repository.
58 :param merge_state: Optional calculate merge state for each repository.
59 This could result in longer time to fetch the data
59 This could result in longer time to fetch the data
60 :type merge_state: bool
60 :type merge_state: bool
61
61
62 Example output:
62 Example output:
63
63
64 .. code-block:: bash
64 .. code-block:: bash
65
65
66 "id": <id_given_in_input>,
66 "id": <id_given_in_input>,
67 "result":
67 "result":
68 {
68 {
69 "pull_request_id": "<pull_request_id>",
69 "pull_request_id": "<pull_request_id>",
70 "url": "<url>",
70 "url": "<url>",
71 "title": "<title>",
71 "title": "<title>",
72 "description": "<description>",
72 "description": "<description>",
73 "status" : "<status>",
73 "status" : "<status>",
74 "created_on": "<date_time_created>",
74 "created_on": "<date_time_created>",
75 "updated_on": "<date_time_updated>",
75 "updated_on": "<date_time_updated>",
76 "versions": "<number_or_versions_of_pr>",
76 "versions": "<number_or_versions_of_pr>",
77 "commit_ids": [
77 "commit_ids": [
78 ...
78 ...
79 "<commit_id>",
79 "<commit_id>",
80 "<commit_id>",
80 "<commit_id>",
81 ...
81 ...
82 ],
82 ],
83 "review_status": "<review_status>",
83 "review_status": "<review_status>",
84 "mergeable": {
84 "mergeable": {
85 "status": "<bool>",
85 "status": "<bool>",
86 "message": "<message>",
86 "message": "<message>",
87 },
87 },
88 "source": {
88 "source": {
89 "clone_url": "<clone_url>",
89 "clone_url": "<clone_url>",
90 "repository": "<repository_name>",
90 "repository": "<repository_name>",
91 "reference":
91 "reference":
92 {
92 {
93 "name": "<name>",
93 "name": "<name>",
94 "type": "<type>",
94 "type": "<type>",
95 "commit_id": "<commit_id>",
95 "commit_id": "<commit_id>",
96 }
96 }
97 },
97 },
98 "target": {
98 "target": {
99 "clone_url": "<clone_url>",
99 "clone_url": "<clone_url>",
100 "repository": "<repository_name>",
100 "repository": "<repository_name>",
101 "reference":
101 "reference":
102 {
102 {
103 "name": "<name>",
103 "name": "<name>",
104 "type": "<type>",
104 "type": "<type>",
105 "commit_id": "<commit_id>",
105 "commit_id": "<commit_id>",
106 }
106 }
107 },
107 },
108 "merge": {
108 "merge": {
109 "clone_url": "<clone_url>",
109 "clone_url": "<clone_url>",
110 "reference":
110 "reference":
111 {
111 {
112 "name": "<name>",
112 "name": "<name>",
113 "type": "<type>",
113 "type": "<type>",
114 "commit_id": "<commit_id>",
114 "commit_id": "<commit_id>",
115 }
115 }
116 },
116 },
117 "author": <user_obj>,
117 "author": <user_obj>,
118 "reviewers": [
118 "reviewers": [
119 ...
119 ...
120 {
120 {
121 "user": "<user_obj>",
121 "user": "<user_obj>",
122 "review_status": "<review_status>",
122 "review_status": "<review_status>",
123 }
123 }
124 ...
124 ...
125 ]
125 ]
126 },
126 },
127 "error": null
127 "error": null
128 """
128 """
129
129
130 pull_request = get_pull_request_or_error(pullrequestid)
130 pull_request = get_pull_request_or_error(pullrequestid)
131 if Optional.extract(repoid):
131 if Optional.extract(repoid):
132 repo = get_repo_or_error(repoid)
132 repo = get_repo_or_error(repoid)
133 else:
133 else:
134 repo = pull_request.target_repo
134 repo = pull_request.target_repo
135
135
136 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
136 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
137 raise JSONRPCError('repository `%s` or pull request `%s` '
137 raise JSONRPCError('repository `%s` or pull request `%s` '
138 'does not exist' % (repoid, pullrequestid))
138 'does not exist' % (repoid, pullrequestid))
139
139
140 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
140 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
141 # otherwise we can lock the repo on calculation of merge state while update/merge
141 # otherwise we can lock the repo on calculation of merge state while update/merge
142 # is happening.
142 # is happening.
143 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
143 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
144 merge_state = Optional.extract(merge_state, binary=True) and pr_created
144 merge_state = Optional.extract(merge_state, binary=True) and pr_created
145 data = pull_request.get_api_data(with_merge_state=merge_state)
145 data = pull_request.get_api_data(with_merge_state=merge_state)
146 return data
146 return data
147
147
148
148
149 @jsonrpc_method()
149 @jsonrpc_method()
150 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
150 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
151 merge_state=Optional(False)):
151 merge_state=Optional(False)):
152 """
152 """
153 Get all pull requests from the repository specified in `repoid`.
153 Get all pull requests from the repository specified in `repoid`.
154
154
155 :param apiuser: This is filled automatically from the |authtoken|.
155 :param apiuser: This is filled automatically from the |authtoken|.
156 :type apiuser: AuthUser
156 :type apiuser: AuthUser
157 :param repoid: Optional repository name or repository ID.
157 :param repoid: Optional repository name or repository ID.
158 :type repoid: str or int
158 :type repoid: str or int
159 :param status: Only return pull requests with the specified status.
159 :param status: Only return pull requests with the specified status.
160 Valid options are.
160 Valid options are.
161 * ``new`` (default)
161 * ``new`` (default)
162 * ``open``
162 * ``open``
163 * ``closed``
163 * ``closed``
164 :type status: str
164 :type status: str
165 :param merge_state: Optional calculate merge state for each repository.
165 :param merge_state: Optional calculate merge state for each repository.
166 This could result in longer time to fetch the data
166 This could result in longer time to fetch the data
167 :type merge_state: bool
167 :type merge_state: bool
168
168
169 Example output:
169 Example output:
170
170
171 .. code-block:: bash
171 .. code-block:: bash
172
172
173 "id": <id_given_in_input>,
173 "id": <id_given_in_input>,
174 "result":
174 "result":
175 [
175 [
176 ...
176 ...
177 {
177 {
178 "pull_request_id": "<pull_request_id>",
178 "pull_request_id": "<pull_request_id>",
179 "url": "<url>",
179 "url": "<url>",
180 "title" : "<title>",
180 "title" : "<title>",
181 "description": "<description>",
181 "description": "<description>",
182 "status": "<status>",
182 "status": "<status>",
183 "created_on": "<date_time_created>",
183 "created_on": "<date_time_created>",
184 "updated_on": "<date_time_updated>",
184 "updated_on": "<date_time_updated>",
185 "commit_ids": [
185 "commit_ids": [
186 ...
186 ...
187 "<commit_id>",
187 "<commit_id>",
188 "<commit_id>",
188 "<commit_id>",
189 ...
189 ...
190 ],
190 ],
191 "review_status": "<review_status>",
191 "review_status": "<review_status>",
192 "mergeable": {
192 "mergeable": {
193 "status": "<bool>",
193 "status": "<bool>",
194 "message: "<message>",
194 "message: "<message>",
195 },
195 },
196 "source": {
196 "source": {
197 "clone_url": "<clone_url>",
197 "clone_url": "<clone_url>",
198 "reference":
198 "reference":
199 {
199 {
200 "name": "<name>",
200 "name": "<name>",
201 "type": "<type>",
201 "type": "<type>",
202 "commit_id": "<commit_id>",
202 "commit_id": "<commit_id>",
203 }
203 }
204 },
204 },
205 "target": {
205 "target": {
206 "clone_url": "<clone_url>",
206 "clone_url": "<clone_url>",
207 "reference":
207 "reference":
208 {
208 {
209 "name": "<name>",
209 "name": "<name>",
210 "type": "<type>",
210 "type": "<type>",
211 "commit_id": "<commit_id>",
211 "commit_id": "<commit_id>",
212 }
212 }
213 },
213 },
214 "merge": {
214 "merge": {
215 "clone_url": "<clone_url>",
215 "clone_url": "<clone_url>",
216 "reference":
216 "reference":
217 {
217 {
218 "name": "<name>",
218 "name": "<name>",
219 "type": "<type>",
219 "type": "<type>",
220 "commit_id": "<commit_id>",
220 "commit_id": "<commit_id>",
221 }
221 }
222 },
222 },
223 "author": <user_obj>,
223 "author": <user_obj>,
224 "reviewers": [
224 "reviewers": [
225 ...
225 ...
226 {
226 {
227 "user": "<user_obj>",
227 "user": "<user_obj>",
228 "review_status": "<review_status>",
228 "review_status": "<review_status>",
229 }
229 }
230 ...
230 ...
231 ]
231 ]
232 }
232 }
233 ...
233 ...
234 ],
234 ],
235 "error": null
235 "error": null
236
236
237 """
237 """
238 repo = get_repo_or_error(repoid)
238 repo = get_repo_or_error(repoid)
239 if not has_superadmin_permission(apiuser):
239 if not has_superadmin_permission(apiuser):
240 _perms = (
240 _perms = (
241 'repository.admin', 'repository.write', 'repository.read',)
241 'repository.admin', 'repository.write', 'repository.read',)
242 validate_repo_permissions(apiuser, repoid, repo, _perms)
242 validate_repo_permissions(apiuser, repoid, repo, _perms)
243
243
244 status = Optional.extract(status)
244 status = Optional.extract(status)
245 merge_state = Optional.extract(merge_state, binary=True)
245 merge_state = Optional.extract(merge_state, binary=True)
246 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
246 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
247 order_by='id', order_dir='desc')
247 order_by='id', order_dir='desc')
248 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
248 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
249 return data
249 return data
250
250
251
251
252 @jsonrpc_method()
252 @jsonrpc_method()
253 def merge_pull_request(
253 def merge_pull_request(
254 request, apiuser, pullrequestid, repoid=Optional(None),
254 request, apiuser, pullrequestid, repoid=Optional(None),
255 userid=Optional(OAttr('apiuser'))):
255 userid=Optional(OAttr('apiuser'))):
256 """
256 """
257 Merge the pull request specified by `pullrequestid` into its target
257 Merge the pull request specified by `pullrequestid` into its target
258 repository.
258 repository.
259
259
260 :param apiuser: This is filled automatically from the |authtoken|.
260 :param apiuser: This is filled automatically from the |authtoken|.
261 :type apiuser: AuthUser
261 :type apiuser: AuthUser
262 :param repoid: Optional, repository name or repository ID of the
262 :param repoid: Optional, repository name or repository ID of the
263 target repository to which the |pr| is to be merged.
263 target repository to which the |pr| is to be merged.
264 :type repoid: str or int
264 :type repoid: str or int
265 :param pullrequestid: ID of the pull request which shall be merged.
265 :param pullrequestid: ID of the pull request which shall be merged.
266 :type pullrequestid: int
266 :type pullrequestid: int
267 :param userid: Merge the pull request as this user.
267 :param userid: Merge the pull request as this user.
268 :type userid: Optional(str or int)
268 :type userid: Optional(str or int)
269
269
270 Example output:
270 Example output:
271
271
272 .. code-block:: bash
272 .. code-block:: bash
273
273
274 "id": <id_given_in_input>,
274 "id": <id_given_in_input>,
275 "result": {
275 "result": {
276 "executed": "<bool>",
276 "executed": "<bool>",
277 "failure_reason": "<int>",
277 "failure_reason": "<int>",
278 "merge_status_message": "<str>",
278 "merge_status_message": "<str>",
279 "merge_commit_id": "<merge_commit_id>",
279 "merge_commit_id": "<merge_commit_id>",
280 "possible": "<bool>",
280 "possible": "<bool>",
281 "merge_ref": {
281 "merge_ref": {
282 "commit_id": "<commit_id>",
282 "commit_id": "<commit_id>",
283 "type": "<type>",
283 "type": "<type>",
284 "name": "<name>"
284 "name": "<name>"
285 }
285 }
286 },
286 },
287 "error": null
287 "error": null
288 """
288 """
289 pull_request = get_pull_request_or_error(pullrequestid)
289 pull_request = get_pull_request_or_error(pullrequestid)
290 if Optional.extract(repoid):
290 if Optional.extract(repoid):
291 repo = get_repo_or_error(repoid)
291 repo = get_repo_or_error(repoid)
292 else:
292 else:
293 repo = pull_request.target_repo
293 repo = pull_request.target_repo
294 auth_user = apiuser
294 auth_user = apiuser
295
295 if not isinstance(userid, Optional):
296 if not isinstance(userid, Optional):
296 if (has_superadmin_permission(apiuser) or
297 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
297 HasRepoPermissionAnyApi('repository.admin')(
298 user=apiuser, repo_name=repo.repo_name)
298 user=apiuser, repo_name=repo.repo_name)):
299 if has_superadmin_permission(apiuser) or is_repo_admin:
299 apiuser = get_user_or_error(userid)
300 apiuser = get_user_or_error(userid)
300 auth_user = apiuser.AuthUser()
301 auth_user = apiuser.AuthUser()
301 else:
302 else:
302 raise JSONRPCError('userid is not the same as your user')
303 raise JSONRPCError('userid is not the same as your user')
303
304
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
305 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
305 raise JSONRPCError(
306 raise JSONRPCError(
306 'Operation forbidden because pull request is in state {}, '
307 'Operation forbidden because pull request is in state {}, '
307 'only state {} is allowed.'.format(
308 'only state {} is allowed.'.format(
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
309 pull_request.pull_request_state, PullRequest.STATE_CREATED))
309
310
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
311 with pull_request.set_state(PullRequest.STATE_UPDATING):
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
312 check = MergeCheck.validate(pull_request, auth_user=auth_user,
312 translator=request.translate)
313 translator=request.translate)
313 merge_possible = not check.failed
314 merge_possible = not check.failed
314
315
315 if not merge_possible:
316 if not merge_possible:
316 error_messages = []
317 error_messages = []
317 for err_type, error_msg in check.errors:
318 for err_type, error_msg in check.errors:
318 error_msg = request.translate(error_msg)
319 error_msg = request.translate(error_msg)
319 error_messages.append(error_msg)
320 error_messages.append(error_msg)
320
321
321 reasons = ','.join(error_messages)
322 reasons = ','.join(error_messages)
322 raise JSONRPCError(
323 raise JSONRPCError(
323 'merge not possible for following reasons: {}'.format(reasons))
324 'merge not possible for following reasons: {}'.format(reasons))
324
325
325 target_repo = pull_request.target_repo
326 target_repo = pull_request.target_repo
326 extras = vcs_operation_context(
327 extras = vcs_operation_context(
327 request.environ, repo_name=target_repo.repo_name,
328 request.environ, repo_name=target_repo.repo_name,
328 username=auth_user.username, action='push',
329 username=auth_user.username, action='push',
329 scm=target_repo.repo_type)
330 scm=target_repo.repo_type)
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
331 with pull_request.set_state(PullRequest.STATE_UPDATING):
331 merge_response = PullRequestModel().merge_repo(
332 merge_response = PullRequestModel().merge_repo(
332 pull_request, apiuser, extras=extras)
333 pull_request, apiuser, extras=extras)
333 if merge_response.executed:
334 if merge_response.executed:
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
335 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
335
336
336 Session().commit()
337 Session().commit()
337
338
338 # In previous versions the merge response directly contained the merge
339 # In previous versions the merge response directly contained the merge
339 # commit id. It is now contained in the merge reference object. To be
340 # commit id. It is now contained in the merge reference object. To be
340 # backwards compatible we have to extract it again.
341 # backwards compatible we have to extract it again.
341 merge_response = merge_response.asdict()
342 merge_response = merge_response.asdict()
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
343 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
343
344
344 return merge_response
345 return merge_response
345
346
346
347
347 @jsonrpc_method()
348 @jsonrpc_method()
348 def get_pull_request_comments(
349 def get_pull_request_comments(
349 request, apiuser, pullrequestid, repoid=Optional(None)):
350 request, apiuser, pullrequestid, repoid=Optional(None)):
350 """
351 """
351 Get all comments of pull request specified with the `pullrequestid`
352 Get all comments of pull request specified with the `pullrequestid`
352
353
353 :param apiuser: This is filled automatically from the |authtoken|.
354 :param apiuser: This is filled automatically from the |authtoken|.
354 :type apiuser: AuthUser
355 :type apiuser: AuthUser
355 :param repoid: Optional repository name or repository ID.
356 :param repoid: Optional repository name or repository ID.
356 :type repoid: str or int
357 :type repoid: str or int
357 :param pullrequestid: The pull request ID.
358 :param pullrequestid: The pull request ID.
358 :type pullrequestid: int
359 :type pullrequestid: int
359
360
360 Example output:
361 Example output:
361
362
362 .. code-block:: bash
363 .. code-block:: bash
363
364
364 id : <id_given_in_input>
365 id : <id_given_in_input>
365 result : [
366 result : [
366 {
367 {
367 "comment_author": {
368 "comment_author": {
368 "active": true,
369 "active": true,
369 "full_name_or_username": "Tom Gore",
370 "full_name_or_username": "Tom Gore",
370 "username": "admin"
371 "username": "admin"
371 },
372 },
372 "comment_created_on": "2017-01-02T18:43:45.533",
373 "comment_created_on": "2017-01-02T18:43:45.533",
373 "comment_f_path": null,
374 "comment_f_path": null,
374 "comment_id": 25,
375 "comment_id": 25,
375 "comment_lineno": null,
376 "comment_lineno": null,
376 "comment_status": {
377 "comment_status": {
377 "status": "under_review",
378 "status": "under_review",
378 "status_lbl": "Under Review"
379 "status_lbl": "Under Review"
379 },
380 },
380 "comment_text": "Example text",
381 "comment_text": "Example text",
381 "comment_type": null,
382 "comment_type": null,
382 "pull_request_version": null,
383 "pull_request_version": null,
383 "comment_commit_id": None,
384 "comment_commit_id": None,
384 "comment_pull_request_id": <pull_request_id>
385 "comment_pull_request_id": <pull_request_id>
385 }
386 }
386 ],
387 ],
387 error : null
388 error : null
388 """
389 """
389
390
390 pull_request = get_pull_request_or_error(pullrequestid)
391 pull_request = get_pull_request_or_error(pullrequestid)
391 if Optional.extract(repoid):
392 if Optional.extract(repoid):
392 repo = get_repo_or_error(repoid)
393 repo = get_repo_or_error(repoid)
393 else:
394 else:
394 repo = pull_request.target_repo
395 repo = pull_request.target_repo
395
396
396 if not PullRequestModel().check_user_read(
397 if not PullRequestModel().check_user_read(
397 pull_request, apiuser, api=True):
398 pull_request, apiuser, api=True):
398 raise JSONRPCError('repository `%s` or pull request `%s` '
399 raise JSONRPCError('repository `%s` or pull request `%s` '
399 'does not exist' % (repoid, pullrequestid))
400 'does not exist' % (repoid, pullrequestid))
400
401
401 (pull_request_latest,
402 (pull_request_latest,
402 pull_request_at_ver,
403 pull_request_at_ver,
403 pull_request_display_obj,
404 pull_request_display_obj,
404 at_version) = PullRequestModel().get_pr_version(
405 at_version) = PullRequestModel().get_pr_version(
405 pull_request.pull_request_id, version=None)
406 pull_request.pull_request_id, version=None)
406
407
407 versions = pull_request_display_obj.versions()
408 versions = pull_request_display_obj.versions()
408 ver_map = {
409 ver_map = {
409 ver.pull_request_version_id: cnt
410 ver.pull_request_version_id: cnt
410 for cnt, ver in enumerate(versions, 1)
411 for cnt, ver in enumerate(versions, 1)
411 }
412 }
412
413
413 # GENERAL COMMENTS with versions #
414 # GENERAL COMMENTS with versions #
414 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
415 q = q.order_by(ChangesetComment.comment_id.asc())
416 q = q.order_by(ChangesetComment.comment_id.asc())
416 general_comments = q.all()
417 general_comments = q.all()
417
418
418 # INLINE COMMENTS with versions #
419 # INLINE COMMENTS with versions #
419 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
420 q = q.order_by(ChangesetComment.comment_id.asc())
421 q = q.order_by(ChangesetComment.comment_id.asc())
421 inline_comments = q.all()
422 inline_comments = q.all()
422
423
423 data = []
424 data = []
424 for comment in inline_comments + general_comments:
425 for comment in inline_comments + general_comments:
425 full_data = comment.get_api_data()
426 full_data = comment.get_api_data()
426 pr_version_id = None
427 pr_version_id = None
427 if comment.pull_request_version_id:
428 if comment.pull_request_version_id:
428 pr_version_id = 'v{}'.format(
429 pr_version_id = 'v{}'.format(
429 ver_map[comment.pull_request_version_id])
430 ver_map[comment.pull_request_version_id])
430
431
431 # sanitize some entries
432 # sanitize some entries
432
433
433 full_data['pull_request_version'] = pr_version_id
434 full_data['pull_request_version'] = pr_version_id
434 full_data['comment_author'] = {
435 full_data['comment_author'] = {
435 'username': full_data['comment_author'].username,
436 'username': full_data['comment_author'].username,
436 'full_name_or_username': full_data['comment_author'].full_name_or_username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
437 'active': full_data['comment_author'].active,
438 'active': full_data['comment_author'].active,
438 }
439 }
439
440
440 if full_data['comment_status']:
441 if full_data['comment_status']:
441 full_data['comment_status'] = {
442 full_data['comment_status'] = {
442 'status': full_data['comment_status'][0].status,
443 'status': full_data['comment_status'][0].status,
443 'status_lbl': full_data['comment_status'][0].status_lbl,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
444 }
445 }
445 else:
446 else:
446 full_data['comment_status'] = {}
447 full_data['comment_status'] = {}
447
448
448 data.append(full_data)
449 data.append(full_data)
449 return data
450 return data
450
451
451
452
452 @jsonrpc_method()
453 @jsonrpc_method()
453 def comment_pull_request(
454 def comment_pull_request(
454 request, apiuser, pullrequestid, repoid=Optional(None),
455 request, apiuser, pullrequestid, repoid=Optional(None),
455 message=Optional(None), commit_id=Optional(None), status=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
456 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
457 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
458 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
459 """
460 """
460 Comment on the pull request specified with the `pullrequestid`,
461 Comment on the pull request specified with the `pullrequestid`,
461 in the |repo| specified by the `repoid`, and optionally change the
462 in the |repo| specified by the `repoid`, and optionally change the
462 review status.
463 review status.
463
464
464 :param apiuser: This is filled automatically from the |authtoken|.
465 :param apiuser: This is filled automatically from the |authtoken|.
465 :type apiuser: AuthUser
466 :type apiuser: AuthUser
466 :param repoid: Optional repository name or repository ID.
467 :param repoid: Optional repository name or repository ID.
467 :type repoid: str or int
468 :type repoid: str or int
468 :param pullrequestid: The pull request ID.
469 :param pullrequestid: The pull request ID.
469 :type pullrequestid: int
470 :type pullrequestid: int
470 :param commit_id: Specify the commit_id for which to set a comment. If
471 :param commit_id: Specify the commit_id for which to set a comment. If
471 given commit_id is different than latest in the PR status
472 given commit_id is different than latest in the PR status
472 change won't be performed.
473 change won't be performed.
473 :type commit_id: str
474 :type commit_id: str
474 :param message: The text content of the comment.
475 :param message: The text content of the comment.
475 :type message: str
476 :type message: str
476 :param status: (**Optional**) Set the approval status of the pull
477 :param status: (**Optional**) Set the approval status of the pull
477 request. One of: 'not_reviewed', 'approved', 'rejected',
478 request. One of: 'not_reviewed', 'approved', 'rejected',
478 'under_review'
479 'under_review'
479 :type status: str
480 :type status: str
480 :param comment_type: Comment type, one of: 'note', 'todo'
481 :param comment_type: Comment type, one of: 'note', 'todo'
481 :type comment_type: Optional(str), default: 'note'
482 :type comment_type: Optional(str), default: 'note'
482 :param resolves_comment_id: id of comment which this one will resolve
483 :param resolves_comment_id: id of comment which this one will resolve
483 :type resolves_comment_id: Optional(int)
484 :type resolves_comment_id: Optional(int)
484 :param extra_recipients: list of user ids or usernames to add
485 :param extra_recipients: list of user ids or usernames to add
485 notifications for this comment. Acts like a CC for notification
486 notifications for this comment. Acts like a CC for notification
486 :type extra_recipients: Optional(list)
487 :type extra_recipients: Optional(list)
487 :param userid: Comment on the pull request as this user
488 :param userid: Comment on the pull request as this user
488 :type userid: Optional(str or int)
489 :type userid: Optional(str or int)
489 :param send_email: Define if this comment should also send email notification
490 :param send_email: Define if this comment should also send email notification
490 :type send_email: Optional(bool)
491 :type send_email: Optional(bool)
491
492
492 Example output:
493 Example output:
493
494
494 .. code-block:: bash
495 .. code-block:: bash
495
496
496 id : <id_given_in_input>
497 id : <id_given_in_input>
497 result : {
498 result : {
498 "pull_request_id": "<Integer>",
499 "pull_request_id": "<Integer>",
499 "comment_id": "<Integer>",
500 "comment_id": "<Integer>",
500 "status": {"given": <given_status>,
501 "status": {"given": <given_status>,
501 "was_changed": <bool status_was_actually_changed> },
502 "was_changed": <bool status_was_actually_changed> },
502 },
503 },
503 error : null
504 error : null
504 """
505 """
505 pull_request = get_pull_request_or_error(pullrequestid)
506 pull_request = get_pull_request_or_error(pullrequestid)
506 if Optional.extract(repoid):
507 if Optional.extract(repoid):
507 repo = get_repo_or_error(repoid)
508 repo = get_repo_or_error(repoid)
508 else:
509 else:
509 repo = pull_request.target_repo
510 repo = pull_request.target_repo
510
511
511 auth_user = apiuser
512 auth_user = apiuser
512 if not isinstance(userid, Optional):
513 if not isinstance(userid, Optional):
513 if (has_superadmin_permission(apiuser) or
514 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
514 HasRepoPermissionAnyApi('repository.admin')(
515 user=apiuser, repo_name=repo.repo_name)
515 user=apiuser, repo_name=repo.repo_name)):
516 if has_superadmin_permission(apiuser) or is_repo_admin:
516 apiuser = get_user_or_error(userid)
517 apiuser = get_user_or_error(userid)
517 auth_user = apiuser.AuthUser()
518 auth_user = apiuser.AuthUser()
518 else:
519 else:
519 raise JSONRPCError('userid is not the same as your user')
520 raise JSONRPCError('userid is not the same as your user')
520
521
521 if pull_request.is_closed():
522 if pull_request.is_closed():
522 raise JSONRPCError(
523 raise JSONRPCError(
523 'pull request `%s` comment failed, pull request is closed' % (
524 'pull request `%s` comment failed, pull request is closed' % (
524 pullrequestid,))
525 pullrequestid,))
525
526
526 if not PullRequestModel().check_user_read(
527 if not PullRequestModel().check_user_read(
527 pull_request, apiuser, api=True):
528 pull_request, apiuser, api=True):
528 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
529 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
529 message = Optional.extract(message)
530 message = Optional.extract(message)
530 status = Optional.extract(status)
531 status = Optional.extract(status)
531 commit_id = Optional.extract(commit_id)
532 commit_id = Optional.extract(commit_id)
532 comment_type = Optional.extract(comment_type)
533 comment_type = Optional.extract(comment_type)
533 resolves_comment_id = Optional.extract(resolves_comment_id)
534 resolves_comment_id = Optional.extract(resolves_comment_id)
534 extra_recipients = Optional.extract(extra_recipients)
535 extra_recipients = Optional.extract(extra_recipients)
535 send_email = Optional.extract(send_email, binary=True)
536 send_email = Optional.extract(send_email, binary=True)
536
537
537 if not message and not status:
538 if not message and not status:
538 raise JSONRPCError(
539 raise JSONRPCError(
539 'Both message and status parameters are missing. '
540 'Both message and status parameters are missing. '
540 'At least one is required.')
541 'At least one is required.')
541
542
542 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
543 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
543 status is not None):
544 status is not None):
544 raise JSONRPCError('Unknown comment status: `%s`' % status)
545 raise JSONRPCError('Unknown comment status: `%s`' % status)
545
546
546 if commit_id and commit_id not in pull_request.revisions:
547 if commit_id and commit_id not in pull_request.revisions:
547 raise JSONRPCError(
548 raise JSONRPCError(
548 'Invalid commit_id `%s` for this pull request.' % commit_id)
549 'Invalid commit_id `%s` for this pull request.' % commit_id)
549
550
550 allowed_to_change_status = PullRequestModel().check_user_change_status(
551 allowed_to_change_status = PullRequestModel().check_user_change_status(
551 pull_request, apiuser)
552 pull_request, apiuser)
552
553
553 # if commit_id is passed re-validated if user is allowed to change status
554 # if commit_id is passed re-validated if user is allowed to change status
554 # based on latest commit_id from the PR
555 # based on latest commit_id from the PR
555 if commit_id:
556 if commit_id:
556 commit_idx = pull_request.revisions.index(commit_id)
557 commit_idx = pull_request.revisions.index(commit_id)
557 if commit_idx != 0:
558 if commit_idx != 0:
558 allowed_to_change_status = False
559 allowed_to_change_status = False
559
560
560 if resolves_comment_id:
561 if resolves_comment_id:
561 comment = ChangesetComment.get(resolves_comment_id)
562 comment = ChangesetComment.get(resolves_comment_id)
562 if not comment:
563 if not comment:
563 raise JSONRPCError(
564 raise JSONRPCError(
564 'Invalid resolves_comment_id `%s` for this pull request.'
565 'Invalid resolves_comment_id `%s` for this pull request.'
565 % resolves_comment_id)
566 % resolves_comment_id)
566 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
567 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
567 raise JSONRPCError(
568 raise JSONRPCError(
568 'Comment `%s` is wrong type for setting status to resolved.'
569 'Comment `%s` is wrong type for setting status to resolved.'
569 % resolves_comment_id)
570 % resolves_comment_id)
570
571
571 text = message
572 text = message
572 status_label = ChangesetStatus.get_status_lbl(status)
573 status_label = ChangesetStatus.get_status_lbl(status)
573 if status and allowed_to_change_status:
574 if status and allowed_to_change_status:
574 st_message = ('Status change %(transition_icon)s %(status)s'
575 st_message = ('Status change %(transition_icon)s %(status)s'
575 % {'transition_icon': '>', 'status': status_label})
576 % {'transition_icon': '>', 'status': status_label})
576 text = message or st_message
577 text = message or st_message
577
578
578 rc_config = SettingsModel().get_all_settings()
579 rc_config = SettingsModel().get_all_settings()
579 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
580 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
580
581
581 status_change = status and allowed_to_change_status
582 status_change = status and allowed_to_change_status
582 comment = CommentsModel().create(
583 comment = CommentsModel().create(
583 text=text,
584 text=text,
584 repo=pull_request.target_repo.repo_id,
585 repo=pull_request.target_repo.repo_id,
585 user=apiuser.user_id,
586 user=apiuser.user_id,
586 pull_request=pull_request.pull_request_id,
587 pull_request=pull_request.pull_request_id,
587 f_path=None,
588 f_path=None,
588 line_no=None,
589 line_no=None,
589 status_change=(status_label if status_change else None),
590 status_change=(status_label if status_change else None),
590 status_change_type=(status if status_change else None),
591 status_change_type=(status if status_change else None),
591 closing_pr=False,
592 closing_pr=False,
592 renderer=renderer,
593 renderer=renderer,
593 comment_type=comment_type,
594 comment_type=comment_type,
594 resolves_comment_id=resolves_comment_id,
595 resolves_comment_id=resolves_comment_id,
595 auth_user=auth_user,
596 auth_user=auth_user,
596 extra_recipients=extra_recipients,
597 extra_recipients=extra_recipients,
597 send_email=send_email
598 send_email=send_email
598 )
599 )
599
600
600 if allowed_to_change_status and status:
601 if allowed_to_change_status and status:
601 old_calculated_status = pull_request.calculated_review_status()
602 old_calculated_status = pull_request.calculated_review_status()
602 ChangesetStatusModel().set_status(
603 ChangesetStatusModel().set_status(
603 pull_request.target_repo.repo_id,
604 pull_request.target_repo.repo_id,
604 status,
605 status,
605 apiuser.user_id,
606 apiuser.user_id,
606 comment,
607 comment,
607 pull_request=pull_request.pull_request_id
608 pull_request=pull_request.pull_request_id
608 )
609 )
609 Session().flush()
610 Session().flush()
610
611
611 Session().commit()
612 Session().commit()
612
613
613 PullRequestModel().trigger_pull_request_hook(
614 PullRequestModel().trigger_pull_request_hook(
614 pull_request, apiuser, 'comment',
615 pull_request, apiuser, 'comment',
615 data={'comment': comment})
616 data={'comment': comment})
616
617
617 if allowed_to_change_status and status:
618 if allowed_to_change_status and status:
618 # we now calculate the status of pull request, and based on that
619 # we now calculate the status of pull request, and based on that
619 # calculation we set the commits status
620 # calculation we set the commits status
620 calculated_status = pull_request.calculated_review_status()
621 calculated_status = pull_request.calculated_review_status()
621 if old_calculated_status != calculated_status:
622 if old_calculated_status != calculated_status:
622 PullRequestModel().trigger_pull_request_hook(
623 PullRequestModel().trigger_pull_request_hook(
623 pull_request, apiuser, 'review_status_change',
624 pull_request, apiuser, 'review_status_change',
624 data={'status': calculated_status})
625 data={'status': calculated_status})
625
626
626 data = {
627 data = {
627 'pull_request_id': pull_request.pull_request_id,
628 'pull_request_id': pull_request.pull_request_id,
628 'comment_id': comment.comment_id if comment else None,
629 'comment_id': comment.comment_id if comment else None,
629 'status': {'given': status, 'was_changed': status_change},
630 'status': {'given': status, 'was_changed': status_change},
630 }
631 }
631 return data
632 return data
632
633
633
634
634 @jsonrpc_method()
635 @jsonrpc_method()
636 def edit_comment(
637 request, apiuser, message, comment_id, version,
638 userid=Optional(OAttr('apiuser')),
639 ):
640 """
641 Edit comment on the pull request or commit,
642 specified by the `comment_id` and version. Initially version should be 0
643
644 :param apiuser: This is filled automatically from the |authtoken|.
645 :type apiuser: AuthUser
646 :param comment_id: Specify the comment_id for editing
647 :type comment_id: int
648 :param version: version of the comment that will be created, starts from 0
649 :type version: int
650 :param message: The text content of the comment.
651 :type message: str
652 :param userid: Comment on the pull request as this user
653 :type userid: Optional(str or int)
654
655 Example output:
656
657 .. code-block:: bash
658
659 id : <id_given_in_input>
660 result : {
661 "comment_history_id": "<Integer>",
662 "version": "<Integer>",
663 },
664 error : null
665 """
666
667 auth_user = apiuser
668 comment = ChangesetComment.get(comment_id)
669
670 is_super_admin = has_superadmin_permission(apiuser)
671 is_repo_admin = HasRepoPermissionAnyApi('repository.admin') \
672 (user=apiuser, repo_name=comment.repo.repo_name)
673
674 if not isinstance(userid, Optional):
675 if is_super_admin or is_repo_admin:
676 apiuser = get_user_or_error(userid)
677 auth_user = apiuser.AuthUser()
678 else:
679 raise JSONRPCError('userid is not the same as your user')
680
681 comment_author = comment.author.user_id == auth_user.user_id
682 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
683 raise JSONRPCError("you don't have access to edit this comment")
684
685 try:
686 comment_history = CommentsModel().edit(
687 comment_id=comment_id,
688 text=message,
689 auth_user=auth_user,
690 version=version,
691 )
692 Session().commit()
693 except CommentVersionMismatch:
694 raise JSONRPCError(
695 'comment ({}) version ({}) mismatch'.format(comment_id, version)
696 )
697 if not comment_history and not message:
698 raise JSONRPCError(
699 "comment ({}) can't be changed with empty string".format(comment_id)
700 )
701 data = {
702 'comment_history_id': comment_history.comment_history_id if comment_history else None,
703 'version': comment_history.version if comment_history else None,
704 }
705 return data
706
707
708 @jsonrpc_method()
635 def create_pull_request(
709 def create_pull_request(
636 request, apiuser, source_repo, target_repo, source_ref, target_ref,
710 request, apiuser, source_repo, target_repo, source_ref, target_ref,
637 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
711 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
638 description_renderer=Optional(''), reviewers=Optional(None)):
712 description_renderer=Optional(''), reviewers=Optional(None)):
639 """
713 """
640 Creates a new pull request.
714 Creates a new pull request.
641
715
642 Accepts refs in the following formats:
716 Accepts refs in the following formats:
643
717
644 * branch:<branch_name>:<sha>
718 * branch:<branch_name>:<sha>
645 * branch:<branch_name>
719 * branch:<branch_name>
646 * bookmark:<bookmark_name>:<sha> (Mercurial only)
720 * bookmark:<bookmark_name>:<sha> (Mercurial only)
647 * bookmark:<bookmark_name> (Mercurial only)
721 * bookmark:<bookmark_name> (Mercurial only)
648
722
649 :param apiuser: This is filled automatically from the |authtoken|.
723 :param apiuser: This is filled automatically from the |authtoken|.
650 :type apiuser: AuthUser
724 :type apiuser: AuthUser
651 :param source_repo: Set the source repository name.
725 :param source_repo: Set the source repository name.
652 :type source_repo: str
726 :type source_repo: str
653 :param target_repo: Set the target repository name.
727 :param target_repo: Set the target repository name.
654 :type target_repo: str
728 :type target_repo: str
655 :param source_ref: Set the source ref name.
729 :param source_ref: Set the source ref name.
656 :type source_ref: str
730 :type source_ref: str
657 :param target_ref: Set the target ref name.
731 :param target_ref: Set the target ref name.
658 :type target_ref: str
732 :type target_ref: str
659 :param owner: user_id or username
733 :param owner: user_id or username
660 :type owner: Optional(str)
734 :type owner: Optional(str)
661 :param title: Optionally Set the pull request title, it's generated otherwise
735 :param title: Optionally Set the pull request title, it's generated otherwise
662 :type title: str
736 :type title: str
663 :param description: Set the pull request description.
737 :param description: Set the pull request description.
664 :type description: Optional(str)
738 :type description: Optional(str)
665 :type description_renderer: Optional(str)
739 :type description_renderer: Optional(str)
666 :param description_renderer: Set pull request renderer for the description.
740 :param description_renderer: Set pull request renderer for the description.
667 It should be 'rst', 'markdown' or 'plain'. If not give default
741 It should be 'rst', 'markdown' or 'plain'. If not give default
668 system renderer will be used
742 system renderer will be used
669 :param reviewers: Set the new pull request reviewers list.
743 :param reviewers: Set the new pull request reviewers list.
670 Reviewer defined by review rules will be added automatically to the
744 Reviewer defined by review rules will be added automatically to the
671 defined list.
745 defined list.
672 :type reviewers: Optional(list)
746 :type reviewers: Optional(list)
673 Accepts username strings or objects of the format:
747 Accepts username strings or objects of the format:
674
748
675 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
749 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
676 """
750 """
677
751
678 source_db_repo = get_repo_or_error(source_repo)
752 source_db_repo = get_repo_or_error(source_repo)
679 target_db_repo = get_repo_or_error(target_repo)
753 target_db_repo = get_repo_or_error(target_repo)
680 if not has_superadmin_permission(apiuser):
754 if not has_superadmin_permission(apiuser):
681 _perms = ('repository.admin', 'repository.write', 'repository.read',)
755 _perms = ('repository.admin', 'repository.write', 'repository.read',)
682 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
756 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
683
757
684 owner = validate_set_owner_permissions(apiuser, owner)
758 owner = validate_set_owner_permissions(apiuser, owner)
685
759
686 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
760 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
687 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
761 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
688
762
689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
763 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
764 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
691
765
692 reviewer_objects = Optional.extract(reviewers) or []
766 reviewer_objects = Optional.extract(reviewers) or []
693
767
694 # serialize and validate passed in given reviewers
768 # serialize and validate passed in given reviewers
695 if reviewer_objects:
769 if reviewer_objects:
696 schema = ReviewerListSchema()
770 schema = ReviewerListSchema()
697 try:
771 try:
698 reviewer_objects = schema.deserialize(reviewer_objects)
772 reviewer_objects = schema.deserialize(reviewer_objects)
699 except Invalid as err:
773 except Invalid as err:
700 raise JSONRPCValidationError(colander_exc=err)
774 raise JSONRPCValidationError(colander_exc=err)
701
775
702 # validate users
776 # validate users
703 for reviewer_object in reviewer_objects:
777 for reviewer_object in reviewer_objects:
704 user = get_user_or_error(reviewer_object['username'])
778 user = get_user_or_error(reviewer_object['username'])
705 reviewer_object['user_id'] = user.user_id
779 reviewer_object['user_id'] = user.user_id
706
780
707 get_default_reviewers_data, validate_default_reviewers = \
781 get_default_reviewers_data, validate_default_reviewers = \
708 PullRequestModel().get_reviewer_functions()
782 PullRequestModel().get_reviewer_functions()
709
783
710 # recalculate reviewers logic, to make sure we can validate this
784 # recalculate reviewers logic, to make sure we can validate this
711 default_reviewers_data = get_default_reviewers_data(
785 default_reviewers_data = get_default_reviewers_data(
712 owner, source_db_repo,
786 owner, source_db_repo,
713 source_commit, target_db_repo, target_commit)
787 source_commit, target_db_repo, target_commit)
714
788
715 # now MERGE our given with the calculated
789 # now MERGE our given with the calculated
716 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
790 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
717
791
718 try:
792 try:
719 reviewers = validate_default_reviewers(
793 reviewers = validate_default_reviewers(
720 reviewer_objects, default_reviewers_data)
794 reviewer_objects, default_reviewers_data)
721 except ValueError as e:
795 except ValueError as e:
722 raise JSONRPCError('Reviewers Validation: {}'.format(e))
796 raise JSONRPCError('Reviewers Validation: {}'.format(e))
723
797
724 title = Optional.extract(title)
798 title = Optional.extract(title)
725 if not title:
799 if not title:
726 title_source_ref = source_ref.split(':', 2)[1]
800 title_source_ref = source_ref.split(':', 2)[1]
727 title = PullRequestModel().generate_pullrequest_title(
801 title = PullRequestModel().generate_pullrequest_title(
728 source=source_repo,
802 source=source_repo,
729 source_ref=title_source_ref,
803 source_ref=title_source_ref,
730 target=target_repo
804 target=target_repo
731 )
805 )
732
806
733 diff_info = default_reviewers_data['diff_info']
807 diff_info = default_reviewers_data['diff_info']
734 common_ancestor_id = diff_info['ancestor']
808 common_ancestor_id = diff_info['ancestor']
735 commits = diff_info['commits']
809 commits = diff_info['commits']
736
810
737 if not common_ancestor_id:
811 if not common_ancestor_id:
738 raise JSONRPCError('no common ancestor found')
812 raise JSONRPCError('no common ancestor found')
739
813
740 if not commits:
814 if not commits:
741 raise JSONRPCError('no commits found')
815 raise JSONRPCError('no commits found')
742
816
743 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
817 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
744 revisions = [commit.raw_id for commit in reversed(commits)]
818 revisions = [commit.raw_id for commit in reversed(commits)]
745
819
746 # recalculate target ref based on ancestor
820 # recalculate target ref based on ancestor
747 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
821 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
748 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
822 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
749
823
750 # fetch renderer, if set fallback to plain in case of PR
824 # fetch renderer, if set fallback to plain in case of PR
751 rc_config = SettingsModel().get_all_settings()
825 rc_config = SettingsModel().get_all_settings()
752 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
826 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
753 description = Optional.extract(description)
827 description = Optional.extract(description)
754 description_renderer = Optional.extract(description_renderer) or default_system_renderer
828 description_renderer = Optional.extract(description_renderer) or default_system_renderer
755
829
756 pull_request = PullRequestModel().create(
830 pull_request = PullRequestModel().create(
757 created_by=owner.user_id,
831 created_by=owner.user_id,
758 source_repo=source_repo,
832 source_repo=source_repo,
759 source_ref=full_source_ref,
833 source_ref=full_source_ref,
760 target_repo=target_repo,
834 target_repo=target_repo,
761 target_ref=full_target_ref,
835 target_ref=full_target_ref,
762 common_ancestor_id=common_ancestor_id,
836 common_ancestor_id=common_ancestor_id,
763 revisions=revisions,
837 revisions=revisions,
764 reviewers=reviewers,
838 reviewers=reviewers,
765 title=title,
839 title=title,
766 description=description,
840 description=description,
767 description_renderer=description_renderer,
841 description_renderer=description_renderer,
768 reviewer_data=default_reviewers_data,
842 reviewer_data=default_reviewers_data,
769 auth_user=apiuser
843 auth_user=apiuser
770 )
844 )
771
845
772 Session().commit()
846 Session().commit()
773 data = {
847 data = {
774 'msg': 'Created new pull request `{}`'.format(title),
848 'msg': 'Created new pull request `{}`'.format(title),
775 'pull_request_id': pull_request.pull_request_id,
849 'pull_request_id': pull_request.pull_request_id,
776 }
850 }
777 return data
851 return data
778
852
779
853
780 @jsonrpc_method()
854 @jsonrpc_method()
781 def update_pull_request(
855 def update_pull_request(
782 request, apiuser, pullrequestid, repoid=Optional(None),
856 request, apiuser, pullrequestid, repoid=Optional(None),
783 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
857 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
784 reviewers=Optional(None), update_commits=Optional(None)):
858 reviewers=Optional(None), update_commits=Optional(None)):
785 """
859 """
786 Updates a pull request.
860 Updates a pull request.
787
861
788 :param apiuser: This is filled automatically from the |authtoken|.
862 :param apiuser: This is filled automatically from the |authtoken|.
789 :type apiuser: AuthUser
863 :type apiuser: AuthUser
790 :param repoid: Optional repository name or repository ID.
864 :param repoid: Optional repository name or repository ID.
791 :type repoid: str or int
865 :type repoid: str or int
792 :param pullrequestid: The pull request ID.
866 :param pullrequestid: The pull request ID.
793 :type pullrequestid: int
867 :type pullrequestid: int
794 :param title: Set the pull request title.
868 :param title: Set the pull request title.
795 :type title: str
869 :type title: str
796 :param description: Update pull request description.
870 :param description: Update pull request description.
797 :type description: Optional(str)
871 :type description: Optional(str)
798 :type description_renderer: Optional(str)
872 :type description_renderer: Optional(str)
799 :param description_renderer: Update pull request renderer for the description.
873 :param description_renderer: Update pull request renderer for the description.
800 It should be 'rst', 'markdown' or 'plain'
874 It should be 'rst', 'markdown' or 'plain'
801 :param reviewers: Update pull request reviewers list with new value.
875 :param reviewers: Update pull request reviewers list with new value.
802 :type reviewers: Optional(list)
876 :type reviewers: Optional(list)
803 Accepts username strings or objects of the format:
877 Accepts username strings or objects of the format:
804
878
805 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
879 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
806
880
807 :param update_commits: Trigger update of commits for this pull request
881 :param update_commits: Trigger update of commits for this pull request
808 :type: update_commits: Optional(bool)
882 :type: update_commits: Optional(bool)
809
883
810 Example output:
884 Example output:
811
885
812 .. code-block:: bash
886 .. code-block:: bash
813
887
814 id : <id_given_in_input>
888 id : <id_given_in_input>
815 result : {
889 result : {
816 "msg": "Updated pull request `63`",
890 "msg": "Updated pull request `63`",
817 "pull_request": <pull_request_object>,
891 "pull_request": <pull_request_object>,
818 "updated_reviewers": {
892 "updated_reviewers": {
819 "added": [
893 "added": [
820 "username"
894 "username"
821 ],
895 ],
822 "removed": []
896 "removed": []
823 },
897 },
824 "updated_commits": {
898 "updated_commits": {
825 "added": [
899 "added": [
826 "<sha1_hash>"
900 "<sha1_hash>"
827 ],
901 ],
828 "common": [
902 "common": [
829 "<sha1_hash>",
903 "<sha1_hash>",
830 "<sha1_hash>",
904 "<sha1_hash>",
831 ],
905 ],
832 "removed": []
906 "removed": []
833 }
907 }
834 }
908 }
835 error : null
909 error : null
836 """
910 """
837
911
838 pull_request = get_pull_request_or_error(pullrequestid)
912 pull_request = get_pull_request_or_error(pullrequestid)
839 if Optional.extract(repoid):
913 if Optional.extract(repoid):
840 repo = get_repo_or_error(repoid)
914 repo = get_repo_or_error(repoid)
841 else:
915 else:
842 repo = pull_request.target_repo
916 repo = pull_request.target_repo
843
917
844 if not PullRequestModel().check_user_update(
918 if not PullRequestModel().check_user_update(
845 pull_request, apiuser, api=True):
919 pull_request, apiuser, api=True):
846 raise JSONRPCError(
920 raise JSONRPCError(
847 'pull request `%s` update failed, no permission to update.' % (
921 'pull request `%s` update failed, no permission to update.' % (
848 pullrequestid,))
922 pullrequestid,))
849 if pull_request.is_closed():
923 if pull_request.is_closed():
850 raise JSONRPCError(
924 raise JSONRPCError(
851 'pull request `%s` update failed, pull request is closed' % (
925 'pull request `%s` update failed, pull request is closed' % (
852 pullrequestid,))
926 pullrequestid,))
853
927
854 reviewer_objects = Optional.extract(reviewers) or []
928 reviewer_objects = Optional.extract(reviewers) or []
855
929
856 if reviewer_objects:
930 if reviewer_objects:
857 schema = ReviewerListSchema()
931 schema = ReviewerListSchema()
858 try:
932 try:
859 reviewer_objects = schema.deserialize(reviewer_objects)
933 reviewer_objects = schema.deserialize(reviewer_objects)
860 except Invalid as err:
934 except Invalid as err:
861 raise JSONRPCValidationError(colander_exc=err)
935 raise JSONRPCValidationError(colander_exc=err)
862
936
863 # validate users
937 # validate users
864 for reviewer_object in reviewer_objects:
938 for reviewer_object in reviewer_objects:
865 user = get_user_or_error(reviewer_object['username'])
939 user = get_user_or_error(reviewer_object['username'])
866 reviewer_object['user_id'] = user.user_id
940 reviewer_object['user_id'] = user.user_id
867
941
868 get_default_reviewers_data, get_validated_reviewers = \
942 get_default_reviewers_data, get_validated_reviewers = \
869 PullRequestModel().get_reviewer_functions()
943 PullRequestModel().get_reviewer_functions()
870
944
871 # re-use stored rules
945 # re-use stored rules
872 reviewer_rules = pull_request.reviewer_data
946 reviewer_rules = pull_request.reviewer_data
873 try:
947 try:
874 reviewers = get_validated_reviewers(
948 reviewers = get_validated_reviewers(
875 reviewer_objects, reviewer_rules)
949 reviewer_objects, reviewer_rules)
876 except ValueError as e:
950 except ValueError as e:
877 raise JSONRPCError('Reviewers Validation: {}'.format(e))
951 raise JSONRPCError('Reviewers Validation: {}'.format(e))
878 else:
952 else:
879 reviewers = []
953 reviewers = []
880
954
881 title = Optional.extract(title)
955 title = Optional.extract(title)
882 description = Optional.extract(description)
956 description = Optional.extract(description)
883 description_renderer = Optional.extract(description_renderer)
957 description_renderer = Optional.extract(description_renderer)
884
958
885 if title or description:
959 if title or description:
886 PullRequestModel().edit(
960 PullRequestModel().edit(
887 pull_request,
961 pull_request,
888 title or pull_request.title,
962 title or pull_request.title,
889 description or pull_request.description,
963 description or pull_request.description,
890 description_renderer or pull_request.description_renderer,
964 description_renderer or pull_request.description_renderer,
891 apiuser)
965 apiuser)
892 Session().commit()
966 Session().commit()
893
967
894 commit_changes = {"added": [], "common": [], "removed": []}
968 commit_changes = {"added": [], "common": [], "removed": []}
895 if str2bool(Optional.extract(update_commits)):
969 if str2bool(Optional.extract(update_commits)):
896
970
897 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
971 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
898 raise JSONRPCError(
972 raise JSONRPCError(
899 'Operation forbidden because pull request is in state {}, '
973 'Operation forbidden because pull request is in state {}, '
900 'only state {} is allowed.'.format(
974 'only state {} is allowed.'.format(
901 pull_request.pull_request_state, PullRequest.STATE_CREATED))
975 pull_request.pull_request_state, PullRequest.STATE_CREATED))
902
976
903 with pull_request.set_state(PullRequest.STATE_UPDATING):
977 with pull_request.set_state(PullRequest.STATE_UPDATING):
904 if PullRequestModel().has_valid_update_type(pull_request):
978 if PullRequestModel().has_valid_update_type(pull_request):
905 db_user = apiuser.get_instance()
979 db_user = apiuser.get_instance()
906 update_response = PullRequestModel().update_commits(
980 update_response = PullRequestModel().update_commits(
907 pull_request, db_user)
981 pull_request, db_user)
908 commit_changes = update_response.changes or commit_changes
982 commit_changes = update_response.changes or commit_changes
909 Session().commit()
983 Session().commit()
910
984
911 reviewers_changes = {"added": [], "removed": []}
985 reviewers_changes = {"added": [], "removed": []}
912 if reviewers:
986 if reviewers:
913 old_calculated_status = pull_request.calculated_review_status()
987 old_calculated_status = pull_request.calculated_review_status()
914 added_reviewers, removed_reviewers = \
988 added_reviewers, removed_reviewers = \
915 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
989 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
916
990
917 reviewers_changes['added'] = sorted(
991 reviewers_changes['added'] = sorted(
918 [get_user_or_error(n).username for n in added_reviewers])
992 [get_user_or_error(n).username for n in added_reviewers])
919 reviewers_changes['removed'] = sorted(
993 reviewers_changes['removed'] = sorted(
920 [get_user_or_error(n).username for n in removed_reviewers])
994 [get_user_or_error(n).username for n in removed_reviewers])
921 Session().commit()
995 Session().commit()
922
996
923 # trigger status changed if change in reviewers changes the status
997 # trigger status changed if change in reviewers changes the status
924 calculated_status = pull_request.calculated_review_status()
998 calculated_status = pull_request.calculated_review_status()
925 if old_calculated_status != calculated_status:
999 if old_calculated_status != calculated_status:
926 PullRequestModel().trigger_pull_request_hook(
1000 PullRequestModel().trigger_pull_request_hook(
927 pull_request, apiuser, 'review_status_change',
1001 pull_request, apiuser, 'review_status_change',
928 data={'status': calculated_status})
1002 data={'status': calculated_status})
929
1003
930 data = {
1004 data = {
931 'msg': 'Updated pull request `{}`'.format(
1005 'msg': 'Updated pull request `{}`'.format(
932 pull_request.pull_request_id),
1006 pull_request.pull_request_id),
933 'pull_request': pull_request.get_api_data(),
1007 'pull_request': pull_request.get_api_data(),
934 'updated_commits': commit_changes,
1008 'updated_commits': commit_changes,
935 'updated_reviewers': reviewers_changes
1009 'updated_reviewers': reviewers_changes
936 }
1010 }
937
1011
938 return data
1012 return data
939
1013
940
1014
941 @jsonrpc_method()
1015 @jsonrpc_method()
942 def close_pull_request(
1016 def close_pull_request(
943 request, apiuser, pullrequestid, repoid=Optional(None),
1017 request, apiuser, pullrequestid, repoid=Optional(None),
944 userid=Optional(OAttr('apiuser')), message=Optional('')):
1018 userid=Optional(OAttr('apiuser')), message=Optional('')):
945 """
1019 """
946 Close the pull request specified by `pullrequestid`.
1020 Close the pull request specified by `pullrequestid`.
947
1021
948 :param apiuser: This is filled automatically from the |authtoken|.
1022 :param apiuser: This is filled automatically from the |authtoken|.
949 :type apiuser: AuthUser
1023 :type apiuser: AuthUser
950 :param repoid: Repository name or repository ID to which the pull
1024 :param repoid: Repository name or repository ID to which the pull
951 request belongs.
1025 request belongs.
952 :type repoid: str or int
1026 :type repoid: str or int
953 :param pullrequestid: ID of the pull request to be closed.
1027 :param pullrequestid: ID of the pull request to be closed.
954 :type pullrequestid: int
1028 :type pullrequestid: int
955 :param userid: Close the pull request as this user.
1029 :param userid: Close the pull request as this user.
956 :type userid: Optional(str or int)
1030 :type userid: Optional(str or int)
957 :param message: Optional message to close the Pull Request with. If not
1031 :param message: Optional message to close the Pull Request with. If not
958 specified it will be generated automatically.
1032 specified it will be generated automatically.
959 :type message: Optional(str)
1033 :type message: Optional(str)
960
1034
961 Example output:
1035 Example output:
962
1036
963 .. code-block:: bash
1037 .. code-block:: bash
964
1038
965 "id": <id_given_in_input>,
1039 "id": <id_given_in_input>,
966 "result": {
1040 "result": {
967 "pull_request_id": "<int>",
1041 "pull_request_id": "<int>",
968 "close_status": "<str:status_lbl>,
1042 "close_status": "<str:status_lbl>,
969 "closed": "<bool>"
1043 "closed": "<bool>"
970 },
1044 },
971 "error": null
1045 "error": null
972
1046
973 """
1047 """
974 _ = request.translate
1048 _ = request.translate
975
1049
976 pull_request = get_pull_request_or_error(pullrequestid)
1050 pull_request = get_pull_request_or_error(pullrequestid)
977 if Optional.extract(repoid):
1051 if Optional.extract(repoid):
978 repo = get_repo_or_error(repoid)
1052 repo = get_repo_or_error(repoid)
979 else:
1053 else:
980 repo = pull_request.target_repo
1054 repo = pull_request.target_repo
981
1055
1056 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
1057 user=apiuser, repo_name=repo.repo_name)
982 if not isinstance(userid, Optional):
1058 if not isinstance(userid, Optional):
983 if (has_superadmin_permission(apiuser) or
1059 if has_superadmin_permission(apiuser) or is_repo_admin:
984 HasRepoPermissionAnyApi('repository.admin')(
985 user=apiuser, repo_name=repo.repo_name)):
986 apiuser = get_user_or_error(userid)
1060 apiuser = get_user_or_error(userid)
987 else:
1061 else:
988 raise JSONRPCError('userid is not the same as your user')
1062 raise JSONRPCError('userid is not the same as your user')
989
1063
990 if pull_request.is_closed():
1064 if pull_request.is_closed():
991 raise JSONRPCError(
1065 raise JSONRPCError(
992 'pull request `%s` is already closed' % (pullrequestid,))
1066 'pull request `%s` is already closed' % (pullrequestid,))
993
1067
994 # only owner or admin or person with write permissions
1068 # only owner or admin or person with write permissions
995 allowed_to_close = PullRequestModel().check_user_update(
1069 allowed_to_close = PullRequestModel().check_user_update(
996 pull_request, apiuser, api=True)
1070 pull_request, apiuser, api=True)
997
1071
998 if not allowed_to_close:
1072 if not allowed_to_close:
999 raise JSONRPCError(
1073 raise JSONRPCError(
1000 'pull request `%s` close failed, no permission to close.' % (
1074 'pull request `%s` close failed, no permission to close.' % (
1001 pullrequestid,))
1075 pullrequestid,))
1002
1076
1003 # message we're using to close the PR, else it's automatically generated
1077 # message we're using to close the PR, else it's automatically generated
1004 message = Optional.extract(message)
1078 message = Optional.extract(message)
1005
1079
1006 # finally close the PR, with proper message comment
1080 # finally close the PR, with proper message comment
1007 comment, status = PullRequestModel().close_pull_request_with_comment(
1081 comment, status = PullRequestModel().close_pull_request_with_comment(
1008 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1082 pull_request, apiuser, repo, message=message, auth_user=apiuser)
1009 status_lbl = ChangesetStatus.get_status_lbl(status)
1083 status_lbl = ChangesetStatus.get_status_lbl(status)
1010
1084
1011 Session().commit()
1085 Session().commit()
1012
1086
1013 data = {
1087 data = {
1014 'pull_request_id': pull_request.pull_request_id,
1088 'pull_request_id': pull_request.pull_request_id,
1015 'close_status': status_lbl,
1089 'close_status': status_lbl,
1016 'closed': True,
1090 'closed': True,
1017 }
1091 }
1018 return data
1092 return data
General Comments 0
You need to be logged in to leave comments. Login now