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