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