##// END OF EJS Templates
api: forbid commenting on closed PR
marcink -
r3463:246c287b default
parent child Browse files
Show More
@@ -1,987 +1,992 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)
29 validate_repo_permissions, resolve_ref_or_error)
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
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 else:
294 else:
295 raise JSONRPCError('userid is not the same as your user')
295 raise JSONRPCError('userid is not the same as your user')
296
296
297 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
297 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
298 raise JSONRPCError(
298 raise JSONRPCError(
299 'Operation forbidden because pull request is in state {}, '
299 'Operation forbidden because pull request is in state {}, '
300 'only state {} is allowed.'.format(
300 'only state {} is allowed.'.format(
301 pull_request.pull_request_state, PullRequest.STATE_CREATED))
301 pull_request.pull_request_state, PullRequest.STATE_CREATED))
302
302
303 with pull_request.set_state(PullRequest.STATE_UPDATING):
303 with pull_request.set_state(PullRequest.STATE_UPDATING):
304 check = MergeCheck.validate(
304 check = MergeCheck.validate(
305 pull_request, auth_user=apiuser,
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=apiuser.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(
329 pull_request.pull_request_id, apiuser)
329 pull_request.pull_request_id, apiuser)
330
330
331 Session().commit()
331 Session().commit()
332
332
333 # In previous versions the merge response directly contained the merge
333 # In previous versions the merge response directly contained the merge
334 # commit id. It is now contained in the merge reference object. To be
334 # commit id. It is now contained in the merge reference object. To be
335 # backwards compatible we have to extract it again.
335 # backwards compatible we have to extract it again.
336 merge_response = merge_response.asdict()
336 merge_response = merge_response.asdict()
337 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
337 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
338
338
339 return merge_response
339 return merge_response
340
340
341
341
342 @jsonrpc_method()
342 @jsonrpc_method()
343 def get_pull_request_comments(
343 def get_pull_request_comments(
344 request, apiuser, pullrequestid, repoid=Optional(None)):
344 request, apiuser, pullrequestid, repoid=Optional(None)):
345 """
345 """
346 Get all comments of pull request specified with the `pullrequestid`
346 Get all comments of pull request specified with the `pullrequestid`
347
347
348 :param apiuser: This is filled automatically from the |authtoken|.
348 :param apiuser: This is filled automatically from the |authtoken|.
349 :type apiuser: AuthUser
349 :type apiuser: AuthUser
350 :param repoid: Optional repository name or repository ID.
350 :param repoid: Optional repository name or repository ID.
351 :type repoid: str or int
351 :type repoid: str or int
352 :param pullrequestid: The pull request ID.
352 :param pullrequestid: The pull request ID.
353 :type pullrequestid: int
353 :type pullrequestid: int
354
354
355 Example output:
355 Example output:
356
356
357 .. code-block:: bash
357 .. code-block:: bash
358
358
359 id : <id_given_in_input>
359 id : <id_given_in_input>
360 result : [
360 result : [
361 {
361 {
362 "comment_author": {
362 "comment_author": {
363 "active": true,
363 "active": true,
364 "full_name_or_username": "Tom Gore",
364 "full_name_or_username": "Tom Gore",
365 "username": "admin"
365 "username": "admin"
366 },
366 },
367 "comment_created_on": "2017-01-02T18:43:45.533",
367 "comment_created_on": "2017-01-02T18:43:45.533",
368 "comment_f_path": null,
368 "comment_f_path": null,
369 "comment_id": 25,
369 "comment_id": 25,
370 "comment_lineno": null,
370 "comment_lineno": null,
371 "comment_status": {
371 "comment_status": {
372 "status": "under_review",
372 "status": "under_review",
373 "status_lbl": "Under Review"
373 "status_lbl": "Under Review"
374 },
374 },
375 "comment_text": "Example text",
375 "comment_text": "Example text",
376 "comment_type": null,
376 "comment_type": null,
377 "pull_request_version": null
377 "pull_request_version": null
378 }
378 }
379 ],
379 ],
380 error : null
380 error : null
381 """
381 """
382
382
383 pull_request = get_pull_request_or_error(pullrequestid)
383 pull_request = get_pull_request_or_error(pullrequestid)
384 if Optional.extract(repoid):
384 if Optional.extract(repoid):
385 repo = get_repo_or_error(repoid)
385 repo = get_repo_or_error(repoid)
386 else:
386 else:
387 repo = pull_request.target_repo
387 repo = pull_request.target_repo
388
388
389 if not PullRequestModel().check_user_read(
389 if not PullRequestModel().check_user_read(
390 pull_request, apiuser, api=True):
390 pull_request, apiuser, api=True):
391 raise JSONRPCError('repository `%s` or pull request `%s` '
391 raise JSONRPCError('repository `%s` or pull request `%s` '
392 'does not exist' % (repoid, pullrequestid))
392 'does not exist' % (repoid, pullrequestid))
393
393
394 (pull_request_latest,
394 (pull_request_latest,
395 pull_request_at_ver,
395 pull_request_at_ver,
396 pull_request_display_obj,
396 pull_request_display_obj,
397 at_version) = PullRequestModel().get_pr_version(
397 at_version) = PullRequestModel().get_pr_version(
398 pull_request.pull_request_id, version=None)
398 pull_request.pull_request_id, version=None)
399
399
400 versions = pull_request_display_obj.versions()
400 versions = pull_request_display_obj.versions()
401 ver_map = {
401 ver_map = {
402 ver.pull_request_version_id: cnt
402 ver.pull_request_version_id: cnt
403 for cnt, ver in enumerate(versions, 1)
403 for cnt, ver in enumerate(versions, 1)
404 }
404 }
405
405
406 # GENERAL COMMENTS with versions #
406 # GENERAL COMMENTS with versions #
407 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
407 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
408 q = q.order_by(ChangesetComment.comment_id.asc())
408 q = q.order_by(ChangesetComment.comment_id.asc())
409 general_comments = q.all()
409 general_comments = q.all()
410
410
411 # INLINE COMMENTS with versions #
411 # INLINE COMMENTS with versions #
412 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
412 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
413 q = q.order_by(ChangesetComment.comment_id.asc())
413 q = q.order_by(ChangesetComment.comment_id.asc())
414 inline_comments = q.all()
414 inline_comments = q.all()
415
415
416 data = []
416 data = []
417 for comment in inline_comments + general_comments:
417 for comment in inline_comments + general_comments:
418 full_data = comment.get_api_data()
418 full_data = comment.get_api_data()
419 pr_version_id = None
419 pr_version_id = None
420 if comment.pull_request_version_id:
420 if comment.pull_request_version_id:
421 pr_version_id = 'v{}'.format(
421 pr_version_id = 'v{}'.format(
422 ver_map[comment.pull_request_version_id])
422 ver_map[comment.pull_request_version_id])
423
423
424 # sanitize some entries
424 # sanitize some entries
425
425
426 full_data['pull_request_version'] = pr_version_id
426 full_data['pull_request_version'] = pr_version_id
427 full_data['comment_author'] = {
427 full_data['comment_author'] = {
428 'username': full_data['comment_author'].username,
428 'username': full_data['comment_author'].username,
429 'full_name_or_username': full_data['comment_author'].full_name_or_username,
429 'full_name_or_username': full_data['comment_author'].full_name_or_username,
430 'active': full_data['comment_author'].active,
430 'active': full_data['comment_author'].active,
431 }
431 }
432
432
433 if full_data['comment_status']:
433 if full_data['comment_status']:
434 full_data['comment_status'] = {
434 full_data['comment_status'] = {
435 'status': full_data['comment_status'][0].status,
435 'status': full_data['comment_status'][0].status,
436 'status_lbl': full_data['comment_status'][0].status_lbl,
436 'status_lbl': full_data['comment_status'][0].status_lbl,
437 }
437 }
438 else:
438 else:
439 full_data['comment_status'] = {}
439 full_data['comment_status'] = {}
440
440
441 data.append(full_data)
441 data.append(full_data)
442 return data
442 return data
443
443
444
444
445 @jsonrpc_method()
445 @jsonrpc_method()
446 def comment_pull_request(
446 def comment_pull_request(
447 request, apiuser, pullrequestid, repoid=Optional(None),
447 request, apiuser, pullrequestid, repoid=Optional(None),
448 message=Optional(None), commit_id=Optional(None), status=Optional(None),
448 message=Optional(None), commit_id=Optional(None), status=Optional(None),
449 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
449 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
450 resolves_comment_id=Optional(None),
450 resolves_comment_id=Optional(None),
451 userid=Optional(OAttr('apiuser'))):
451 userid=Optional(OAttr('apiuser'))):
452 """
452 """
453 Comment on the pull request specified with the `pullrequestid`,
453 Comment on the pull request specified with the `pullrequestid`,
454 in the |repo| specified by the `repoid`, and optionally change the
454 in the |repo| specified by the `repoid`, and optionally change the
455 review status.
455 review status.
456
456
457 :param apiuser: This is filled automatically from the |authtoken|.
457 :param apiuser: This is filled automatically from the |authtoken|.
458 :type apiuser: AuthUser
458 :type apiuser: AuthUser
459 :param repoid: Optional repository name or repository ID.
459 :param repoid: Optional repository name or repository ID.
460 :type repoid: str or int
460 :type repoid: str or int
461 :param pullrequestid: The pull request ID.
461 :param pullrequestid: The pull request ID.
462 :type pullrequestid: int
462 :type pullrequestid: int
463 :param commit_id: Specify the commit_id for which to set a comment. If
463 :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
464 given commit_id is different than latest in the PR status
465 change won't be performed.
465 change won't be performed.
466 :type commit_id: str
466 :type commit_id: str
467 :param message: The text content of the comment.
467 :param message: The text content of the comment.
468 :type message: str
468 :type message: str
469 :param status: (**Optional**) Set the approval status of the pull
469 :param status: (**Optional**) Set the approval status of the pull
470 request. One of: 'not_reviewed', 'approved', 'rejected',
470 request. One of: 'not_reviewed', 'approved', 'rejected',
471 'under_review'
471 'under_review'
472 :type status: str
472 :type status: str
473 :param comment_type: Comment type, one of: 'note', 'todo'
473 :param comment_type: Comment type, one of: 'note', 'todo'
474 :type comment_type: Optional(str), default: 'note'
474 :type comment_type: Optional(str), default: 'note'
475 :param userid: Comment on the pull request as this user
475 :param userid: Comment on the pull request as this user
476 :type userid: Optional(str or int)
476 :type userid: Optional(str or int)
477
477
478 Example output:
478 Example output:
479
479
480 .. code-block:: bash
480 .. code-block:: bash
481
481
482 id : <id_given_in_input>
482 id : <id_given_in_input>
483 result : {
483 result : {
484 "pull_request_id": "<Integer>",
484 "pull_request_id": "<Integer>",
485 "comment_id": "<Integer>",
485 "comment_id": "<Integer>",
486 "status": {"given": <given_status>,
486 "status": {"given": <given_status>,
487 "was_changed": <bool status_was_actually_changed> },
487 "was_changed": <bool status_was_actually_changed> },
488 },
488 },
489 error : null
489 error : null
490 """
490 """
491 pull_request = get_pull_request_or_error(pullrequestid)
491 pull_request = get_pull_request_or_error(pullrequestid)
492 if Optional.extract(repoid):
492 if Optional.extract(repoid):
493 repo = get_repo_or_error(repoid)
493 repo = get_repo_or_error(repoid)
494 else:
494 else:
495 repo = pull_request.target_repo
495 repo = pull_request.target_repo
496
496
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 else:
502 else:
503 raise JSONRPCError('userid is not the same as your user')
503 raise JSONRPCError('userid is not the same as your user')
504
504
505 if pull_request.is_closed():
506 raise JSONRPCError(
507 'pull request `%s` comment failed, pull request is closed' % (
508 pullrequestid,))
509
505 if not PullRequestModel().check_user_read(
510 if not PullRequestModel().check_user_read(
506 pull_request, apiuser, api=True):
511 pull_request, apiuser, api=True):
507 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
512 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
508 message = Optional.extract(message)
513 message = Optional.extract(message)
509 status = Optional.extract(status)
514 status = Optional.extract(status)
510 commit_id = Optional.extract(commit_id)
515 commit_id = Optional.extract(commit_id)
511 comment_type = Optional.extract(comment_type)
516 comment_type = Optional.extract(comment_type)
512 resolves_comment_id = Optional.extract(resolves_comment_id)
517 resolves_comment_id = Optional.extract(resolves_comment_id)
513
518
514 if not message and not status:
519 if not message and not status:
515 raise JSONRPCError(
520 raise JSONRPCError(
516 'Both message and status parameters are missing. '
521 'Both message and status parameters are missing. '
517 'At least one is required.')
522 'At least one is required.')
518
523
519 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
524 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
520 status is not None):
525 status is not None):
521 raise JSONRPCError('Unknown comment status: `%s`' % status)
526 raise JSONRPCError('Unknown comment status: `%s`' % status)
522
527
523 if commit_id and commit_id not in pull_request.revisions:
528 if commit_id and commit_id not in pull_request.revisions:
524 raise JSONRPCError(
529 raise JSONRPCError(
525 'Invalid commit_id `%s` for this pull request.' % commit_id)
530 'Invalid commit_id `%s` for this pull request.' % commit_id)
526
531
527 allowed_to_change_status = PullRequestModel().check_user_change_status(
532 allowed_to_change_status = PullRequestModel().check_user_change_status(
528 pull_request, apiuser)
533 pull_request, apiuser)
529
534
530 # if commit_id is passed re-validated if user is allowed to change status
535 # if commit_id is passed re-validated if user is allowed to change status
531 # based on latest commit_id from the PR
536 # based on latest commit_id from the PR
532 if commit_id:
537 if commit_id:
533 commit_idx = pull_request.revisions.index(commit_id)
538 commit_idx = pull_request.revisions.index(commit_id)
534 if commit_idx != 0:
539 if commit_idx != 0:
535 allowed_to_change_status = False
540 allowed_to_change_status = False
536
541
537 if resolves_comment_id:
542 if resolves_comment_id:
538 comment = ChangesetComment.get(resolves_comment_id)
543 comment = ChangesetComment.get(resolves_comment_id)
539 if not comment:
544 if not comment:
540 raise JSONRPCError(
545 raise JSONRPCError(
541 'Invalid resolves_comment_id `%s` for this pull request.'
546 'Invalid resolves_comment_id `%s` for this pull request.'
542 % resolves_comment_id)
547 % resolves_comment_id)
543 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
548 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
544 raise JSONRPCError(
549 raise JSONRPCError(
545 'Comment `%s` is wrong type for setting status to resolved.'
550 'Comment `%s` is wrong type for setting status to resolved.'
546 % resolves_comment_id)
551 % resolves_comment_id)
547
552
548 text = message
553 text = message
549 status_label = ChangesetStatus.get_status_lbl(status)
554 status_label = ChangesetStatus.get_status_lbl(status)
550 if status and allowed_to_change_status:
555 if status and allowed_to_change_status:
551 st_message = ('Status change %(transition_icon)s %(status)s'
556 st_message = ('Status change %(transition_icon)s %(status)s'
552 % {'transition_icon': '>', 'status': status_label})
557 % {'transition_icon': '>', 'status': status_label})
553 text = message or st_message
558 text = message or st_message
554
559
555 rc_config = SettingsModel().get_all_settings()
560 rc_config = SettingsModel().get_all_settings()
556 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
561 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
557
562
558 status_change = status and allowed_to_change_status
563 status_change = status and allowed_to_change_status
559 comment = CommentsModel().create(
564 comment = CommentsModel().create(
560 text=text,
565 text=text,
561 repo=pull_request.target_repo.repo_id,
566 repo=pull_request.target_repo.repo_id,
562 user=apiuser.user_id,
567 user=apiuser.user_id,
563 pull_request=pull_request.pull_request_id,
568 pull_request=pull_request.pull_request_id,
564 f_path=None,
569 f_path=None,
565 line_no=None,
570 line_no=None,
566 status_change=(status_label if status_change else None),
571 status_change=(status_label if status_change else None),
567 status_change_type=(status if status_change else None),
572 status_change_type=(status if status_change else None),
568 closing_pr=False,
573 closing_pr=False,
569 renderer=renderer,
574 renderer=renderer,
570 comment_type=comment_type,
575 comment_type=comment_type,
571 resolves_comment_id=resolves_comment_id,
576 resolves_comment_id=resolves_comment_id,
572 auth_user=apiuser
577 auth_user=apiuser
573 )
578 )
574
579
575 if allowed_to_change_status and status:
580 if allowed_to_change_status and status:
576 old_calculated_status = pull_request.calculated_review_status()
581 old_calculated_status = pull_request.calculated_review_status()
577 ChangesetStatusModel().set_status(
582 ChangesetStatusModel().set_status(
578 pull_request.target_repo.repo_id,
583 pull_request.target_repo.repo_id,
579 status,
584 status,
580 apiuser.user_id,
585 apiuser.user_id,
581 comment,
586 comment,
582 pull_request=pull_request.pull_request_id
587 pull_request=pull_request.pull_request_id
583 )
588 )
584 Session().flush()
589 Session().flush()
585
590
586 Session().commit()
591 Session().commit()
587
592
588 PullRequestModel().trigger_pull_request_hook(
593 PullRequestModel().trigger_pull_request_hook(
589 pull_request, apiuser, 'comment',
594 pull_request, apiuser, 'comment',
590 data={'comment': comment})
595 data={'comment': comment})
591
596
592 if allowed_to_change_status and status:
597 if allowed_to_change_status and status:
593 # we now calculate the status of pull request, and based on that
598 # we now calculate the status of pull request, and based on that
594 # calculation we set the commits status
599 # calculation we set the commits status
595 calculated_status = pull_request.calculated_review_status()
600 calculated_status = pull_request.calculated_review_status()
596 if old_calculated_status != calculated_status:
601 if old_calculated_status != calculated_status:
597 PullRequestModel().trigger_pull_request_hook(
602 PullRequestModel().trigger_pull_request_hook(
598 pull_request, apiuser, 'review_status_change',
603 pull_request, apiuser, 'review_status_change',
599 data={'status': calculated_status})
604 data={'status': calculated_status})
600
605
601 data = {
606 data = {
602 'pull_request_id': pull_request.pull_request_id,
607 'pull_request_id': pull_request.pull_request_id,
603 'comment_id': comment.comment_id if comment else None,
608 'comment_id': comment.comment_id if comment else None,
604 'status': {'given': status, 'was_changed': status_change},
609 'status': {'given': status, 'was_changed': status_change},
605 }
610 }
606 return data
611 return data
607
612
608
613
609 @jsonrpc_method()
614 @jsonrpc_method()
610 def create_pull_request(
615 def create_pull_request(
611 request, apiuser, source_repo, target_repo, source_ref, target_ref,
616 request, apiuser, source_repo, target_repo, source_ref, target_ref,
612 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
617 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
613 reviewers=Optional(None)):
618 reviewers=Optional(None)):
614 """
619 """
615 Creates a new pull request.
620 Creates a new pull request.
616
621
617 Accepts refs in the following formats:
622 Accepts refs in the following formats:
618
623
619 * branch:<branch_name>:<sha>
624 * branch:<branch_name>:<sha>
620 * branch:<branch_name>
625 * branch:<branch_name>
621 * bookmark:<bookmark_name>:<sha> (Mercurial only)
626 * bookmark:<bookmark_name>:<sha> (Mercurial only)
622 * bookmark:<bookmark_name> (Mercurial only)
627 * bookmark:<bookmark_name> (Mercurial only)
623
628
624 :param apiuser: This is filled automatically from the |authtoken|.
629 :param apiuser: This is filled automatically from the |authtoken|.
625 :type apiuser: AuthUser
630 :type apiuser: AuthUser
626 :param source_repo: Set the source repository name.
631 :param source_repo: Set the source repository name.
627 :type source_repo: str
632 :type source_repo: str
628 :param target_repo: Set the target repository name.
633 :param target_repo: Set the target repository name.
629 :type target_repo: str
634 :type target_repo: str
630 :param source_ref: Set the source ref name.
635 :param source_ref: Set the source ref name.
631 :type source_ref: str
636 :type source_ref: str
632 :param target_ref: Set the target ref name.
637 :param target_ref: Set the target ref name.
633 :type target_ref: str
638 :type target_ref: str
634 :param title: Optionally Set the pull request title, it's generated otherwise
639 :param title: Optionally Set the pull request title, it's generated otherwise
635 :type title: str
640 :type title: str
636 :param description: Set the pull request description.
641 :param description: Set the pull request description.
637 :type description: Optional(str)
642 :type description: Optional(str)
638 :type description_renderer: Optional(str)
643 :type description_renderer: Optional(str)
639 :param description_renderer: Set pull request renderer for the description.
644 :param description_renderer: Set pull request renderer for the description.
640 It should be 'rst', 'markdown' or 'plain'. If not give default
645 It should be 'rst', 'markdown' or 'plain'. If not give default
641 system renderer will be used
646 system renderer will be used
642 :param reviewers: Set the new pull request reviewers list.
647 :param reviewers: Set the new pull request reviewers list.
643 Reviewer defined by review rules will be added automatically to the
648 Reviewer defined by review rules will be added automatically to the
644 defined list.
649 defined list.
645 :type reviewers: Optional(list)
650 :type reviewers: Optional(list)
646 Accepts username strings or objects of the format:
651 Accepts username strings or objects of the format:
647
652
648 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
653 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
649 """
654 """
650
655
651 source_db_repo = get_repo_or_error(source_repo)
656 source_db_repo = get_repo_or_error(source_repo)
652 target_db_repo = get_repo_or_error(target_repo)
657 target_db_repo = get_repo_or_error(target_repo)
653 if not has_superadmin_permission(apiuser):
658 if not has_superadmin_permission(apiuser):
654 _perms = ('repository.admin', 'repository.write', 'repository.read',)
659 _perms = ('repository.admin', 'repository.write', 'repository.read',)
655 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
660 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
656
661
657 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
662 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
658 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
663 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
659
664
660 source_scm = source_db_repo.scm_instance()
665 source_scm = source_db_repo.scm_instance()
661 target_scm = target_db_repo.scm_instance()
666 target_scm = target_db_repo.scm_instance()
662
667
663 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
668 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
664 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
669 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
665
670
666 ancestor = source_scm.get_common_ancestor(
671 ancestor = source_scm.get_common_ancestor(
667 source_commit.raw_id, target_commit.raw_id, target_scm)
672 source_commit.raw_id, target_commit.raw_id, target_scm)
668 if not ancestor:
673 if not ancestor:
669 raise JSONRPCError('no common ancestor found')
674 raise JSONRPCError('no common ancestor found')
670
675
671 # recalculate target ref based on ancestor
676 # recalculate target ref based on ancestor
672 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
677 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
673 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
678 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
674
679
675 commit_ranges = target_scm.compare(
680 commit_ranges = target_scm.compare(
676 target_commit.raw_id, source_commit.raw_id, source_scm,
681 target_commit.raw_id, source_commit.raw_id, source_scm,
677 merge=True, pre_load=[])
682 merge=True, pre_load=[])
678
683
679 if not commit_ranges:
684 if not commit_ranges:
680 raise JSONRPCError('no commits found')
685 raise JSONRPCError('no commits found')
681
686
682 reviewer_objects = Optional.extract(reviewers) or []
687 reviewer_objects = Optional.extract(reviewers) or []
683
688
684 # serialize and validate passed in given reviewers
689 # serialize and validate passed in given reviewers
685 if reviewer_objects:
690 if reviewer_objects:
686 schema = ReviewerListSchema()
691 schema = ReviewerListSchema()
687 try:
692 try:
688 reviewer_objects = schema.deserialize(reviewer_objects)
693 reviewer_objects = schema.deserialize(reviewer_objects)
689 except Invalid as err:
694 except Invalid as err:
690 raise JSONRPCValidationError(colander_exc=err)
695 raise JSONRPCValidationError(colander_exc=err)
691
696
692 # validate users
697 # validate users
693 for reviewer_object in reviewer_objects:
698 for reviewer_object in reviewer_objects:
694 user = get_user_or_error(reviewer_object['username'])
699 user = get_user_or_error(reviewer_object['username'])
695 reviewer_object['user_id'] = user.user_id
700 reviewer_object['user_id'] = user.user_id
696
701
697 get_default_reviewers_data, validate_default_reviewers = \
702 get_default_reviewers_data, validate_default_reviewers = \
698 PullRequestModel().get_reviewer_functions()
703 PullRequestModel().get_reviewer_functions()
699
704
700 # recalculate reviewers logic, to make sure we can validate this
705 # recalculate reviewers logic, to make sure we can validate this
701 reviewer_rules = get_default_reviewers_data(
706 reviewer_rules = get_default_reviewers_data(
702 apiuser.get_instance(), source_db_repo,
707 apiuser.get_instance(), source_db_repo,
703 source_commit, target_db_repo, target_commit)
708 source_commit, target_db_repo, target_commit)
704
709
705 # now MERGE our given with the calculated
710 # now MERGE our given with the calculated
706 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
711 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
707
712
708 try:
713 try:
709 reviewers = validate_default_reviewers(
714 reviewers = validate_default_reviewers(
710 reviewer_objects, reviewer_rules)
715 reviewer_objects, reviewer_rules)
711 except ValueError as e:
716 except ValueError as e:
712 raise JSONRPCError('Reviewers Validation: {}'.format(e))
717 raise JSONRPCError('Reviewers Validation: {}'.format(e))
713
718
714 title = Optional.extract(title)
719 title = Optional.extract(title)
715 if not title:
720 if not title:
716 title_source_ref = source_ref.split(':', 2)[1]
721 title_source_ref = source_ref.split(':', 2)[1]
717 title = PullRequestModel().generate_pullrequest_title(
722 title = PullRequestModel().generate_pullrequest_title(
718 source=source_repo,
723 source=source_repo,
719 source_ref=title_source_ref,
724 source_ref=title_source_ref,
720 target=target_repo
725 target=target_repo
721 )
726 )
722 # fetch renderer, if set fallback to plain in case of PR
727 # fetch renderer, if set fallback to plain in case of PR
723 rc_config = SettingsModel().get_all_settings()
728 rc_config = SettingsModel().get_all_settings()
724 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
729 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
725 description = Optional.extract(description)
730 description = Optional.extract(description)
726 description_renderer = Optional.extract(description_renderer) or default_system_renderer
731 description_renderer = Optional.extract(description_renderer) or default_system_renderer
727
732
728 pull_request = PullRequestModel().create(
733 pull_request = PullRequestModel().create(
729 created_by=apiuser.user_id,
734 created_by=apiuser.user_id,
730 source_repo=source_repo,
735 source_repo=source_repo,
731 source_ref=full_source_ref,
736 source_ref=full_source_ref,
732 target_repo=target_repo,
737 target_repo=target_repo,
733 target_ref=full_target_ref,
738 target_ref=full_target_ref,
734 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
739 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
735 reviewers=reviewers,
740 reviewers=reviewers,
736 title=title,
741 title=title,
737 description=description,
742 description=description,
738 description_renderer=description_renderer,
743 description_renderer=description_renderer,
739 reviewer_data=reviewer_rules,
744 reviewer_data=reviewer_rules,
740 auth_user=apiuser
745 auth_user=apiuser
741 )
746 )
742
747
743 Session().commit()
748 Session().commit()
744 data = {
749 data = {
745 'msg': 'Created new pull request `{}`'.format(title),
750 'msg': 'Created new pull request `{}`'.format(title),
746 'pull_request_id': pull_request.pull_request_id,
751 'pull_request_id': pull_request.pull_request_id,
747 }
752 }
748 return data
753 return data
749
754
750
755
751 @jsonrpc_method()
756 @jsonrpc_method()
752 def update_pull_request(
757 def update_pull_request(
753 request, apiuser, pullrequestid, repoid=Optional(None),
758 request, apiuser, pullrequestid, repoid=Optional(None),
754 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
759 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
755 reviewers=Optional(None), update_commits=Optional(None)):
760 reviewers=Optional(None), update_commits=Optional(None)):
756 """
761 """
757 Updates a pull request.
762 Updates a pull request.
758
763
759 :param apiuser: This is filled automatically from the |authtoken|.
764 :param apiuser: This is filled automatically from the |authtoken|.
760 :type apiuser: AuthUser
765 :type apiuser: AuthUser
761 :param repoid: Optional repository name or repository ID.
766 :param repoid: Optional repository name or repository ID.
762 :type repoid: str or int
767 :type repoid: str or int
763 :param pullrequestid: The pull request ID.
768 :param pullrequestid: The pull request ID.
764 :type pullrequestid: int
769 :type pullrequestid: int
765 :param title: Set the pull request title.
770 :param title: Set the pull request title.
766 :type title: str
771 :type title: str
767 :param description: Update pull request description.
772 :param description: Update pull request description.
768 :type description: Optional(str)
773 :type description: Optional(str)
769 :type description_renderer: Optional(str)
774 :type description_renderer: Optional(str)
770 :param description_renderer: Update pull request renderer for the description.
775 :param description_renderer: Update pull request renderer for the description.
771 It should be 'rst', 'markdown' or 'plain'
776 It should be 'rst', 'markdown' or 'plain'
772 :param reviewers: Update pull request reviewers list with new value.
777 :param reviewers: Update pull request reviewers list with new value.
773 :type reviewers: Optional(list)
778 :type reviewers: Optional(list)
774 Accepts username strings or objects of the format:
779 Accepts username strings or objects of the format:
775
780
776 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
781 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
777
782
778 :param update_commits: Trigger update of commits for this pull request
783 :param update_commits: Trigger update of commits for this pull request
779 :type: update_commits: Optional(bool)
784 :type: update_commits: Optional(bool)
780
785
781 Example output:
786 Example output:
782
787
783 .. code-block:: bash
788 .. code-block:: bash
784
789
785 id : <id_given_in_input>
790 id : <id_given_in_input>
786 result : {
791 result : {
787 "msg": "Updated pull request `63`",
792 "msg": "Updated pull request `63`",
788 "pull_request": <pull_request_object>,
793 "pull_request": <pull_request_object>,
789 "updated_reviewers": {
794 "updated_reviewers": {
790 "added": [
795 "added": [
791 "username"
796 "username"
792 ],
797 ],
793 "removed": []
798 "removed": []
794 },
799 },
795 "updated_commits": {
800 "updated_commits": {
796 "added": [
801 "added": [
797 "<sha1_hash>"
802 "<sha1_hash>"
798 ],
803 ],
799 "common": [
804 "common": [
800 "<sha1_hash>",
805 "<sha1_hash>",
801 "<sha1_hash>",
806 "<sha1_hash>",
802 ],
807 ],
803 "removed": []
808 "removed": []
804 }
809 }
805 }
810 }
806 error : null
811 error : null
807 """
812 """
808
813
809 pull_request = get_pull_request_or_error(pullrequestid)
814 pull_request = get_pull_request_or_error(pullrequestid)
810 if Optional.extract(repoid):
815 if Optional.extract(repoid):
811 repo = get_repo_or_error(repoid)
816 repo = get_repo_or_error(repoid)
812 else:
817 else:
813 repo = pull_request.target_repo
818 repo = pull_request.target_repo
814
819
815 if not PullRequestModel().check_user_update(
820 if not PullRequestModel().check_user_update(
816 pull_request, apiuser, api=True):
821 pull_request, apiuser, api=True):
817 raise JSONRPCError(
822 raise JSONRPCError(
818 'pull request `%s` update failed, no permission to update.' % (
823 'pull request `%s` update failed, no permission to update.' % (
819 pullrequestid,))
824 pullrequestid,))
820 if pull_request.is_closed():
825 if pull_request.is_closed():
821 raise JSONRPCError(
826 raise JSONRPCError(
822 'pull request `%s` update failed, pull request is closed' % (
827 'pull request `%s` update failed, pull request is closed' % (
823 pullrequestid,))
828 pullrequestid,))
824
829
825 reviewer_objects = Optional.extract(reviewers) or []
830 reviewer_objects = Optional.extract(reviewers) or []
826
831
827 if reviewer_objects:
832 if reviewer_objects:
828 schema = ReviewerListSchema()
833 schema = ReviewerListSchema()
829 try:
834 try:
830 reviewer_objects = schema.deserialize(reviewer_objects)
835 reviewer_objects = schema.deserialize(reviewer_objects)
831 except Invalid as err:
836 except Invalid as err:
832 raise JSONRPCValidationError(colander_exc=err)
837 raise JSONRPCValidationError(colander_exc=err)
833
838
834 # validate users
839 # validate users
835 for reviewer_object in reviewer_objects:
840 for reviewer_object in reviewer_objects:
836 user = get_user_or_error(reviewer_object['username'])
841 user = get_user_or_error(reviewer_object['username'])
837 reviewer_object['user_id'] = user.user_id
842 reviewer_object['user_id'] = user.user_id
838
843
839 get_default_reviewers_data, get_validated_reviewers = \
844 get_default_reviewers_data, get_validated_reviewers = \
840 PullRequestModel().get_reviewer_functions()
845 PullRequestModel().get_reviewer_functions()
841
846
842 # re-use stored rules
847 # re-use stored rules
843 reviewer_rules = pull_request.reviewer_data
848 reviewer_rules = pull_request.reviewer_data
844 try:
849 try:
845 reviewers = get_validated_reviewers(
850 reviewers = get_validated_reviewers(
846 reviewer_objects, reviewer_rules)
851 reviewer_objects, reviewer_rules)
847 except ValueError as e:
852 except ValueError as e:
848 raise JSONRPCError('Reviewers Validation: {}'.format(e))
853 raise JSONRPCError('Reviewers Validation: {}'.format(e))
849 else:
854 else:
850 reviewers = []
855 reviewers = []
851
856
852 title = Optional.extract(title)
857 title = Optional.extract(title)
853 description = Optional.extract(description)
858 description = Optional.extract(description)
854 description_renderer = Optional.extract(description_renderer)
859 description_renderer = Optional.extract(description_renderer)
855
860
856 if title or description:
861 if title or description:
857 PullRequestModel().edit(
862 PullRequestModel().edit(
858 pull_request,
863 pull_request,
859 title or pull_request.title,
864 title or pull_request.title,
860 description or pull_request.description,
865 description or pull_request.description,
861 description_renderer or pull_request.description_renderer,
866 description_renderer or pull_request.description_renderer,
862 apiuser)
867 apiuser)
863 Session().commit()
868 Session().commit()
864
869
865 commit_changes = {"added": [], "common": [], "removed": []}
870 commit_changes = {"added": [], "common": [], "removed": []}
866 if str2bool(Optional.extract(update_commits)):
871 if str2bool(Optional.extract(update_commits)):
867
872
868 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
873 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
869 raise JSONRPCError(
874 raise JSONRPCError(
870 'Operation forbidden because pull request is in state {}, '
875 'Operation forbidden because pull request is in state {}, '
871 'only state {} is allowed.'.format(
876 'only state {} is allowed.'.format(
872 pull_request.pull_request_state, PullRequest.STATE_CREATED))
877 pull_request.pull_request_state, PullRequest.STATE_CREATED))
873
878
874 with pull_request.set_state(PullRequest.STATE_UPDATING):
879 with pull_request.set_state(PullRequest.STATE_UPDATING):
875 if PullRequestModel().has_valid_update_type(pull_request):
880 if PullRequestModel().has_valid_update_type(pull_request):
876 update_response = PullRequestModel().update_commits(pull_request)
881 update_response = PullRequestModel().update_commits(pull_request)
877 commit_changes = update_response.changes or commit_changes
882 commit_changes = update_response.changes or commit_changes
878 Session().commit()
883 Session().commit()
879
884
880 reviewers_changes = {"added": [], "removed": []}
885 reviewers_changes = {"added": [], "removed": []}
881 if reviewers:
886 if reviewers:
882 old_calculated_status = pull_request.calculated_review_status()
887 old_calculated_status = pull_request.calculated_review_status()
883 added_reviewers, removed_reviewers = \
888 added_reviewers, removed_reviewers = \
884 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
889 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
885
890
886 reviewers_changes['added'] = sorted(
891 reviewers_changes['added'] = sorted(
887 [get_user_or_error(n).username for n in added_reviewers])
892 [get_user_or_error(n).username for n in added_reviewers])
888 reviewers_changes['removed'] = sorted(
893 reviewers_changes['removed'] = sorted(
889 [get_user_or_error(n).username for n in removed_reviewers])
894 [get_user_or_error(n).username for n in removed_reviewers])
890 Session().commit()
895 Session().commit()
891
896
892 # trigger status changed if change in reviewers changes the status
897 # trigger status changed if change in reviewers changes the status
893 calculated_status = pull_request.calculated_review_status()
898 calculated_status = pull_request.calculated_review_status()
894 if old_calculated_status != calculated_status:
899 if old_calculated_status != calculated_status:
895 PullRequestModel().trigger_pull_request_hook(
900 PullRequestModel().trigger_pull_request_hook(
896 pull_request, apiuser, 'review_status_change',
901 pull_request, apiuser, 'review_status_change',
897 data={'status': calculated_status})
902 data={'status': calculated_status})
898
903
899 data = {
904 data = {
900 'msg': 'Updated pull request `{}`'.format(
905 'msg': 'Updated pull request `{}`'.format(
901 pull_request.pull_request_id),
906 pull_request.pull_request_id),
902 'pull_request': pull_request.get_api_data(),
907 'pull_request': pull_request.get_api_data(),
903 'updated_commits': commit_changes,
908 'updated_commits': commit_changes,
904 'updated_reviewers': reviewers_changes
909 'updated_reviewers': reviewers_changes
905 }
910 }
906
911
907 return data
912 return data
908
913
909
914
910 @jsonrpc_method()
915 @jsonrpc_method()
911 def close_pull_request(
916 def close_pull_request(
912 request, apiuser, pullrequestid, repoid=Optional(None),
917 request, apiuser, pullrequestid, repoid=Optional(None),
913 userid=Optional(OAttr('apiuser')), message=Optional('')):
918 userid=Optional(OAttr('apiuser')), message=Optional('')):
914 """
919 """
915 Close the pull request specified by `pullrequestid`.
920 Close the pull request specified by `pullrequestid`.
916
921
917 :param apiuser: This is filled automatically from the |authtoken|.
922 :param apiuser: This is filled automatically from the |authtoken|.
918 :type apiuser: AuthUser
923 :type apiuser: AuthUser
919 :param repoid: Repository name or repository ID to which the pull
924 :param repoid: Repository name or repository ID to which the pull
920 request belongs.
925 request belongs.
921 :type repoid: str or int
926 :type repoid: str or int
922 :param pullrequestid: ID of the pull request to be closed.
927 :param pullrequestid: ID of the pull request to be closed.
923 :type pullrequestid: int
928 :type pullrequestid: int
924 :param userid: Close the pull request as this user.
929 :param userid: Close the pull request as this user.
925 :type userid: Optional(str or int)
930 :type userid: Optional(str or int)
926 :param message: Optional message to close the Pull Request with. If not
931 :param message: Optional message to close the Pull Request with. If not
927 specified it will be generated automatically.
932 specified it will be generated automatically.
928 :type message: Optional(str)
933 :type message: Optional(str)
929
934
930 Example output:
935 Example output:
931
936
932 .. code-block:: bash
937 .. code-block:: bash
933
938
934 "id": <id_given_in_input>,
939 "id": <id_given_in_input>,
935 "result": {
940 "result": {
936 "pull_request_id": "<int>",
941 "pull_request_id": "<int>",
937 "close_status": "<str:status_lbl>,
942 "close_status": "<str:status_lbl>,
938 "closed": "<bool>"
943 "closed": "<bool>"
939 },
944 },
940 "error": null
945 "error": null
941
946
942 """
947 """
943 _ = request.translate
948 _ = request.translate
944
949
945 pull_request = get_pull_request_or_error(pullrequestid)
950 pull_request = get_pull_request_or_error(pullrequestid)
946 if Optional.extract(repoid):
951 if Optional.extract(repoid):
947 repo = get_repo_or_error(repoid)
952 repo = get_repo_or_error(repoid)
948 else:
953 else:
949 repo = pull_request.target_repo
954 repo = pull_request.target_repo
950
955
951 if not isinstance(userid, Optional):
956 if not isinstance(userid, Optional):
952 if (has_superadmin_permission(apiuser) or
957 if (has_superadmin_permission(apiuser) or
953 HasRepoPermissionAnyApi('repository.admin')(
958 HasRepoPermissionAnyApi('repository.admin')(
954 user=apiuser, repo_name=repo.repo_name)):
959 user=apiuser, repo_name=repo.repo_name)):
955 apiuser = get_user_or_error(userid)
960 apiuser = get_user_or_error(userid)
956 else:
961 else:
957 raise JSONRPCError('userid is not the same as your user')
962 raise JSONRPCError('userid is not the same as your user')
958
963
959 if pull_request.is_closed():
964 if pull_request.is_closed():
960 raise JSONRPCError(
965 raise JSONRPCError(
961 'pull request `%s` is already closed' % (pullrequestid,))
966 'pull request `%s` is already closed' % (pullrequestid,))
962
967
963 # only owner or admin or person with write permissions
968 # only owner or admin or person with write permissions
964 allowed_to_close = PullRequestModel().check_user_update(
969 allowed_to_close = PullRequestModel().check_user_update(
965 pull_request, apiuser, api=True)
970 pull_request, apiuser, api=True)
966
971
967 if not allowed_to_close:
972 if not allowed_to_close:
968 raise JSONRPCError(
973 raise JSONRPCError(
969 'pull request `%s` close failed, no permission to close.' % (
974 'pull request `%s` close failed, no permission to close.' % (
970 pullrequestid,))
975 pullrequestid,))
971
976
972 # message we're using to close the PR, else it's automatically generated
977 # message we're using to close the PR, else it's automatically generated
973 message = Optional.extract(message)
978 message = Optional.extract(message)
974
979
975 # finally close the PR, with proper message comment
980 # finally close the PR, with proper message comment
976 comment, status = PullRequestModel().close_pull_request_with_comment(
981 comment, status = PullRequestModel().close_pull_request_with_comment(
977 pull_request, apiuser, repo, message=message, auth_user=apiuser)
982 pull_request, apiuser, repo, message=message, auth_user=apiuser)
978 status_lbl = ChangesetStatus.get_status_lbl(status)
983 status_lbl = ChangesetStatus.get_status_lbl(status)
979
984
980 Session().commit()
985 Session().commit()
981
986
982 data = {
987 data = {
983 'pull_request_id': pull_request.pull_request_id,
988 'pull_request_id': pull_request.pull_request_id,
984 'close_status': status_lbl,
989 'close_status': status_lbl,
985 'closed': True,
990 'closed': True,
986 }
991 }
987 return data
992 return data
General Comments 0
You need to be logged in to leave comments. Login now