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