##// END OF EJS Templates
reviewers: optimize diff data, and creation of PR with advanced default reviewers
marcink -
r4510:b532b1b7 stable
parent child
Show More
@@ -1,1052 +1,1056
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib import channelstream
29 from rhodecode.lib import channelstream
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 ReviewerListSchema
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 @jsonrpc_method()
44 @jsonrpc_method()
45 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
45 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None),
46 merge_state=Optional(False)):
46 merge_state=Optional(False)):
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 :param merge_state: Optional calculate merge state for each repository.
57 :param merge_state: Optional calculate merge state for each repository.
58 This could result in longer time to fetch the data
58 This could result in longer time to fetch the data
59 :type merge_state: bool
59 :type merge_state: bool
60
60
61 Example output:
61 Example output:
62
62
63 .. code-block:: bash
63 .. code-block:: bash
64
64
65 "id": <id_given_in_input>,
65 "id": <id_given_in_input>,
66 "result":
66 "result":
67 {
67 {
68 "pull_request_id": "<pull_request_id>",
68 "pull_request_id": "<pull_request_id>",
69 "url": "<url>",
69 "url": "<url>",
70 "title": "<title>",
70 "title": "<title>",
71 "description": "<description>",
71 "description": "<description>",
72 "status" : "<status>",
72 "status" : "<status>",
73 "created_on": "<date_time_created>",
73 "created_on": "<date_time_created>",
74 "updated_on": "<date_time_updated>",
74 "updated_on": "<date_time_updated>",
75 "versions": "<number_or_versions_of_pr>",
75 "versions": "<number_or_versions_of_pr>",
76 "commit_ids": [
76 "commit_ids": [
77 ...
77 ...
78 "<commit_id>",
78 "<commit_id>",
79 "<commit_id>",
79 "<commit_id>",
80 ...
80 ...
81 ],
81 ],
82 "review_status": "<review_status>",
82 "review_status": "<review_status>",
83 "mergeable": {
83 "mergeable": {
84 "status": "<bool>",
84 "status": "<bool>",
85 "message": "<message>",
85 "message": "<message>",
86 },
86 },
87 "source": {
87 "source": {
88 "clone_url": "<clone_url>",
88 "clone_url": "<clone_url>",
89 "repository": "<repository_name>",
89 "repository": "<repository_name>",
90 "reference":
90 "reference":
91 {
91 {
92 "name": "<name>",
92 "name": "<name>",
93 "type": "<type>",
93 "type": "<type>",
94 "commit_id": "<commit_id>",
94 "commit_id": "<commit_id>",
95 }
95 }
96 },
96 },
97 "target": {
97 "target": {
98 "clone_url": "<clone_url>",
98 "clone_url": "<clone_url>",
99 "repository": "<repository_name>",
99 "repository": "<repository_name>",
100 "reference":
100 "reference":
101 {
101 {
102 "name": "<name>",
102 "name": "<name>",
103 "type": "<type>",
103 "type": "<type>",
104 "commit_id": "<commit_id>",
104 "commit_id": "<commit_id>",
105 }
105 }
106 },
106 },
107 "merge": {
107 "merge": {
108 "clone_url": "<clone_url>",
108 "clone_url": "<clone_url>",
109 "reference":
109 "reference":
110 {
110 {
111 "name": "<name>",
111 "name": "<name>",
112 "type": "<type>",
112 "type": "<type>",
113 "commit_id": "<commit_id>",
113 "commit_id": "<commit_id>",
114 }
114 }
115 },
115 },
116 "author": <user_obj>,
116 "author": <user_obj>,
117 "reviewers": [
117 "reviewers": [
118 ...
118 ...
119 {
119 {
120 "user": "<user_obj>",
120 "user": "<user_obj>",
121 "review_status": "<review_status>",
121 "review_status": "<review_status>",
122 }
122 }
123 ...
123 ...
124 ]
124 ]
125 },
125 },
126 "error": null
126 "error": null
127 """
127 """
128
128
129 pull_request = get_pull_request_or_error(pullrequestid)
129 pull_request = get_pull_request_or_error(pullrequestid)
130 if Optional.extract(repoid):
130 if Optional.extract(repoid):
131 repo = get_repo_or_error(repoid)
131 repo = get_repo_or_error(repoid)
132 else:
132 else:
133 repo = pull_request.target_repo
133 repo = pull_request.target_repo
134
134
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
135 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
136 raise JSONRPCError('repository `%s` or pull request `%s` '
136 raise JSONRPCError('repository `%s` or pull request `%s` '
137 'does not exist' % (repoid, pullrequestid))
137 'does not exist' % (repoid, pullrequestid))
138
138
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
139 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
140 # otherwise we can lock the repo on calculation of merge state while update/merge
140 # otherwise we can lock the repo on calculation of merge state while update/merge
141 # is happening.
141 # is happening.
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
142 pr_created = pull_request.pull_request_state == pull_request.STATE_CREATED
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
143 merge_state = Optional.extract(merge_state, binary=True) and pr_created
144 data = pull_request.get_api_data(with_merge_state=merge_state)
144 data = pull_request.get_api_data(with_merge_state=merge_state)
145 return data
145 return data
146
146
147
147
148 @jsonrpc_method()
148 @jsonrpc_method()
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
149 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
150 merge_state=Optional(False)):
150 merge_state=Optional(False)):
151 """
151 """
152 Get all pull requests from the repository specified in `repoid`.
152 Get all pull requests from the repository specified in `repoid`.
153
153
154 :param apiuser: This is filled automatically from the |authtoken|.
154 :param apiuser: This is filled automatically from the |authtoken|.
155 :type apiuser: AuthUser
155 :type apiuser: AuthUser
156 :param repoid: Optional repository name or repository ID.
156 :param repoid: Optional repository name or repository ID.
157 :type repoid: str or int
157 :type repoid: str or int
158 :param status: Only return pull requests with the specified status.
158 :param status: Only return pull requests with the specified status.
159 Valid options are.
159 Valid options are.
160 * ``new`` (default)
160 * ``new`` (default)
161 * ``open``
161 * ``open``
162 * ``closed``
162 * ``closed``
163 :type status: str
163 :type status: str
164 :param merge_state: Optional calculate merge state for each repository.
164 :param merge_state: Optional calculate merge state for each repository.
165 This could result in longer time to fetch the data
165 This could result in longer time to fetch the data
166 :type merge_state: bool
166 :type merge_state: bool
167
167
168 Example output:
168 Example output:
169
169
170 .. code-block:: bash
170 .. code-block:: bash
171
171
172 "id": <id_given_in_input>,
172 "id": <id_given_in_input>,
173 "result":
173 "result":
174 [
174 [
175 ...
175 ...
176 {
176 {
177 "pull_request_id": "<pull_request_id>",
177 "pull_request_id": "<pull_request_id>",
178 "url": "<url>",
178 "url": "<url>",
179 "title" : "<title>",
179 "title" : "<title>",
180 "description": "<description>",
180 "description": "<description>",
181 "status": "<status>",
181 "status": "<status>",
182 "created_on": "<date_time_created>",
182 "created_on": "<date_time_created>",
183 "updated_on": "<date_time_updated>",
183 "updated_on": "<date_time_updated>",
184 "commit_ids": [
184 "commit_ids": [
185 ...
185 ...
186 "<commit_id>",
186 "<commit_id>",
187 "<commit_id>",
187 "<commit_id>",
188 ...
188 ...
189 ],
189 ],
190 "review_status": "<review_status>",
190 "review_status": "<review_status>",
191 "mergeable": {
191 "mergeable": {
192 "status": "<bool>",
192 "status": "<bool>",
193 "message: "<message>",
193 "message: "<message>",
194 },
194 },
195 "source": {
195 "source": {
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 "target": {
204 "target": {
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 "merge": {
213 "merge": {
214 "clone_url": "<clone_url>",
214 "clone_url": "<clone_url>",
215 "reference":
215 "reference":
216 {
216 {
217 "name": "<name>",
217 "name": "<name>",
218 "type": "<type>",
218 "type": "<type>",
219 "commit_id": "<commit_id>",
219 "commit_id": "<commit_id>",
220 }
220 }
221 },
221 },
222 "author": <user_obj>,
222 "author": <user_obj>,
223 "reviewers": [
223 "reviewers": [
224 ...
224 ...
225 {
225 {
226 "user": "<user_obj>",
226 "user": "<user_obj>",
227 "review_status": "<review_status>",
227 "review_status": "<review_status>",
228 }
228 }
229 ...
229 ...
230 ]
230 ]
231 }
231 }
232 ...
232 ...
233 ],
233 ],
234 "error": null
234 "error": null
235
235
236 """
236 """
237 repo = get_repo_or_error(repoid)
237 repo = get_repo_or_error(repoid)
238 if not has_superadmin_permission(apiuser):
238 if not has_superadmin_permission(apiuser):
239 _perms = (
239 _perms = (
240 'repository.admin', 'repository.write', 'repository.read',)
240 'repository.admin', 'repository.write', 'repository.read',)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
241 validate_repo_permissions(apiuser, repoid, repo, _perms)
242
242
243 status = Optional.extract(status)
243 status = Optional.extract(status)
244 merge_state = Optional.extract(merge_state, binary=True)
244 merge_state = Optional.extract(merge_state, binary=True)
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
245 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
246 order_by='id', order_dir='desc')
246 order_by='id', order_dir='desc')
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
247 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
248 return data
248 return data
249
249
250
250
251 @jsonrpc_method()
251 @jsonrpc_method()
252 def merge_pull_request(
252 def merge_pull_request(
253 request, apiuser, pullrequestid, repoid=Optional(None),
253 request, apiuser, pullrequestid, repoid=Optional(None),
254 userid=Optional(OAttr('apiuser'))):
254 userid=Optional(OAttr('apiuser'))):
255 """
255 """
256 Merge the pull request specified by `pullrequestid` into its target
256 Merge the pull request specified by `pullrequestid` into its target
257 repository.
257 repository.
258
258
259 :param apiuser: This is filled automatically from the |authtoken|.
259 :param apiuser: This is filled automatically from the |authtoken|.
260 :type apiuser: AuthUser
260 :type apiuser: AuthUser
261 :param repoid: Optional, repository name or repository ID of the
261 :param repoid: Optional, repository name or repository ID of the
262 target repository to which the |pr| is to be merged.
262 target repository to which the |pr| is to be merged.
263 :type repoid: str or int
263 :type repoid: str or int
264 :param pullrequestid: ID of the pull request which shall be merged.
264 :param pullrequestid: ID of the pull request which shall be merged.
265 :type pullrequestid: int
265 :type pullrequestid: int
266 :param userid: Merge the pull request as this user.
266 :param userid: Merge the pull request as this user.
267 :type userid: Optional(str or int)
267 :type userid: Optional(str or int)
268
268
269 Example output:
269 Example output:
270
270
271 .. code-block:: bash
271 .. code-block:: bash
272
272
273 "id": <id_given_in_input>,
273 "id": <id_given_in_input>,
274 "result": {
274 "result": {
275 "executed": "<bool>",
275 "executed": "<bool>",
276 "failure_reason": "<int>",
276 "failure_reason": "<int>",
277 "merge_status_message": "<str>",
277 "merge_status_message": "<str>",
278 "merge_commit_id": "<merge_commit_id>",
278 "merge_commit_id": "<merge_commit_id>",
279 "possible": "<bool>",
279 "possible": "<bool>",
280 "merge_ref": {
280 "merge_ref": {
281 "commit_id": "<commit_id>",
281 "commit_id": "<commit_id>",
282 "type": "<type>",
282 "type": "<type>",
283 "name": "<name>"
283 "name": "<name>"
284 }
284 }
285 },
285 },
286 "error": null
286 "error": null
287 """
287 """
288 pull_request = get_pull_request_or_error(pullrequestid)
288 pull_request = get_pull_request_or_error(pullrequestid)
289 if Optional.extract(repoid):
289 if Optional.extract(repoid):
290 repo = get_repo_or_error(repoid)
290 repo = get_repo_or_error(repoid)
291 else:
291 else:
292 repo = pull_request.target_repo
292 repo = pull_request.target_repo
293 auth_user = apiuser
293 auth_user = apiuser
294
294
295 if not isinstance(userid, Optional):
295 if not isinstance(userid, Optional):
296 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
296 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
297 user=apiuser, repo_name=repo.repo_name)
297 user=apiuser, repo_name=repo.repo_name)
298 if has_superadmin_permission(apiuser) or is_repo_admin:
298 if has_superadmin_permission(apiuser) or is_repo_admin:
299 apiuser = get_user_or_error(userid)
299 apiuser = get_user_or_error(userid)
300 auth_user = apiuser.AuthUser()
300 auth_user = apiuser.AuthUser()
301 else:
301 else:
302 raise JSONRPCError('userid is not the same as your user')
302 raise JSONRPCError('userid is not the same as your user')
303
303
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
304 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
305 raise JSONRPCError(
305 raise JSONRPCError(
306 'Operation forbidden because pull request is in state {}, '
306 'Operation forbidden because pull request is in state {}, '
307 'only state {} is allowed.'.format(
307 'only state {} is allowed.'.format(
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
308 pull_request.pull_request_state, PullRequest.STATE_CREATED))
309
309
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
310 with pull_request.set_state(PullRequest.STATE_UPDATING):
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
311 check = MergeCheck.validate(pull_request, auth_user=auth_user,
312 translator=request.translate)
312 translator=request.translate)
313 merge_possible = not check.failed
313 merge_possible = not check.failed
314
314
315 if not merge_possible:
315 if not merge_possible:
316 error_messages = []
316 error_messages = []
317 for err_type, error_msg in check.errors:
317 for err_type, error_msg in check.errors:
318 error_msg = request.translate(error_msg)
318 error_msg = request.translate(error_msg)
319 error_messages.append(error_msg)
319 error_messages.append(error_msg)
320
320
321 reasons = ','.join(error_messages)
321 reasons = ','.join(error_messages)
322 raise JSONRPCError(
322 raise JSONRPCError(
323 'merge not possible for following reasons: {}'.format(reasons))
323 'merge not possible for following reasons: {}'.format(reasons))
324
324
325 target_repo = pull_request.target_repo
325 target_repo = pull_request.target_repo
326 extras = vcs_operation_context(
326 extras = vcs_operation_context(
327 request.environ, repo_name=target_repo.repo_name,
327 request.environ, repo_name=target_repo.repo_name,
328 username=auth_user.username, action='push',
328 username=auth_user.username, action='push',
329 scm=target_repo.repo_type)
329 scm=target_repo.repo_type)
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
330 with pull_request.set_state(PullRequest.STATE_UPDATING):
331 merge_response = PullRequestModel().merge_repo(
331 merge_response = PullRequestModel().merge_repo(
332 pull_request, apiuser, extras=extras)
332 pull_request, apiuser, extras=extras)
333 if merge_response.executed:
333 if merge_response.executed:
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
334 PullRequestModel().close_pull_request(pull_request.pull_request_id, auth_user)
335
335
336 Session().commit()
336 Session().commit()
337
337
338 # In previous versions the merge response directly contained the merge
338 # In previous versions the merge response directly contained the merge
339 # commit id. It is now contained in the merge reference object. To be
339 # commit id. It is now contained in the merge reference object. To be
340 # backwards compatible we have to extract it again.
340 # backwards compatible we have to extract it again.
341 merge_response = merge_response.asdict()
341 merge_response = merge_response.asdict()
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
342 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
343
343
344 return merge_response
344 return merge_response
345
345
346
346
347 @jsonrpc_method()
347 @jsonrpc_method()
348 def get_pull_request_comments(
348 def get_pull_request_comments(
349 request, apiuser, pullrequestid, repoid=Optional(None)):
349 request, apiuser, pullrequestid, repoid=Optional(None)):
350 """
350 """
351 Get all comments of pull request specified with the `pullrequestid`
351 Get all comments of pull request specified with the `pullrequestid`
352
352
353 :param apiuser: This is filled automatically from the |authtoken|.
353 :param apiuser: This is filled automatically from the |authtoken|.
354 :type apiuser: AuthUser
354 :type apiuser: AuthUser
355 :param repoid: Optional repository name or repository ID.
355 :param repoid: Optional repository name or repository ID.
356 :type repoid: str or int
356 :type repoid: str or int
357 :param pullrequestid: The pull request ID.
357 :param pullrequestid: The pull request ID.
358 :type pullrequestid: int
358 :type pullrequestid: int
359
359
360 Example output:
360 Example output:
361
361
362 .. code-block:: bash
362 .. code-block:: bash
363
363
364 id : <id_given_in_input>
364 id : <id_given_in_input>
365 result : [
365 result : [
366 {
366 {
367 "comment_author": {
367 "comment_author": {
368 "active": true,
368 "active": true,
369 "full_name_or_username": "Tom Gore",
369 "full_name_or_username": "Tom Gore",
370 "username": "admin"
370 "username": "admin"
371 },
371 },
372 "comment_created_on": "2017-01-02T18:43:45.533",
372 "comment_created_on": "2017-01-02T18:43:45.533",
373 "comment_f_path": null,
373 "comment_f_path": null,
374 "comment_id": 25,
374 "comment_id": 25,
375 "comment_lineno": null,
375 "comment_lineno": null,
376 "comment_status": {
376 "comment_status": {
377 "status": "under_review",
377 "status": "under_review",
378 "status_lbl": "Under Review"
378 "status_lbl": "Under Review"
379 },
379 },
380 "comment_text": "Example text",
380 "comment_text": "Example text",
381 "comment_type": null,
381 "comment_type": null,
382 "comment_last_version: 0,
382 "comment_last_version: 0,
383 "pull_request_version": null,
383 "pull_request_version": null,
384 "comment_commit_id": None,
384 "comment_commit_id": None,
385 "comment_pull_request_id": <pull_request_id>
385 "comment_pull_request_id": <pull_request_id>
386 }
386 }
387 ],
387 ],
388 error : null
388 error : null
389 """
389 """
390
390
391 pull_request = get_pull_request_or_error(pullrequestid)
391 pull_request = get_pull_request_or_error(pullrequestid)
392 if Optional.extract(repoid):
392 if Optional.extract(repoid):
393 repo = get_repo_or_error(repoid)
393 repo = get_repo_or_error(repoid)
394 else:
394 else:
395 repo = pull_request.target_repo
395 repo = pull_request.target_repo
396
396
397 if not PullRequestModel().check_user_read(
397 if not PullRequestModel().check_user_read(
398 pull_request, apiuser, api=True):
398 pull_request, apiuser, api=True):
399 raise JSONRPCError('repository `%s` or pull request `%s` '
399 raise JSONRPCError('repository `%s` or pull request `%s` '
400 'does not exist' % (repoid, pullrequestid))
400 'does not exist' % (repoid, pullrequestid))
401
401
402 (pull_request_latest,
402 (pull_request_latest,
403 pull_request_at_ver,
403 pull_request_at_ver,
404 pull_request_display_obj,
404 pull_request_display_obj,
405 at_version) = PullRequestModel().get_pr_version(
405 at_version) = PullRequestModel().get_pr_version(
406 pull_request.pull_request_id, version=None)
406 pull_request.pull_request_id, version=None)
407
407
408 versions = pull_request_display_obj.versions()
408 versions = pull_request_display_obj.versions()
409 ver_map = {
409 ver_map = {
410 ver.pull_request_version_id: cnt
410 ver.pull_request_version_id: cnt
411 for cnt, ver in enumerate(versions, 1)
411 for cnt, ver in enumerate(versions, 1)
412 }
412 }
413
413
414 # GENERAL COMMENTS with versions #
414 # GENERAL COMMENTS with versions #
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
415 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
416 q = q.order_by(ChangesetComment.comment_id.asc())
416 q = q.order_by(ChangesetComment.comment_id.asc())
417 general_comments = q.all()
417 general_comments = q.all()
418
418
419 # INLINE COMMENTS with versions #
419 # INLINE COMMENTS with versions #
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
420 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
421 q = q.order_by(ChangesetComment.comment_id.asc())
421 q = q.order_by(ChangesetComment.comment_id.asc())
422 inline_comments = q.all()
422 inline_comments = q.all()
423
423
424 data = []
424 data = []
425 for comment in inline_comments + general_comments:
425 for comment in inline_comments + general_comments:
426 full_data = comment.get_api_data()
426 full_data = comment.get_api_data()
427 pr_version_id = None
427 pr_version_id = None
428 if comment.pull_request_version_id:
428 if comment.pull_request_version_id:
429 pr_version_id = 'v{}'.format(
429 pr_version_id = 'v{}'.format(
430 ver_map[comment.pull_request_version_id])
430 ver_map[comment.pull_request_version_id])
431
431
432 # sanitize some entries
432 # sanitize some entries
433
433
434 full_data['pull_request_version'] = pr_version_id
434 full_data['pull_request_version'] = pr_version_id
435 full_data['comment_author'] = {
435 full_data['comment_author'] = {
436 'username': full_data['comment_author'].username,
436 'username': full_data['comment_author'].username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
437 'full_name_or_username': full_data['comment_author'].full_name_or_username,
438 'active': full_data['comment_author'].active,
438 'active': full_data['comment_author'].active,
439 }
439 }
440
440
441 if full_data['comment_status']:
441 if full_data['comment_status']:
442 full_data['comment_status'] = {
442 full_data['comment_status'] = {
443 'status': full_data['comment_status'][0].status,
443 'status': full_data['comment_status'][0].status,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
444 'status_lbl': full_data['comment_status'][0].status_lbl,
445 }
445 }
446 else:
446 else:
447 full_data['comment_status'] = {}
447 full_data['comment_status'] = {}
448
448
449 data.append(full_data)
449 data.append(full_data)
450 return data
450 return data
451
451
452
452
453 @jsonrpc_method()
453 @jsonrpc_method()
454 def comment_pull_request(
454 def comment_pull_request(
455 request, apiuser, pullrequestid, repoid=Optional(None),
455 request, apiuser, pullrequestid, repoid=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
456 message=Optional(None), commit_id=Optional(None), status=Optional(None),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
457 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
458 resolves_comment_id=Optional(None), extra_recipients=Optional([]),
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
459 userid=Optional(OAttr('apiuser')), send_email=Optional(True)):
460 """
460 """
461 Comment on the pull request specified with the `pullrequestid`,
461 Comment on the pull request specified with the `pullrequestid`,
462 in the |repo| specified by the `repoid`, and optionally change the
462 in the |repo| specified by the `repoid`, and optionally change the
463 review status.
463 review status.
464
464
465 :param apiuser: This is filled automatically from the |authtoken|.
465 :param apiuser: This is filled automatically from the |authtoken|.
466 :type apiuser: AuthUser
466 :type apiuser: AuthUser
467 :param repoid: Optional repository name or repository ID.
467 :param repoid: Optional repository name or repository ID.
468 :type repoid: str or int
468 :type repoid: str or int
469 :param pullrequestid: The pull request ID.
469 :param pullrequestid: The pull request ID.
470 :type pullrequestid: int
470 :type pullrequestid: int
471 :param commit_id: Specify the commit_id for which to set a comment. If
471 :param commit_id: Specify the commit_id for which to set a comment. If
472 given commit_id is different than latest in the PR status
472 given commit_id is different than latest in the PR status
473 change won't be performed.
473 change won't be performed.
474 :type commit_id: str
474 :type commit_id: str
475 :param message: The text content of the comment.
475 :param message: The text content of the comment.
476 :type message: str
476 :type message: str
477 :param status: (**Optional**) Set the approval status of the pull
477 :param status: (**Optional**) Set the approval status of the pull
478 request. One of: 'not_reviewed', 'approved', 'rejected',
478 request. One of: 'not_reviewed', 'approved', 'rejected',
479 'under_review'
479 'under_review'
480 :type status: str
480 :type status: str
481 :param comment_type: Comment type, one of: 'note', 'todo'
481 :param comment_type: Comment type, one of: 'note', 'todo'
482 :type comment_type: Optional(str), default: 'note'
482 :type comment_type: Optional(str), default: 'note'
483 :param resolves_comment_id: id of comment which this one will resolve
483 :param resolves_comment_id: id of comment which this one will resolve
484 :type resolves_comment_id: Optional(int)
484 :type resolves_comment_id: Optional(int)
485 :param extra_recipients: list of user ids or usernames to add
485 :param extra_recipients: list of user ids or usernames to add
486 notifications for this comment. Acts like a CC for notification
486 notifications for this comment. Acts like a CC for notification
487 :type extra_recipients: Optional(list)
487 :type extra_recipients: Optional(list)
488 :param userid: Comment on the pull request as this user
488 :param userid: Comment on the pull request as this user
489 :type userid: Optional(str or int)
489 :type userid: Optional(str or int)
490 :param send_email: Define if this comment should also send email notification
490 :param send_email: Define if this comment should also send email notification
491 :type send_email: Optional(bool)
491 :type send_email: Optional(bool)
492
492
493 Example output:
493 Example output:
494
494
495 .. code-block:: bash
495 .. code-block:: bash
496
496
497 id : <id_given_in_input>
497 id : <id_given_in_input>
498 result : {
498 result : {
499 "pull_request_id": "<Integer>",
499 "pull_request_id": "<Integer>",
500 "comment_id": "<Integer>",
500 "comment_id": "<Integer>",
501 "status": {"given": <given_status>,
501 "status": {"given": <given_status>,
502 "was_changed": <bool status_was_actually_changed> },
502 "was_changed": <bool status_was_actually_changed> },
503 },
503 },
504 error : null
504 error : null
505 """
505 """
506 _ = request.translate
506 _ = request.translate
507
507
508 pull_request = get_pull_request_or_error(pullrequestid)
508 pull_request = get_pull_request_or_error(pullrequestid)
509 if Optional.extract(repoid):
509 if Optional.extract(repoid):
510 repo = get_repo_or_error(repoid)
510 repo = get_repo_or_error(repoid)
511 else:
511 else:
512 repo = pull_request.target_repo
512 repo = pull_request.target_repo
513
513
514 db_repo_name = repo.repo_name
514 db_repo_name = repo.repo_name
515 auth_user = apiuser
515 auth_user = apiuser
516 if not isinstance(userid, Optional):
516 if not isinstance(userid, Optional):
517 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
517 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
518 user=apiuser, repo_name=db_repo_name)
518 user=apiuser, repo_name=db_repo_name)
519 if has_superadmin_permission(apiuser) or is_repo_admin:
519 if has_superadmin_permission(apiuser) or is_repo_admin:
520 apiuser = get_user_or_error(userid)
520 apiuser = get_user_or_error(userid)
521 auth_user = apiuser.AuthUser()
521 auth_user = apiuser.AuthUser()
522 else:
522 else:
523 raise JSONRPCError('userid is not the same as your user')
523 raise JSONRPCError('userid is not the same as your user')
524
524
525 if pull_request.is_closed():
525 if pull_request.is_closed():
526 raise JSONRPCError(
526 raise JSONRPCError(
527 'pull request `%s` comment failed, pull request is closed' % (
527 'pull request `%s` comment failed, pull request is closed' % (
528 pullrequestid,))
528 pullrequestid,))
529
529
530 if not PullRequestModel().check_user_read(
530 if not PullRequestModel().check_user_read(
531 pull_request, apiuser, api=True):
531 pull_request, apiuser, api=True):
532 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
532 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
533 message = Optional.extract(message)
533 message = Optional.extract(message)
534 status = Optional.extract(status)
534 status = Optional.extract(status)
535 commit_id = Optional.extract(commit_id)
535 commit_id = Optional.extract(commit_id)
536 comment_type = Optional.extract(comment_type)
536 comment_type = Optional.extract(comment_type)
537 resolves_comment_id = Optional.extract(resolves_comment_id)
537 resolves_comment_id = Optional.extract(resolves_comment_id)
538 extra_recipients = Optional.extract(extra_recipients)
538 extra_recipients = Optional.extract(extra_recipients)
539 send_email = Optional.extract(send_email, binary=True)
539 send_email = Optional.extract(send_email, binary=True)
540
540
541 if not message and not status:
541 if not message and not status:
542 raise JSONRPCError(
542 raise JSONRPCError(
543 'Both message and status parameters are missing. '
543 'Both message and status parameters are missing. '
544 'At least one is required.')
544 'At least one is required.')
545
545
546 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
546 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
547 status is not None):
547 status is not None):
548 raise JSONRPCError('Unknown comment status: `%s`' % status)
548 raise JSONRPCError('Unknown comment status: `%s`' % status)
549
549
550 if commit_id and commit_id not in pull_request.revisions:
550 if commit_id and commit_id not in pull_request.revisions:
551 raise JSONRPCError(
551 raise JSONRPCError(
552 'Invalid commit_id `%s` for this pull request.' % commit_id)
552 'Invalid commit_id `%s` for this pull request.' % commit_id)
553
553
554 allowed_to_change_status = PullRequestModel().check_user_change_status(
554 allowed_to_change_status = PullRequestModel().check_user_change_status(
555 pull_request, apiuser)
555 pull_request, apiuser)
556
556
557 # if commit_id is passed re-validated if user is allowed to change status
557 # if commit_id is passed re-validated if user is allowed to change status
558 # based on latest commit_id from the PR
558 # based on latest commit_id from the PR
559 if commit_id:
559 if commit_id:
560 commit_idx = pull_request.revisions.index(commit_id)
560 commit_idx = pull_request.revisions.index(commit_id)
561 if commit_idx != 0:
561 if commit_idx != 0:
562 allowed_to_change_status = False
562 allowed_to_change_status = False
563
563
564 if resolves_comment_id:
564 if resolves_comment_id:
565 comment = ChangesetComment.get(resolves_comment_id)
565 comment = ChangesetComment.get(resolves_comment_id)
566 if not comment:
566 if not comment:
567 raise JSONRPCError(
567 raise JSONRPCError(
568 'Invalid resolves_comment_id `%s` for this pull request.'
568 'Invalid resolves_comment_id `%s` for this pull request.'
569 % resolves_comment_id)
569 % resolves_comment_id)
570 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
570 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
571 raise JSONRPCError(
571 raise JSONRPCError(
572 'Comment `%s` is wrong type for setting status to resolved.'
572 'Comment `%s` is wrong type for setting status to resolved.'
573 % resolves_comment_id)
573 % resolves_comment_id)
574
574
575 text = message
575 text = message
576 status_label = ChangesetStatus.get_status_lbl(status)
576 status_label = ChangesetStatus.get_status_lbl(status)
577 if status and allowed_to_change_status:
577 if status and allowed_to_change_status:
578 st_message = ('Status change %(transition_icon)s %(status)s'
578 st_message = ('Status change %(transition_icon)s %(status)s'
579 % {'transition_icon': '>', 'status': status_label})
579 % {'transition_icon': '>', 'status': status_label})
580 text = message or st_message
580 text = message or st_message
581
581
582 rc_config = SettingsModel().get_all_settings()
582 rc_config = SettingsModel().get_all_settings()
583 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
583 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
584
584
585 status_change = status and allowed_to_change_status
585 status_change = status and allowed_to_change_status
586 comment = CommentsModel().create(
586 comment = CommentsModel().create(
587 text=text,
587 text=text,
588 repo=pull_request.target_repo.repo_id,
588 repo=pull_request.target_repo.repo_id,
589 user=apiuser.user_id,
589 user=apiuser.user_id,
590 pull_request=pull_request.pull_request_id,
590 pull_request=pull_request.pull_request_id,
591 f_path=None,
591 f_path=None,
592 line_no=None,
592 line_no=None,
593 status_change=(status_label if status_change else None),
593 status_change=(status_label if status_change else None),
594 status_change_type=(status if status_change else None),
594 status_change_type=(status if status_change else None),
595 closing_pr=False,
595 closing_pr=False,
596 renderer=renderer,
596 renderer=renderer,
597 comment_type=comment_type,
597 comment_type=comment_type,
598 resolves_comment_id=resolves_comment_id,
598 resolves_comment_id=resolves_comment_id,
599 auth_user=auth_user,
599 auth_user=auth_user,
600 extra_recipients=extra_recipients,
600 extra_recipients=extra_recipients,
601 send_email=send_email
601 send_email=send_email
602 )
602 )
603 is_inline = bool(comment.f_path and comment.line_no)
603 is_inline = bool(comment.f_path and comment.line_no)
604
604
605 if allowed_to_change_status and status:
605 if allowed_to_change_status and status:
606 old_calculated_status = pull_request.calculated_review_status()
606 old_calculated_status = pull_request.calculated_review_status()
607 ChangesetStatusModel().set_status(
607 ChangesetStatusModel().set_status(
608 pull_request.target_repo.repo_id,
608 pull_request.target_repo.repo_id,
609 status,
609 status,
610 apiuser.user_id,
610 apiuser.user_id,
611 comment,
611 comment,
612 pull_request=pull_request.pull_request_id
612 pull_request=pull_request.pull_request_id
613 )
613 )
614 Session().flush()
614 Session().flush()
615
615
616 Session().commit()
616 Session().commit()
617
617
618 PullRequestModel().trigger_pull_request_hook(
618 PullRequestModel().trigger_pull_request_hook(
619 pull_request, apiuser, 'comment',
619 pull_request, apiuser, 'comment',
620 data={'comment': comment})
620 data={'comment': comment})
621
621
622 if allowed_to_change_status and status:
622 if allowed_to_change_status and status:
623 # we now calculate the status of pull request, and based on that
623 # we now calculate the status of pull request, and based on that
624 # calculation we set the commits status
624 # calculation we set the commits status
625 calculated_status = pull_request.calculated_review_status()
625 calculated_status = pull_request.calculated_review_status()
626 if old_calculated_status != calculated_status: