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