##// END OF EJS Templates
API: added pull-requests versions into returned API data...
dan -
r4197:01c1fb34 stable
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

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