##// END OF EJS Templates
api: expose owner to create_pull_request API. Fixes #5537
marcink -
r3474:e9c81a05 default
parent child Browse files
Show More
@@ -1,992 +1,996 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, validate_set_owner_permissions)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
53 the pull request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57
57
58 Example output:
58 Example output:
59
59
60 .. code-block:: bash
60 .. code-block:: bash
61
61
62 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
63 "result":
63 "result":
64 {
64 {
65 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
66 "url": "<url>",
66 "url": "<url>",
67 "title": "<title>",
67 "title": "<title>",
68 "description": "<description>",
68 "description": "<description>",
69 "status" : "<status>",
69 "status" : "<status>",
70 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
71 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
72 "commit_ids": [
72 "commit_ids": [
73 ...
73 ...
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 "<commit_id>",
76 ...
76 ...
77 ],
77 ],
78 "review_status": "<review_status>",
78 "review_status": "<review_status>",
79 "mergeable": {
79 "mergeable": {
80 "status": "<bool>",
80 "status": "<bool>",
81 "message": "<message>",
81 "message": "<message>",
82 },
82 },
83 "source": {
83 "source": {
84 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
85 "repository": "<repository_name>",
85 "repository": "<repository_name>",
86 "reference":
86 "reference":
87 {
87 {
88 "name": "<name>",
88 "name": "<name>",
89 "type": "<type>",
89 "type": "<type>",
90 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
91 }
91 }
92 },
92 },
93 "target": {
93 "target": {
94 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
95 "repository": "<repository_name>",
95 "repository": "<repository_name>",
96 "reference":
96 "reference":
97 {
97 {
98 "name": "<name>",
98 "name": "<name>",
99 "type": "<type>",
99 "type": "<type>",
100 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
101 }
101 }
102 },
102 },
103 "merge": {
103 "merge": {
104 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
105 "reference":
105 "reference":
106 {
106 {
107 "name": "<name>",
107 "name": "<name>",
108 "type": "<type>",
108 "type": "<type>",
109 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
110 }
110 }
111 },
111 },
112 "author": <user_obj>,
112 "author": <user_obj>,
113 "reviewers": [
113 "reviewers": [
114 ...
114 ...
115 {
115 {
116 "user": "<user_obj>",
116 "user": "<user_obj>",
117 "review_status": "<review_status>",
117 "review_status": "<review_status>",
118 }
118 }
119 ...
119 ...
120 ]
120 ]
121 },
121 },
122 "error": null
122 "error": null
123 """
123 """
124
124
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if Optional.extract(repoid):
126 if Optional.extract(repoid):
127 repo = get_repo_or_error(repoid)
127 repo = get_repo_or_error(repoid)
128 else:
128 else:
129 repo = pull_request.target_repo
129 repo = pull_request.target_repo
130
130
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
132 raise JSONRPCError('repository `%s` or pull request `%s` '
132 raise JSONRPCError('repository `%s` or pull request `%s` '
133 'does not exist' % (repoid, pullrequestid))
133 'does not exist' % (repoid, pullrequestid))
134
134
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
136 # otherwise we can lock the repo on calculation of merge state while update/merge
136 # otherwise we can lock the repo on calculation of merge state while update/merge
137 # is happening.
137 # is happening.
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
139 data = pull_request.get_api_data(with_merge_state=merge_state)
139 data = pull_request.get_api_data(with_merge_state=merge_state)
140 return data
140 return data
141
141
142
142
143 @jsonrpc_method()
143 @jsonrpc_method()
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
145 merge_state=Optional(True)):
145 merge_state=Optional(True)):
146 """
146 """
147 Get all pull requests from the repository specified in `repoid`.
147 Get all pull requests from the repository specified in `repoid`.
148
148
149 :param apiuser: This is filled automatically from the |authtoken|.
149 :param apiuser: This is filled automatically from the |authtoken|.
150 :type apiuser: AuthUser
150 :type apiuser: AuthUser
151 :param repoid: Optional repository name or repository ID.
151 :param repoid: Optional repository name or repository ID.
152 :type repoid: str or int
152 :type repoid: str or int
153 :param status: Only return pull requests with the specified status.
153 :param status: Only return pull requests with the specified status.
154 Valid options are.
154 Valid options are.
155 * ``new`` (default)
155 * ``new`` (default)
156 * ``open``
156 * ``open``
157 * ``closed``
157 * ``closed``
158 :type status: str
158 :type status: str
159 :param merge_state: Optional calculate merge state for each repository.
159 :param merge_state: Optional calculate merge state for each repository.
160 This could result in longer time to fetch the data
160 This could result in longer time to fetch the data
161 :type merge_state: bool
161 :type merge_state: bool
162
162
163 Example output:
163 Example output:
164
164
165 .. code-block:: bash
165 .. code-block:: bash
166
166
167 "id": <id_given_in_input>,
167 "id": <id_given_in_input>,
168 "result":
168 "result":
169 [
169 [
170 ...
170 ...
171 {
171 {
172 "pull_request_id": "<pull_request_id>",
172 "pull_request_id": "<pull_request_id>",
173 "url": "<url>",
173 "url": "<url>",
174 "title" : "<title>",
174 "title" : "<title>",
175 "description": "<description>",
175 "description": "<description>",
176 "status": "<status>",
176 "status": "<status>",
177 "created_on": "<date_time_created>",
177 "created_on": "<date_time_created>",
178 "updated_on": "<date_time_updated>",
178 "updated_on": "<date_time_updated>",
179 "commit_ids": [
179 "commit_ids": [
180 ...
180 ...
181 "<commit_id>",
181 "<commit_id>",
182 "<commit_id>",
182 "<commit_id>",
183 ...
183 ...
184 ],
184 ],
185 "review_status": "<review_status>",
185 "review_status": "<review_status>",
186 "mergeable": {
186 "mergeable": {
187 "status": "<bool>",
187 "status": "<bool>",
188 "message: "<message>",
188 "message: "<message>",
189 },
189 },
190 "source": {
190 "source": {
191 "clone_url": "<clone_url>",
191 "clone_url": "<clone_url>",
192 "reference":
192 "reference":
193 {
193 {
194 "name": "<name>",
194 "name": "<name>",
195 "type": "<type>",
195 "type": "<type>",
196 "commit_id": "<commit_id>",
196 "commit_id": "<commit_id>",
197 }
197 }
198 },
198 },
199 "target": {
199 "target": {
200 "clone_url": "<clone_url>",
200 "clone_url": "<clone_url>",
201 "reference":
201 "reference":
202 {
202 {
203 "name": "<name>",
203 "name": "<name>",
204 "type": "<type>",
204 "type": "<type>",
205 "commit_id": "<commit_id>",
205 "commit_id": "<commit_id>",
206 }
206 }
207 },
207 },
208 "merge": {
208 "merge": {
209 "clone_url": "<clone_url>",
209 "clone_url": "<clone_url>",
210 "reference":
210 "reference":
211 {
211 {
212 "name": "<name>",
212 "name": "<name>",
213 "type": "<type>",
213 "type": "<type>",
214 "commit_id": "<commit_id>",
214 "commit_id": "<commit_id>",
215 }
215 }
216 },
216 },
217 "author": <user_obj>,
217 "author": <user_obj>,
218 "reviewers": [
218 "reviewers": [
219 ...
219 ...
220 {
220 {
221 "user": "<user_obj>",
221 "user": "<user_obj>",
222 "review_status": "<review_status>",
222 "review_status": "<review_status>",
223 }
223 }
224 ...
224 ...
225 ]
225 ]
226 }
226 }
227 ...
227 ...
228 ],
228 ],
229 "error": null
229 "error": null
230
230
231 """
231 """
232 repo = get_repo_or_error(repoid)
232 repo = get_repo_or_error(repoid)
233 if not has_superadmin_permission(apiuser):
233 if not has_superadmin_permission(apiuser):
234 _perms = (
234 _perms = (
235 'repository.admin', 'repository.write', 'repository.read',)
235 'repository.admin', 'repository.write', 'repository.read',)
236 validate_repo_permissions(apiuser, repoid, repo, _perms)
236 validate_repo_permissions(apiuser, repoid, repo, _perms)
237
237
238 status = Optional.extract(status)
238 status = Optional.extract(status)
239 merge_state = Optional.extract(merge_state, binary=True)
239 merge_state = Optional.extract(merge_state, binary=True)
240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
241 order_by='id', order_dir='desc')
241 order_by='id', order_dir='desc')
242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
243 return data
243 return data
244
244
245
245
246 @jsonrpc_method()
246 @jsonrpc_method()
247 def merge_pull_request(
247 def merge_pull_request(
248 request, apiuser, pullrequestid, repoid=Optional(None),
248 request, apiuser, pullrequestid, repoid=Optional(None),
249 userid=Optional(OAttr('apiuser'))):
249 userid=Optional(OAttr('apiuser'))):
250 """
250 """
251 Merge the pull request specified by `pullrequestid` into its target
251 Merge the pull request specified by `pullrequestid` into its target
252 repository.
252 repository.
253
253
254 :param apiuser: This is filled automatically from the |authtoken|.
254 :param apiuser: This is filled automatically from the |authtoken|.
255 :type apiuser: AuthUser
255 :type apiuser: AuthUser
256 :param repoid: Optional, repository name or repository ID of the
256 :param repoid: Optional, repository name or repository ID of the
257 target repository to which the |pr| is to be merged.
257 target repository to which the |pr| is to be merged.
258 :type repoid: str or int
258 :type repoid: str or int
259 :param pullrequestid: ID of the pull request which shall be merged.
259 :param pullrequestid: ID of the pull request which shall be merged.
260 :type pullrequestid: int
260 :type pullrequestid: int
261 :param userid: Merge the pull request as this user.
261 :param userid: Merge the pull request as this user.
262 :type userid: Optional(str or int)
262 :type userid: Optional(str or int)
263
263
264 Example output:
264 Example output:
265
265
266 .. code-block:: bash
266 .. code-block:: bash
267
267
268 "id": <id_given_in_input>,
268 "id": <id_given_in_input>,
269 "result": {
269 "result": {
270 "executed": "<bool>",
270 "executed": "<bool>",
271 "failure_reason": "<int>",
271 "failure_reason": "<int>",
272 "merge_status_message": "<str>",
272 "merge_status_message": "<str>",
273 "merge_commit_id": "<merge_commit_id>",
273 "merge_commit_id": "<merge_commit_id>",
274 "possible": "<bool>",
274 "possible": "<bool>",
275 "merge_ref": {
275 "merge_ref": {
276 "commit_id": "<commit_id>",
276 "commit_id": "<commit_id>",
277 "type": "<type>",
277 "type": "<type>",
278 "name": "<name>"
278 "name": "<name>"
279 }
279 }
280 },
280 },
281 "error": null
281 "error": null
282 """
282 """
283 pull_request = get_pull_request_or_error(pullrequestid)
283 pull_request = get_pull_request_or_error(pullrequestid)
284 if Optional.extract(repoid):
284 if Optional.extract(repoid):
285 repo = get_repo_or_error(repoid)
285 repo = get_repo_or_error(repoid)
286 else:
286 else:
287 repo = pull_request.target_repo
287 repo = pull_request.target_repo
288
288
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():
505 if pull_request.is_closed():
506 raise JSONRPCError(
506 raise JSONRPCError(
507 'pull request `%s` comment failed, pull request is closed' % (
507 'pull request `%s` comment failed, pull request is closed' % (
508 pullrequestid,))
508 pullrequestid,))
509
509
510 if not PullRequestModel().check_user_read(
510 if not PullRequestModel().check_user_read(
511 pull_request, apiuser, api=True):
511 pull_request, apiuser, api=True):
512 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
512 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
513 message = Optional.extract(message)
513 message = Optional.extract(message)
514 status = Optional.extract(status)
514 status = Optional.extract(status)
515 commit_id = Optional.extract(commit_id)
515 commit_id = Optional.extract(commit_id)
516 comment_type = Optional.extract(comment_type)
516 comment_type = Optional.extract(comment_type)
517 resolves_comment_id = Optional.extract(resolves_comment_id)
517 resolves_comment_id = Optional.extract(resolves_comment_id)
518
518
519 if not message and not status:
519 if not message and not status:
520 raise JSONRPCError(
520 raise JSONRPCError(
521 'Both message and status parameters are missing. '
521 'Both message and status parameters are missing. '
522 'At least one is required.')
522 'At least one is required.')
523
523
524 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
525 status is not None):
525 status is not None):
526 raise JSONRPCError('Unknown comment status: `%s`' % status)
526 raise JSONRPCError('Unknown comment status: `%s`' % status)
527
527
528 if commit_id and commit_id not in pull_request.revisions:
528 if commit_id and commit_id not in pull_request.revisions:
529 raise JSONRPCError(
529 raise JSONRPCError(
530 'Invalid commit_id `%s` for this pull request.' % commit_id)
530 'Invalid commit_id `%s` for this pull request.' % commit_id)
531
531
532 allowed_to_change_status = PullRequestModel().check_user_change_status(
532 allowed_to_change_status = PullRequestModel().check_user_change_status(
533 pull_request, apiuser)
533 pull_request, apiuser)
534
534
535 # 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
536 # based on latest commit_id from the PR
536 # based on latest commit_id from the PR
537 if commit_id:
537 if commit_id:
538 commit_idx = pull_request.revisions.index(commit_id)
538 commit_idx = pull_request.revisions.index(commit_id)
539 if commit_idx != 0:
539 if commit_idx != 0:
540 allowed_to_change_status = False
540 allowed_to_change_status = False
541
541
542 if resolves_comment_id:
542 if resolves_comment_id:
543 comment = ChangesetComment.get(resolves_comment_id)
543 comment = ChangesetComment.get(resolves_comment_id)
544 if not comment:
544 if not comment:
545 raise JSONRPCError(
545 raise JSONRPCError(
546 'Invalid resolves_comment_id `%s` for this pull request.'
546 'Invalid resolves_comment_id `%s` for this pull request.'
547 % resolves_comment_id)
547 % resolves_comment_id)
548 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
548 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
549 raise JSONRPCError(
549 raise JSONRPCError(
550 'Comment `%s` is wrong type for setting status to resolved.'
550 'Comment `%s` is wrong type for setting status to resolved.'
551 % resolves_comment_id)
551 % resolves_comment_id)
552
552
553 text = message
553 text = message
554 status_label = ChangesetStatus.get_status_lbl(status)
554 status_label = ChangesetStatus.get_status_lbl(status)
555 if status and allowed_to_change_status:
555 if status and allowed_to_change_status:
556 st_message = ('Status change %(transition_icon)s %(status)s'
556 st_message = ('Status change %(transition_icon)s %(status)s'
557 % {'transition_icon': '>', 'status': status_label})
557 % {'transition_icon': '>', 'status': status_label})
558 text = message or st_message
558 text = message or st_message
559
559
560 rc_config = SettingsModel().get_all_settings()
560 rc_config = SettingsModel().get_all_settings()
561 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
561 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
562
562
563 status_change = status and allowed_to_change_status
563 status_change = status and allowed_to_change_status
564 comment = CommentsModel().create(
564 comment = CommentsModel().create(
565 text=text,
565 text=text,
566 repo=pull_request.target_repo.repo_id,
566 repo=pull_request.target_repo.repo_id,
567 user=apiuser.user_id,
567 user=apiuser.user_id,
568 pull_request=pull_request.pull_request_id,
568 pull_request=pull_request.pull_request_id,
569 f_path=None,
569 f_path=None,
570 line_no=None,
570 line_no=None,
571 status_change=(status_label if status_change else None),
571 status_change=(status_label if status_change else None),
572 status_change_type=(status if status_change else None),
572 status_change_type=(status if status_change else None),
573 closing_pr=False,
573 closing_pr=False,
574 renderer=renderer,
574 renderer=renderer,
575 comment_type=comment_type,
575 comment_type=comment_type,
576 resolves_comment_id=resolves_comment_id,
576 resolves_comment_id=resolves_comment_id,
577 auth_user=apiuser
577 auth_user=apiuser
578 )
578 )
579
579
580 if allowed_to_change_status and status:
580 if allowed_to_change_status and status:
581 old_calculated_status = pull_request.calculated_review_status()
581 old_calculated_status = pull_request.calculated_review_status()
582 ChangesetStatusModel().set_status(
582 ChangesetStatusModel().set_status(
583 pull_request.target_repo.repo_id,
583 pull_request.target_repo.repo_id,
584 status,
584 status,
585 apiuser.user_id,
585 apiuser.user_id,
586 comment,
586 comment,
587 pull_request=pull_request.pull_request_id
587 pull_request=pull_request.pull_request_id
588 )
588 )
589 Session().flush()
589 Session().flush()
590
590
591 Session().commit()
591 Session().commit()
592
592
593 PullRequestModel().trigger_pull_request_hook(
593 PullRequestModel().trigger_pull_request_hook(
594 pull_request, apiuser, 'comment',
594 pull_request, apiuser, 'comment',
595 data={'comment': comment})
595 data={'comment': comment})
596
596
597 if allowed_to_change_status and status:
597 if allowed_to_change_status and status:
598 # 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
599 # calculation we set the commits status
599 # calculation we set the commits status
600 calculated_status = pull_request.calculated_review_status()
600 calculated_status = pull_request.calculated_review_status()
601 if old_calculated_status != calculated_status:
601 if old_calculated_status != calculated_status:
602 PullRequestModel().trigger_pull_request_hook(
602 PullRequestModel().trigger_pull_request_hook(
603 pull_request, apiuser, 'review_status_change',
603 pull_request, apiuser, 'review_status_change',
604 data={'status': calculated_status})
604 data={'status': calculated_status})
605
605
606 data = {
606 data = {
607 'pull_request_id': pull_request.pull_request_id,
607 'pull_request_id': pull_request.pull_request_id,
608 'comment_id': comment.comment_id if comment else None,
608 'comment_id': comment.comment_id if comment else None,
609 'status': {'given': status, 'was_changed': status_change},
609 'status': {'given': status, 'was_changed': status_change},
610 }
610 }
611 return data
611 return data
612
612
613
613
614 @jsonrpc_method()
614 @jsonrpc_method()
615 def create_pull_request(
615 def create_pull_request(
616 request, apiuser, source_repo, target_repo, source_ref, target_ref,
616 request, apiuser, source_repo, target_repo, source_ref, target_ref,
617 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
617 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
618 reviewers=Optional(None)):
618 description_renderer=Optional(''), reviewers=Optional(None)):
619 """
619 """
620 Creates a new pull request.
620 Creates a new pull request.
621
621
622 Accepts refs in the following formats:
622 Accepts refs in the following formats:
623
623
624 * branch:<branch_name>:<sha>
624 * branch:<branch_name>:<sha>
625 * branch:<branch_name>
625 * branch:<branch_name>
626 * bookmark:<bookmark_name>:<sha> (Mercurial only)
626 * bookmark:<bookmark_name>:<sha> (Mercurial only)
627 * bookmark:<bookmark_name> (Mercurial only)
627 * bookmark:<bookmark_name> (Mercurial only)
628
628
629 :param apiuser: This is filled automatically from the |authtoken|.
629 :param apiuser: This is filled automatically from the |authtoken|.
630 :type apiuser: AuthUser
630 :type apiuser: AuthUser
631 :param source_repo: Set the source repository name.
631 :param source_repo: Set the source repository name.
632 :type source_repo: str
632 :type source_repo: str
633 :param target_repo: Set the target repository name.
633 :param target_repo: Set the target repository name.
634 :type target_repo: str
634 :type target_repo: str
635 :param source_ref: Set the source ref name.
635 :param source_ref: Set the source ref name.
636 :type source_ref: str
636 :type source_ref: str
637 :param target_ref: Set the target ref name.
637 :param target_ref: Set the target ref name.
638 :type target_ref: str
638 :type target_ref: str
639 :param owner: user_id or username
640 :type owner: Optional(str)
639 :param title: Optionally Set the pull request title, it's generated otherwise
641 :param title: Optionally Set the pull request title, it's generated otherwise
640 :type title: str
642 :type title: str
641 :param description: Set the pull request description.
643 :param description: Set the pull request description.
642 :type description: Optional(str)
644 :type description: Optional(str)
643 :type description_renderer: Optional(str)
645 :type description_renderer: Optional(str)
644 :param description_renderer: Set pull request renderer for the description.
646 :param description_renderer: Set pull request renderer for the description.
645 It should be 'rst', 'markdown' or 'plain'. If not give default
647 It should be 'rst', 'markdown' or 'plain'. If not give default
646 system renderer will be used
648 system renderer will be used
647 :param reviewers: Set the new pull request reviewers list.
649 :param reviewers: Set the new pull request reviewers list.
648 Reviewer defined by review rules will be added automatically to the
650 Reviewer defined by review rules will be added automatically to the
649 defined list.
651 defined list.
650 :type reviewers: Optional(list)
652 :type reviewers: Optional(list)
651 Accepts username strings or objects of the format:
653 Accepts username strings or objects of the format:
652
654
653 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
655 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
654 """
656 """
655
657
656 source_db_repo = get_repo_or_error(source_repo)
658 source_db_repo = get_repo_or_error(source_repo)
657 target_db_repo = get_repo_or_error(target_repo)
659 target_db_repo = get_repo_or_error(target_repo)
658 if not has_superadmin_permission(apiuser):
660 if not has_superadmin_permission(apiuser):
659 _perms = ('repository.admin', 'repository.write', 'repository.read',)
661 _perms = ('repository.admin', 'repository.write', 'repository.read',)
660 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
662 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
661
663
664 owner = validate_set_owner_permissions(apiuser, owner)
665
662 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
666 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
663 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
667 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
664
668
665 source_scm = source_db_repo.scm_instance()
669 source_scm = source_db_repo.scm_instance()
666 target_scm = target_db_repo.scm_instance()
670 target_scm = target_db_repo.scm_instance()
667
671
668 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
672 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
669 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
673 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
670
674
671 ancestor = source_scm.get_common_ancestor(
675 ancestor = source_scm.get_common_ancestor(
672 source_commit.raw_id, target_commit.raw_id, target_scm)
676 source_commit.raw_id, target_commit.raw_id, target_scm)
673 if not ancestor:
677 if not ancestor:
674 raise JSONRPCError('no common ancestor found')
678 raise JSONRPCError('no common ancestor found')
675
679
676 # recalculate target ref based on ancestor
680 # recalculate target ref based on ancestor
677 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
681 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
678 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
682 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
679
683
680 commit_ranges = target_scm.compare(
684 commit_ranges = target_scm.compare(
681 target_commit.raw_id, source_commit.raw_id, source_scm,
685 target_commit.raw_id, source_commit.raw_id, source_scm,
682 merge=True, pre_load=[])
686 merge=True, pre_load=[])
683
687
684 if not commit_ranges:
688 if not commit_ranges:
685 raise JSONRPCError('no commits found')
689 raise JSONRPCError('no commits found')
686
690
687 reviewer_objects = Optional.extract(reviewers) or []
691 reviewer_objects = Optional.extract(reviewers) or []
688
692
689 # serialize and validate passed in given reviewers
693 # serialize and validate passed in given reviewers
690 if reviewer_objects:
694 if reviewer_objects:
691 schema = ReviewerListSchema()
695 schema = ReviewerListSchema()
692 try:
696 try:
693 reviewer_objects = schema.deserialize(reviewer_objects)
697 reviewer_objects = schema.deserialize(reviewer_objects)
694 except Invalid as err:
698 except Invalid as err:
695 raise JSONRPCValidationError(colander_exc=err)
699 raise JSONRPCValidationError(colander_exc=err)
696
700
697 # validate users
701 # validate users
698 for reviewer_object in reviewer_objects:
702 for reviewer_object in reviewer_objects:
699 user = get_user_or_error(reviewer_object['username'])
703 user = get_user_or_error(reviewer_object['username'])
700 reviewer_object['user_id'] = user.user_id
704 reviewer_object['user_id'] = user.user_id
701
705
702 get_default_reviewers_data, validate_default_reviewers = \
706 get_default_reviewers_data, validate_default_reviewers = \
703 PullRequestModel().get_reviewer_functions()
707 PullRequestModel().get_reviewer_functions()
704
708
705 # recalculate reviewers logic, to make sure we can validate this
709 # recalculate reviewers logic, to make sure we can validate this
706 reviewer_rules = get_default_reviewers_data(
710 reviewer_rules = get_default_reviewers_data(
707 apiuser.get_instance(), source_db_repo,
711 owner, source_db_repo,
708 source_commit, target_db_repo, target_commit)
712 source_commit, target_db_repo, target_commit)
709
713
710 # now MERGE our given with the calculated
714 # now MERGE our given with the calculated
711 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
715 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
712
716
713 try:
717 try:
714 reviewers = validate_default_reviewers(
718 reviewers = validate_default_reviewers(
715 reviewer_objects, reviewer_rules)
719 reviewer_objects, reviewer_rules)
716 except ValueError as e:
720 except ValueError as e:
717 raise JSONRPCError('Reviewers Validation: {}'.format(e))
721 raise JSONRPCError('Reviewers Validation: {}'.format(e))
718
722
719 title = Optional.extract(title)
723 title = Optional.extract(title)
720 if not title:
724 if not title:
721 title_source_ref = source_ref.split(':', 2)[1]
725 title_source_ref = source_ref.split(':', 2)[1]
722 title = PullRequestModel().generate_pullrequest_title(
726 title = PullRequestModel().generate_pullrequest_title(
723 source=source_repo,
727 source=source_repo,
724 source_ref=title_source_ref,
728 source_ref=title_source_ref,
725 target=target_repo
729 target=target_repo
726 )
730 )
727 # fetch renderer, if set fallback to plain in case of PR
731 # fetch renderer, if set fallback to plain in case of PR
728 rc_config = SettingsModel().get_all_settings()
732 rc_config = SettingsModel().get_all_settings()
729 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
733 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
730 description = Optional.extract(description)
734 description = Optional.extract(description)
731 description_renderer = Optional.extract(description_renderer) or default_system_renderer
735 description_renderer = Optional.extract(description_renderer) or default_system_renderer
732
736
733 pull_request = PullRequestModel().create(
737 pull_request = PullRequestModel().create(
734 created_by=apiuser.user_id,
738 created_by=owner.user_id,
735 source_repo=source_repo,
739 source_repo=source_repo,
736 source_ref=full_source_ref,
740 source_ref=full_source_ref,
737 target_repo=target_repo,
741 target_repo=target_repo,
738 target_ref=full_target_ref,
742 target_ref=full_target_ref,
739 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
743 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
740 reviewers=reviewers,
744 reviewers=reviewers,
741 title=title,
745 title=title,
742 description=description,
746 description=description,
743 description_renderer=description_renderer,
747 description_renderer=description_renderer,
744 reviewer_data=reviewer_rules,
748 reviewer_data=reviewer_rules,
745 auth_user=apiuser
749 auth_user=apiuser
746 )
750 )
747
751
748 Session().commit()
752 Session().commit()
749 data = {
753 data = {
750 'msg': 'Created new pull request `{}`'.format(title),
754 'msg': 'Created new pull request `{}`'.format(title),
751 'pull_request_id': pull_request.pull_request_id,
755 'pull_request_id': pull_request.pull_request_id,
752 }
756 }
753 return data
757 return data
754
758
755
759
756 @jsonrpc_method()
760 @jsonrpc_method()
757 def update_pull_request(
761 def update_pull_request(
758 request, apiuser, pullrequestid, repoid=Optional(None),
762 request, apiuser, pullrequestid, repoid=Optional(None),
759 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
763 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
760 reviewers=Optional(None), update_commits=Optional(None)):
764 reviewers=Optional(None), update_commits=Optional(None)):
761 """
765 """
762 Updates a pull request.
766 Updates a pull request.
763
767
764 :param apiuser: This is filled automatically from the |authtoken|.
768 :param apiuser: This is filled automatically from the |authtoken|.
765 :type apiuser: AuthUser
769 :type apiuser: AuthUser
766 :param repoid: Optional repository name or repository ID.
770 :param repoid: Optional repository name or repository ID.
767 :type repoid: str or int
771 :type repoid: str or int
768 :param pullrequestid: The pull request ID.
772 :param pullrequestid: The pull request ID.
769 :type pullrequestid: int
773 :type pullrequestid: int
770 :param title: Set the pull request title.
774 :param title: Set the pull request title.
771 :type title: str
775 :type title: str
772 :param description: Update pull request description.
776 :param description: Update pull request description.
773 :type description: Optional(str)
777 :type description: Optional(str)
774 :type description_renderer: Optional(str)
778 :type description_renderer: Optional(str)
775 :param description_renderer: Update pull request renderer for the description.
779 :param description_renderer: Update pull request renderer for the description.
776 It should be 'rst', 'markdown' or 'plain'
780 It should be 'rst', 'markdown' or 'plain'
777 :param reviewers: Update pull request reviewers list with new value.
781 :param reviewers: Update pull request reviewers list with new value.
778 :type reviewers: Optional(list)
782 :type reviewers: Optional(list)
779 Accepts username strings or objects of the format:
783 Accepts username strings or objects of the format:
780
784
781 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
785 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
782
786
783 :param update_commits: Trigger update of commits for this pull request
787 :param update_commits: Trigger update of commits for this pull request
784 :type: update_commits: Optional(bool)
788 :type: update_commits: Optional(bool)
785
789
786 Example output:
790 Example output:
787
791
788 .. code-block:: bash
792 .. code-block:: bash
789
793
790 id : <id_given_in_input>
794 id : <id_given_in_input>
791 result : {
795 result : {
792 "msg": "Updated pull request `63`",
796 "msg": "Updated pull request `63`",
793 "pull_request": <pull_request_object>,
797 "pull_request": <pull_request_object>,
794 "updated_reviewers": {
798 "updated_reviewers": {
795 "added": [
799 "added": [
796 "username"
800 "username"
797 ],
801 ],
798 "removed": []
802 "removed": []
799 },
803 },
800 "updated_commits": {
804 "updated_commits": {
801 "added": [
805 "added": [
802 "<sha1_hash>"
806 "<sha1_hash>"
803 ],
807 ],
804 "common": [
808 "common": [
805 "<sha1_hash>",
809 "<sha1_hash>",
806 "<sha1_hash>",
810 "<sha1_hash>",
807 ],
811 ],
808 "removed": []
812 "removed": []
809 }
813 }
810 }
814 }
811 error : null
815 error : null
812 """
816 """
813
817
814 pull_request = get_pull_request_or_error(pullrequestid)
818 pull_request = get_pull_request_or_error(pullrequestid)
815 if Optional.extract(repoid):
819 if Optional.extract(repoid):
816 repo = get_repo_or_error(repoid)
820 repo = get_repo_or_error(repoid)
817 else:
821 else:
818 repo = pull_request.target_repo
822 repo = pull_request.target_repo
819
823
820 if not PullRequestModel().check_user_update(
824 if not PullRequestModel().check_user_update(
821 pull_request, apiuser, api=True):
825 pull_request, apiuser, api=True):
822 raise JSONRPCError(
826 raise JSONRPCError(
823 'pull request `%s` update failed, no permission to update.' % (
827 'pull request `%s` update failed, no permission to update.' % (
824 pullrequestid,))
828 pullrequestid,))
825 if pull_request.is_closed():
829 if pull_request.is_closed():
826 raise JSONRPCError(
830 raise JSONRPCError(
827 'pull request `%s` update failed, pull request is closed' % (
831 'pull request `%s` update failed, pull request is closed' % (
828 pullrequestid,))
832 pullrequestid,))
829
833
830 reviewer_objects = Optional.extract(reviewers) or []
834 reviewer_objects = Optional.extract(reviewers) or []
831
835
832 if reviewer_objects:
836 if reviewer_objects:
833 schema = ReviewerListSchema()
837 schema = ReviewerListSchema()
834 try:
838 try:
835 reviewer_objects = schema.deserialize(reviewer_objects)
839 reviewer_objects = schema.deserialize(reviewer_objects)
836 except Invalid as err:
840 except Invalid as err:
837 raise JSONRPCValidationError(colander_exc=err)
841 raise JSONRPCValidationError(colander_exc=err)
838
842
839 # validate users
843 # validate users
840 for reviewer_object in reviewer_objects:
844 for reviewer_object in reviewer_objects:
841 user = get_user_or_error(reviewer_object['username'])
845 user = get_user_or_error(reviewer_object['username'])
842 reviewer_object['user_id'] = user.user_id
846 reviewer_object['user_id'] = user.user_id
843
847
844 get_default_reviewers_data, get_validated_reviewers = \
848 get_default_reviewers_data, get_validated_reviewers = \
845 PullRequestModel().get_reviewer_functions()
849 PullRequestModel().get_reviewer_functions()
846
850
847 # re-use stored rules
851 # re-use stored rules
848 reviewer_rules = pull_request.reviewer_data
852 reviewer_rules = pull_request.reviewer_data
849 try:
853 try:
850 reviewers = get_validated_reviewers(
854 reviewers = get_validated_reviewers(
851 reviewer_objects, reviewer_rules)
855 reviewer_objects, reviewer_rules)
852 except ValueError as e:
856 except ValueError as e:
853 raise JSONRPCError('Reviewers Validation: {}'.format(e))
857 raise JSONRPCError('Reviewers Validation: {}'.format(e))
854 else:
858 else:
855 reviewers = []
859 reviewers = []
856
860
857 title = Optional.extract(title)
861 title = Optional.extract(title)
858 description = Optional.extract(description)
862 description = Optional.extract(description)
859 description_renderer = Optional.extract(description_renderer)
863 description_renderer = Optional.extract(description_renderer)
860
864
861 if title or description:
865 if title or description:
862 PullRequestModel().edit(
866 PullRequestModel().edit(
863 pull_request,
867 pull_request,
864 title or pull_request.title,
868 title or pull_request.title,
865 description or pull_request.description,
869 description or pull_request.description,
866 description_renderer or pull_request.description_renderer,
870 description_renderer or pull_request.description_renderer,
867 apiuser)
871 apiuser)
868 Session().commit()
872 Session().commit()
869
873
870 commit_changes = {"added": [], "common": [], "removed": []}
874 commit_changes = {"added": [], "common": [], "removed": []}
871 if str2bool(Optional.extract(update_commits)):
875 if str2bool(Optional.extract(update_commits)):
872
876
873 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
877 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
874 raise JSONRPCError(
878 raise JSONRPCError(
875 'Operation forbidden because pull request is in state {}, '
879 'Operation forbidden because pull request is in state {}, '
876 'only state {} is allowed.'.format(
880 'only state {} is allowed.'.format(
877 pull_request.pull_request_state, PullRequest.STATE_CREATED))
881 pull_request.pull_request_state, PullRequest.STATE_CREATED))
878
882
879 with pull_request.set_state(PullRequest.STATE_UPDATING):
883 with pull_request.set_state(PullRequest.STATE_UPDATING):
880 if PullRequestModel().has_valid_update_type(pull_request):
884 if PullRequestModel().has_valid_update_type(pull_request):
881 update_response = PullRequestModel().update_commits(pull_request)
885 update_response = PullRequestModel().update_commits(pull_request)
882 commit_changes = update_response.changes or commit_changes
886 commit_changes = update_response.changes or commit_changes
883 Session().commit()
887 Session().commit()
884
888
885 reviewers_changes = {"added": [], "removed": []}
889 reviewers_changes = {"added": [], "removed": []}
886 if reviewers:
890 if reviewers:
887 old_calculated_status = pull_request.calculated_review_status()
891 old_calculated_status = pull_request.calculated_review_status()
888 added_reviewers, removed_reviewers = \
892 added_reviewers, removed_reviewers = \
889 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
893 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
890
894
891 reviewers_changes['added'] = sorted(
895 reviewers_changes['added'] = sorted(
892 [get_user_or_error(n).username for n in added_reviewers])
896 [get_user_or_error(n).username for n in added_reviewers])
893 reviewers_changes['removed'] = sorted(
897 reviewers_changes['removed'] = sorted(
894 [get_user_or_error(n).username for n in removed_reviewers])
898 [get_user_or_error(n).username for n in removed_reviewers])
895 Session().commit()
899 Session().commit()
896
900
897 # trigger status changed if change in reviewers changes the status
901 # trigger status changed if change in reviewers changes the status
898 calculated_status = pull_request.calculated_review_status()
902 calculated_status = pull_request.calculated_review_status()
899 if old_calculated_status != calculated_status:
903 if old_calculated_status != calculated_status:
900 PullRequestModel().trigger_pull_request_hook(
904 PullRequestModel().trigger_pull_request_hook(
901 pull_request, apiuser, 'review_status_change',
905 pull_request, apiuser, 'review_status_change',
902 data={'status': calculated_status})
906 data={'status': calculated_status})
903
907
904 data = {
908 data = {
905 'msg': 'Updated pull request `{}`'.format(
909 'msg': 'Updated pull request `{}`'.format(
906 pull_request.pull_request_id),
910 pull_request.pull_request_id),
907 'pull_request': pull_request.get_api_data(),
911 'pull_request': pull_request.get_api_data(),
908 'updated_commits': commit_changes,
912 'updated_commits': commit_changes,
909 'updated_reviewers': reviewers_changes
913 'updated_reviewers': reviewers_changes
910 }
914 }
911
915
912 return data
916 return data
913
917
914
918
915 @jsonrpc_method()
919 @jsonrpc_method()
916 def close_pull_request(
920 def close_pull_request(
917 request, apiuser, pullrequestid, repoid=Optional(None),
921 request, apiuser, pullrequestid, repoid=Optional(None),
918 userid=Optional(OAttr('apiuser')), message=Optional('')):
922 userid=Optional(OAttr('apiuser')), message=Optional('')):
919 """
923 """
920 Close the pull request specified by `pullrequestid`.
924 Close the pull request specified by `pullrequestid`.
921
925
922 :param apiuser: This is filled automatically from the |authtoken|.
926 :param apiuser: This is filled automatically from the |authtoken|.
923 :type apiuser: AuthUser
927 :type apiuser: AuthUser
924 :param repoid: Repository name or repository ID to which the pull
928 :param repoid: Repository name or repository ID to which the pull
925 request belongs.
929 request belongs.
926 :type repoid: str or int
930 :type repoid: str or int
927 :param pullrequestid: ID of the pull request to be closed.
931 :param pullrequestid: ID of the pull request to be closed.
928 :type pullrequestid: int
932 :type pullrequestid: int
929 :param userid: Close the pull request as this user.
933 :param userid: Close the pull request as this user.
930 :type userid: Optional(str or int)
934 :type userid: Optional(str or int)
931 :param message: Optional message to close the Pull Request with. If not
935 :param message: Optional message to close the Pull Request with. If not
932 specified it will be generated automatically.
936 specified it will be generated automatically.
933 :type message: Optional(str)
937 :type message: Optional(str)
934
938
935 Example output:
939 Example output:
936
940
937 .. code-block:: bash
941 .. code-block:: bash
938
942
939 "id": <id_given_in_input>,
943 "id": <id_given_in_input>,
940 "result": {
944 "result": {
941 "pull_request_id": "<int>",
945 "pull_request_id": "<int>",
942 "close_status": "<str:status_lbl>,
946 "close_status": "<str:status_lbl>,
943 "closed": "<bool>"
947 "closed": "<bool>"
944 },
948 },
945 "error": null
949 "error": null
946
950
947 """
951 """
948 _ = request.translate
952 _ = request.translate
949
953
950 pull_request = get_pull_request_or_error(pullrequestid)
954 pull_request = get_pull_request_or_error(pullrequestid)
951 if Optional.extract(repoid):
955 if Optional.extract(repoid):
952 repo = get_repo_or_error(repoid)
956 repo = get_repo_or_error(repoid)
953 else:
957 else:
954 repo = pull_request.target_repo
958 repo = pull_request.target_repo
955
959
956 if not isinstance(userid, Optional):
960 if not isinstance(userid, Optional):
957 if (has_superadmin_permission(apiuser) or
961 if (has_superadmin_permission(apiuser) or
958 HasRepoPermissionAnyApi('repository.admin')(
962 HasRepoPermissionAnyApi('repository.admin')(
959 user=apiuser, repo_name=repo.repo_name)):
963 user=apiuser, repo_name=repo.repo_name)):
960 apiuser = get_user_or_error(userid)
964 apiuser = get_user_or_error(userid)
961 else:
965 else:
962 raise JSONRPCError('userid is not the same as your user')
966 raise JSONRPCError('userid is not the same as your user')
963
967
964 if pull_request.is_closed():
968 if pull_request.is_closed():
965 raise JSONRPCError(
969 raise JSONRPCError(
966 'pull request `%s` is already closed' % (pullrequestid,))
970 'pull request `%s` is already closed' % (pullrequestid,))
967
971
968 # only owner or admin or person with write permissions
972 # only owner or admin or person with write permissions
969 allowed_to_close = PullRequestModel().check_user_update(
973 allowed_to_close = PullRequestModel().check_user_update(
970 pull_request, apiuser, api=True)
974 pull_request, apiuser, api=True)
971
975
972 if not allowed_to_close:
976 if not allowed_to_close:
973 raise JSONRPCError(
977 raise JSONRPCError(
974 'pull request `%s` close failed, no permission to close.' % (
978 'pull request `%s` close failed, no permission to close.' % (
975 pullrequestid,))
979 pullrequestid,))
976
980
977 # message we're using to close the PR, else it's automatically generated
981 # message we're using to close the PR, else it's automatically generated
978 message = Optional.extract(message)
982 message = Optional.extract(message)
979
983
980 # finally close the PR, with proper message comment
984 # finally close the PR, with proper message comment
981 comment, status = PullRequestModel().close_pull_request_with_comment(
985 comment, status = PullRequestModel().close_pull_request_with_comment(
982 pull_request, apiuser, repo, message=message, auth_user=apiuser)
986 pull_request, apiuser, repo, message=message, auth_user=apiuser)
983 status_lbl = ChangesetStatus.get_status_lbl(status)
987 status_lbl = ChangesetStatus.get_status_lbl(status)
984
988
985 Session().commit()
989 Session().commit()
986
990
987 data = {
991 data = {
988 'pull_request_id': pull_request.pull_request_id,
992 'pull_request_id': pull_request.pull_request_id,
989 'close_status': status_lbl,
993 'close_status': status_lbl,
990 'closed': True,
994 'closed': True,
991 }
995 }
992 return data
996 return data
General Comments 0
You need to be logged in to leave comments. Login now