##// END OF EJS Templates
api: pull-requests get_all now sortes the results and have a flag to show/hide merge result state
marcink -
r3445:f33ce258 default
parent child Browse files
Show More
@@ -1,980 +1,986 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error)
29 validate_repo_permissions, resolve_ref_or_error)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
53 the pull request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57
57
58 Example output:
58 Example output:
59
59
60 .. code-block:: bash
60 .. code-block:: bash
61
61
62 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
63 "result":
63 "result":
64 {
64 {
65 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
66 "url": "<url>",
66 "url": "<url>",
67 "title": "<title>",
67 "title": "<title>",
68 "description": "<description>",
68 "description": "<description>",
69 "status" : "<status>",
69 "status" : "<status>",
70 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
71 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
72 "commit_ids": [
72 "commit_ids": [
73 ...
73 ...
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 "<commit_id>",
76 ...
76 ...
77 ],
77 ],
78 "review_status": "<review_status>",
78 "review_status": "<review_status>",
79 "mergeable": {
79 "mergeable": {
80 "status": "<bool>",
80 "status": "<bool>",
81 "message": "<message>",
81 "message": "<message>",
82 },
82 },
83 "source": {
83 "source": {
84 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
85 "repository": "<repository_name>",
85 "repository": "<repository_name>",
86 "reference":
86 "reference":
87 {
87 {
88 "name": "<name>",
88 "name": "<name>",
89 "type": "<type>",
89 "type": "<type>",
90 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
91 }
91 }
92 },
92 },
93 "target": {
93 "target": {
94 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
95 "repository": "<repository_name>",
95 "repository": "<repository_name>",
96 "reference":
96 "reference":
97 {
97 {
98 "name": "<name>",
98 "name": "<name>",
99 "type": "<type>",
99 "type": "<type>",
100 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
101 }
101 }
102 },
102 },
103 "merge": {
103 "merge": {
104 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
105 "reference":
105 "reference":
106 {
106 {
107 "name": "<name>",
107 "name": "<name>",
108 "type": "<type>",
108 "type": "<type>",
109 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
110 }
110 }
111 },
111 },
112 "author": <user_obj>,
112 "author": <user_obj>,
113 "reviewers": [
113 "reviewers": [
114 ...
114 ...
115 {
115 {
116 "user": "<user_obj>",
116 "user": "<user_obj>",
117 "review_status": "<review_status>",
117 "review_status": "<review_status>",
118 }
118 }
119 ...
119 ...
120 ]
120 ]
121 },
121 },
122 "error": null
122 "error": null
123 """
123 """
124
124
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if Optional.extract(repoid):
126 if Optional.extract(repoid):
127 repo = get_repo_or_error(repoid)
127 repo = get_repo_or_error(repoid)
128 else:
128 else:
129 repo = pull_request.target_repo
129 repo = pull_request.target_repo
130
130
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
132 raise JSONRPCError('repository `%s` or pull request `%s` '
132 raise JSONRPCError('repository `%s` or pull request `%s` '
133 'does not exist' % (repoid, pullrequestid))
133 'does not exist' % (repoid, pullrequestid))
134
134
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
136 # otherwise we can lock the repo on calculation of merge state while update/merge
136 # otherwise we can lock the repo on calculation of merge state while update/merge
137 # is happening.
137 # is happening.
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
139 data = pull_request.get_api_data(with_merge_state=merge_state)
139 data = pull_request.get_api_data(with_merge_state=merge_state)
140 return data
140 return data
141
141
142
142
143 @jsonrpc_method()
143 @jsonrpc_method()
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
145 merge_state=Optional(True)):
145 """
146 """
146 Get all pull requests from the repository specified in `repoid`.
147 Get all pull requests from the repository specified in `repoid`.
147
148
148 :param apiuser: This is filled automatically from the |authtoken|.
149 :param apiuser: This is filled automatically from the |authtoken|.
149 :type apiuser: AuthUser
150 :type apiuser: AuthUser
150 :param repoid: Optional repository name or repository ID.
151 :param repoid: Optional repository name or repository ID.
151 :type repoid: str or int
152 :type repoid: str or int
152 :param status: Only return pull requests with the specified status.
153 :param status: Only return pull requests with the specified status.
153 Valid options are.
154 Valid options are.
154 * ``new`` (default)
155 * ``new`` (default)
155 * ``open``
156 * ``open``
156 * ``closed``
157 * ``closed``
157 :type status: str
158 :type status: str
159 :param merge_state: Optional calculate merge state for each repository.
160 This could result in longer time to fetch the data
161 :type merge_state: bool
158
162
159 Example output:
163 Example output:
160
164
161 .. code-block:: bash
165 .. code-block:: bash
162
166
163 "id": <id_given_in_input>,
167 "id": <id_given_in_input>,
164 "result":
168 "result":
165 [
169 [
166 ...
170 ...
167 {
171 {
168 "pull_request_id": "<pull_request_id>",
172 "pull_request_id": "<pull_request_id>",
169 "url": "<url>",
173 "url": "<url>",
170 "title" : "<title>",
174 "title" : "<title>",
171 "description": "<description>",
175 "description": "<description>",
172 "status": "<status>",
176 "status": "<status>",
173 "created_on": "<date_time_created>",
177 "created_on": "<date_time_created>",
174 "updated_on": "<date_time_updated>",
178 "updated_on": "<date_time_updated>",
175 "commit_ids": [
179 "commit_ids": [
176 ...
180 ...
177 "<commit_id>",
181 "<commit_id>",
178 "<commit_id>",
182 "<commit_id>",
179 ...
183 ...
180 ],
184 ],
181 "review_status": "<review_status>",
185 "review_status": "<review_status>",
182 "mergeable": {
186 "mergeable": {
183 "status": "<bool>",
187 "status": "<bool>",
184 "message: "<message>",
188 "message: "<message>",
185 },
189 },
186 "source": {
190 "source": {
187 "clone_url": "<clone_url>",
191 "clone_url": "<clone_url>",
188 "reference":
192 "reference":
189 {
193 {
190 "name": "<name>",
194 "name": "<name>",
191 "type": "<type>",
195 "type": "<type>",
192 "commit_id": "<commit_id>",
196 "commit_id": "<commit_id>",
193 }
197 }
194 },
198 },
195 "target": {
199 "target": {
196 "clone_url": "<clone_url>",
200 "clone_url": "<clone_url>",
197 "reference":
201 "reference":
198 {
202 {
199 "name": "<name>",
203 "name": "<name>",
200 "type": "<type>",
204 "type": "<type>",
201 "commit_id": "<commit_id>",
205 "commit_id": "<commit_id>",
202 }
206 }
203 },
207 },
204 "merge": {
208 "merge": {
205 "clone_url": "<clone_url>",
209 "clone_url": "<clone_url>",
206 "reference":
210 "reference":
207 {
211 {
208 "name": "<name>",
212 "name": "<name>",
209 "type": "<type>",
213 "type": "<type>",
210 "commit_id": "<commit_id>",
214 "commit_id": "<commit_id>",
211 }
215 }
212 },
216 },
213 "author": <user_obj>,
217 "author": <user_obj>,
214 "reviewers": [
218 "reviewers": [
215 ...
219 ...
216 {
220 {
217 "user": "<user_obj>",
221 "user": "<user_obj>",
218 "review_status": "<review_status>",
222 "review_status": "<review_status>",
219 }
223 }
220 ...
224 ...
221 ]
225 ]
222 }
226 }
223 ...
227 ...
224 ],
228 ],
225 "error": null
229 "error": null
226
230
227 """
231 """
228 repo = get_repo_or_error(repoid)
232 repo = get_repo_or_error(repoid)
229 if not has_superadmin_permission(apiuser):
233 if not has_superadmin_permission(apiuser):
230 _perms = (
234 _perms = (
231 'repository.admin', 'repository.write', 'repository.read',)
235 'repository.admin', 'repository.write', 'repository.read',)
232 validate_repo_permissions(apiuser, repoid, repo, _perms)
236 validate_repo_permissions(apiuser, repoid, repo, _perms)
233
237
234 status = Optional.extract(status)
238 status = Optional.extract(status)
235 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
239 merge_state = Optional.extract(merge_state, binary=True)
236 data = [pr.get_api_data() for pr in pull_requests]
240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
241 order_by='id', order_dir='desc')
242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
237 return data
243 return data
238
244
239
245
240 @jsonrpc_method()
246 @jsonrpc_method()
241 def merge_pull_request(
247 def merge_pull_request(
242 request, apiuser, pullrequestid, repoid=Optional(None),
248 request, apiuser, pullrequestid, repoid=Optional(None),
243 userid=Optional(OAttr('apiuser'))):
249 userid=Optional(OAttr('apiuser'))):
244 """
250 """
245 Merge the pull request specified by `pullrequestid` into its target
251 Merge the pull request specified by `pullrequestid` into its target
246 repository.
252 repository.
247
253
248 :param apiuser: This is filled automatically from the |authtoken|.
254 :param apiuser: This is filled automatically from the |authtoken|.
249 :type apiuser: AuthUser
255 :type apiuser: AuthUser
250 :param repoid: Optional, repository name or repository ID of the
256 :param repoid: Optional, repository name or repository ID of the
251 target repository to which the |pr| is to be merged.
257 target repository to which the |pr| is to be merged.
252 :type repoid: str or int
258 :type repoid: str or int
253 :param pullrequestid: ID of the pull request which shall be merged.
259 :param pullrequestid: ID of the pull request which shall be merged.
254 :type pullrequestid: int
260 :type pullrequestid: int
255 :param userid: Merge the pull request as this user.
261 :param userid: Merge the pull request as this user.
256 :type userid: Optional(str or int)
262 :type userid: Optional(str or int)
257
263
258 Example output:
264 Example output:
259
265
260 .. code-block:: bash
266 .. code-block:: bash
261
267
262 "id": <id_given_in_input>,
268 "id": <id_given_in_input>,
263 "result": {
269 "result": {
264 "executed": "<bool>",
270 "executed": "<bool>",
265 "failure_reason": "<int>",
271 "failure_reason": "<int>",
266 "merge_commit_id": "<merge_commit_id>",
272 "merge_commit_id": "<merge_commit_id>",
267 "possible": "<bool>",
273 "possible": "<bool>",
268 "merge_ref": {
274 "merge_ref": {
269 "commit_id": "<commit_id>",
275 "commit_id": "<commit_id>",
270 "type": "<type>",
276 "type": "<type>",
271 "name": "<name>"
277 "name": "<name>"
272 }
278 }
273 },
279 },
274 "error": null
280 "error": null
275 """
281 """
276 pull_request = get_pull_request_or_error(pullrequestid)
282 pull_request = get_pull_request_or_error(pullrequestid)
277 if Optional.extract(repoid):
283 if Optional.extract(repoid):
278 repo = get_repo_or_error(repoid)
284 repo = get_repo_or_error(repoid)
279 else:
285 else:
280 repo = pull_request.target_repo
286 repo = pull_request.target_repo
281
287
282 if not isinstance(userid, Optional):
288 if not isinstance(userid, Optional):
283 if (has_superadmin_permission(apiuser) or
289 if (has_superadmin_permission(apiuser) or
284 HasRepoPermissionAnyApi('repository.admin')(
290 HasRepoPermissionAnyApi('repository.admin')(
285 user=apiuser, repo_name=repo.repo_name)):
291 user=apiuser, repo_name=repo.repo_name)):
286 apiuser = get_user_or_error(userid)
292 apiuser = get_user_or_error(userid)
287 else:
293 else:
288 raise JSONRPCError('userid is not the same as your user')
294 raise JSONRPCError('userid is not the same as your user')
289
295
290 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
296 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
291 raise JSONRPCError(
297 raise JSONRPCError(
292 'Operation forbidden because pull request is in state {}, '
298 'Operation forbidden because pull request is in state {}, '
293 'only state {} is allowed.'.format(
299 'only state {} is allowed.'.format(
294 pull_request.pull_request_state, PullRequest.STATE_CREATED))
300 pull_request.pull_request_state, PullRequest.STATE_CREATED))
295
301
296 with pull_request.set_state(PullRequest.STATE_UPDATING):
302 with pull_request.set_state(PullRequest.STATE_UPDATING):
297 check = MergeCheck.validate(
303 check = MergeCheck.validate(
298 pull_request, auth_user=apiuser,
304 pull_request, auth_user=apiuser,
299 translator=request.translate)
305 translator=request.translate)
300 merge_possible = not check.failed
306 merge_possible = not check.failed
301
307
302 if not merge_possible:
308 if not merge_possible:
303 error_messages = []
309 error_messages = []
304 for err_type, error_msg in check.errors:
310 for err_type, error_msg in check.errors:
305 error_msg = request.translate(error_msg)
311 error_msg = request.translate(error_msg)
306 error_messages.append(error_msg)
312 error_messages.append(error_msg)
307
313
308 reasons = ','.join(error_messages)
314 reasons = ','.join(error_messages)
309 raise JSONRPCError(
315 raise JSONRPCError(
310 'merge not possible for following reasons: {}'.format(reasons))
316 'merge not possible for following reasons: {}'.format(reasons))
311
317
312 target_repo = pull_request.target_repo
318 target_repo = pull_request.target_repo
313 extras = vcs_operation_context(
319 extras = vcs_operation_context(
314 request.environ, repo_name=target_repo.repo_name,
320 request.environ, repo_name=target_repo.repo_name,
315 username=apiuser.username, action='push',
321 username=apiuser.username, action='push',
316 scm=target_repo.repo_type)
322 scm=target_repo.repo_type)
317 with pull_request.set_state(PullRequest.STATE_UPDATING):
323 with pull_request.set_state(PullRequest.STATE_UPDATING):
318 merge_response = PullRequestModel().merge_repo(
324 merge_response = PullRequestModel().merge_repo(
319 pull_request, apiuser, extras=extras)
325 pull_request, apiuser, extras=extras)
320 if merge_response.executed:
326 if merge_response.executed:
321 PullRequestModel().close_pull_request(
327 PullRequestModel().close_pull_request(
322 pull_request.pull_request_id, apiuser)
328 pull_request.pull_request_id, apiuser)
323
329
324 Session().commit()
330 Session().commit()
325
331
326 # In previous versions the merge response directly contained the merge
332 # In previous versions the merge response directly contained the merge
327 # commit id. It is now contained in the merge reference object. To be
333 # commit id. It is now contained in the merge reference object. To be
328 # backwards compatible we have to extract it again.
334 # backwards compatible we have to extract it again.
329 merge_response = merge_response.asdict()
335 merge_response = merge_response.asdict()
330 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
336 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
331
337
332 return merge_response
338 return merge_response
333
339
334
340
335 @jsonrpc_method()
341 @jsonrpc_method()
336 def get_pull_request_comments(
342 def get_pull_request_comments(
337 request, apiuser, pullrequestid, repoid=Optional(None)):
343 request, apiuser, pullrequestid, repoid=Optional(None)):
338 """
344 """
339 Get all comments of pull request specified with the `pullrequestid`
345 Get all comments of pull request specified with the `pullrequestid`
340
346
341 :param apiuser: This is filled automatically from the |authtoken|.
347 :param apiuser: This is filled automatically from the |authtoken|.
342 :type apiuser: AuthUser
348 :type apiuser: AuthUser
343 :param repoid: Optional repository name or repository ID.
349 :param repoid: Optional repository name or repository ID.
344 :type repoid: str or int
350 :type repoid: str or int
345 :param pullrequestid: The pull request ID.
351 :param pullrequestid: The pull request ID.
346 :type pullrequestid: int
352 :type pullrequestid: int
347
353
348 Example output:
354 Example output:
349
355
350 .. code-block:: bash
356 .. code-block:: bash
351
357
352 id : <id_given_in_input>
358 id : <id_given_in_input>
353 result : [
359 result : [
354 {
360 {
355 "comment_author": {
361 "comment_author": {
356 "active": true,
362 "active": true,
357 "full_name_or_username": "Tom Gore",
363 "full_name_or_username": "Tom Gore",
358 "username": "admin"
364 "username": "admin"
359 },
365 },
360 "comment_created_on": "2017-01-02T18:43:45.533",
366 "comment_created_on": "2017-01-02T18:43:45.533",
361 "comment_f_path": null,
367 "comment_f_path": null,
362 "comment_id": 25,
368 "comment_id": 25,
363 "comment_lineno": null,
369 "comment_lineno": null,
364 "comment_status": {
370 "comment_status": {
365 "status": "under_review",
371 "status": "under_review",
366 "status_lbl": "Under Review"
372 "status_lbl": "Under Review"
367 },
373 },
368 "comment_text": "Example text",
374 "comment_text": "Example text",
369 "comment_type": null,
375 "comment_type": null,
370 "pull_request_version": null
376 "pull_request_version": null
371 }
377 }
372 ],
378 ],
373 error : null
379 error : null
374 """
380 """
375
381
376 pull_request = get_pull_request_or_error(pullrequestid)
382 pull_request = get_pull_request_or_error(pullrequestid)
377 if Optional.extract(repoid):
383 if Optional.extract(repoid):
378 repo = get_repo_or_error(repoid)
384 repo = get_repo_or_error(repoid)
379 else:
385 else:
380 repo = pull_request.target_repo
386 repo = pull_request.target_repo
381
387
382 if not PullRequestModel().check_user_read(
388 if not PullRequestModel().check_user_read(
383 pull_request, apiuser, api=True):
389 pull_request, apiuser, api=True):
384 raise JSONRPCError('repository `%s` or pull request `%s` '
390 raise JSONRPCError('repository `%s` or pull request `%s` '
385 'does not exist' % (repoid, pullrequestid))
391 'does not exist' % (repoid, pullrequestid))
386
392
387 (pull_request_latest,
393 (pull_request_latest,
388 pull_request_at_ver,
394 pull_request_at_ver,
389 pull_request_display_obj,
395 pull_request_display_obj,
390 at_version) = PullRequestModel().get_pr_version(
396 at_version) = PullRequestModel().get_pr_version(
391 pull_request.pull_request_id, version=None)
397 pull_request.pull_request_id, version=None)
392
398
393 versions = pull_request_display_obj.versions()
399 versions = pull_request_display_obj.versions()
394 ver_map = {
400 ver_map = {
395 ver.pull_request_version_id: cnt
401 ver.pull_request_version_id: cnt
396 for cnt, ver in enumerate(versions, 1)
402 for cnt, ver in enumerate(versions, 1)
397 }
403 }
398
404
399 # GENERAL COMMENTS with versions #
405 # GENERAL COMMENTS with versions #
400 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
406 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
401 q = q.order_by(ChangesetComment.comment_id.asc())
407 q = q.order_by(ChangesetComment.comment_id.asc())
402 general_comments = q.all()
408 general_comments = q.all()
403
409
404 # INLINE COMMENTS with versions #
410 # INLINE COMMENTS with versions #
405 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
411 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
406 q = q.order_by(ChangesetComment.comment_id.asc())
412 q = q.order_by(ChangesetComment.comment_id.asc())
407 inline_comments = q.all()
413 inline_comments = q.all()
408
414
409 data = []
415 data = []
410 for comment in inline_comments + general_comments:
416 for comment in inline_comments + general_comments:
411 full_data = comment.get_api_data()
417 full_data = comment.get_api_data()
412 pr_version_id = None
418 pr_version_id = None
413 if comment.pull_request_version_id:
419 if comment.pull_request_version_id:
414 pr_version_id = 'v{}'.format(
420 pr_version_id = 'v{}'.format(
415 ver_map[comment.pull_request_version_id])
421 ver_map[comment.pull_request_version_id])
416
422
417 # sanitize some entries
423 # sanitize some entries
418
424
419 full_data['pull_request_version'] = pr_version_id
425 full_data['pull_request_version'] = pr_version_id
420 full_data['comment_author'] = {
426 full_data['comment_author'] = {
421 'username': full_data['comment_author'].username,
427 'username': full_data['comment_author'].username,
422 'full_name_or_username': full_data['comment_author'].full_name_or_username,
428 'full_name_or_username': full_data['comment_author'].full_name_or_username,
423 'active': full_data['comment_author'].active,
429 'active': full_data['comment_author'].active,
424 }
430 }
425
431
426 if full_data['comment_status']:
432 if full_data['comment_status']:
427 full_data['comment_status'] = {
433 full_data['comment_status'] = {
428 'status': full_data['comment_status'][0].status,
434 'status': full_data['comment_status'][0].status,
429 'status_lbl': full_data['comment_status'][0].status_lbl,
435 'status_lbl': full_data['comment_status'][0].status_lbl,
430 }
436 }
431 else:
437 else:
432 full_data['comment_status'] = {}
438 full_data['comment_status'] = {}
433
439
434 data.append(full_data)
440 data.append(full_data)
435 return data
441 return data
436
442
437
443
438 @jsonrpc_method()
444 @jsonrpc_method()
439 def comment_pull_request(
445 def comment_pull_request(
440 request, apiuser, pullrequestid, repoid=Optional(None),
446 request, apiuser, pullrequestid, repoid=Optional(None),
441 message=Optional(None), commit_id=Optional(None), status=Optional(None),
447 message=Optional(None), commit_id=Optional(None), status=Optional(None),
442 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
448 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
443 resolves_comment_id=Optional(None),
449 resolves_comment_id=Optional(None),
444 userid=Optional(OAttr('apiuser'))):
450 userid=Optional(OAttr('apiuser'))):
445 """
451 """
446 Comment on the pull request specified with the `pullrequestid`,
452 Comment on the pull request specified with the `pullrequestid`,
447 in the |repo| specified by the `repoid`, and optionally change the
453 in the |repo| specified by the `repoid`, and optionally change the
448 review status.
454 review status.
449
455
450 :param apiuser: This is filled automatically from the |authtoken|.
456 :param apiuser: This is filled automatically from the |authtoken|.
451 :type apiuser: AuthUser
457 :type apiuser: AuthUser
452 :param repoid: Optional repository name or repository ID.
458 :param repoid: Optional repository name or repository ID.
453 :type repoid: str or int
459 :type repoid: str or int
454 :param pullrequestid: The pull request ID.
460 :param pullrequestid: The pull request ID.
455 :type pullrequestid: int
461 :type pullrequestid: int
456 :param commit_id: Specify the commit_id for which to set a comment. If
462 :param commit_id: Specify the commit_id for which to set a comment. If
457 given commit_id is different than latest in the PR status
463 given commit_id is different than latest in the PR status
458 change won't be performed.
464 change won't be performed.
459 :type commit_id: str
465 :type commit_id: str
460 :param message: The text content of the comment.
466 :param message: The text content of the comment.
461 :type message: str
467 :type message: str
462 :param status: (**Optional**) Set the approval status of the pull
468 :param status: (**Optional**) Set the approval status of the pull
463 request. One of: 'not_reviewed', 'approved', 'rejected',
469 request. One of: 'not_reviewed', 'approved', 'rejected',
464 'under_review'
470 'under_review'
465 :type status: str
471 :type status: str
466 :param comment_type: Comment type, one of: 'note', 'todo'
472 :param comment_type: Comment type, one of: 'note', 'todo'
467 :type comment_type: Optional(str), default: 'note'
473 :type comment_type: Optional(str), default: 'note'
468 :param userid: Comment on the pull request as this user
474 :param userid: Comment on the pull request as this user
469 :type userid: Optional(str or int)
475 :type userid: Optional(str or int)
470
476
471 Example output:
477 Example output:
472
478
473 .. code-block:: bash
479 .. code-block:: bash
474
480
475 id : <id_given_in_input>
481 id : <id_given_in_input>
476 result : {
482 result : {
477 "pull_request_id": "<Integer>",
483 "pull_request_id": "<Integer>",
478 "comment_id": "<Integer>",
484 "comment_id": "<Integer>",
479 "status": {"given": <given_status>,
485 "status": {"given": <given_status>,
480 "was_changed": <bool status_was_actually_changed> },
486 "was_changed": <bool status_was_actually_changed> },
481 },
487 },
482 error : null
488 error : null
483 """
489 """
484 pull_request = get_pull_request_or_error(pullrequestid)
490 pull_request = get_pull_request_or_error(pullrequestid)
485 if Optional.extract(repoid):
491 if Optional.extract(repoid):
486 repo = get_repo_or_error(repoid)
492 repo = get_repo_or_error(repoid)
487 else:
493 else:
488 repo = pull_request.target_repo
494 repo = pull_request.target_repo
489
495
490 if not isinstance(userid, Optional):
496 if not isinstance(userid, Optional):
491 if (has_superadmin_permission(apiuser) or
497 if (has_superadmin_permission(apiuser) or
492 HasRepoPermissionAnyApi('repository.admin')(
498 HasRepoPermissionAnyApi('repository.admin')(
493 user=apiuser, repo_name=repo.repo_name)):
499 user=apiuser, repo_name=repo.repo_name)):
494 apiuser = get_user_or_error(userid)
500 apiuser = get_user_or_error(userid)
495 else:
501 else:
496 raise JSONRPCError('userid is not the same as your user')
502 raise JSONRPCError('userid is not the same as your user')
497
503
498 if not PullRequestModel().check_user_read(
504 if not PullRequestModel().check_user_read(
499 pull_request, apiuser, api=True):
505 pull_request, apiuser, api=True):
500 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
506 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
501 message = Optional.extract(message)
507 message = Optional.extract(message)
502 status = Optional.extract(status)
508 status = Optional.extract(status)
503 commit_id = Optional.extract(commit_id)
509 commit_id = Optional.extract(commit_id)
504 comment_type = Optional.extract(comment_type)
510 comment_type = Optional.extract(comment_type)
505 resolves_comment_id = Optional.extract(resolves_comment_id)
511 resolves_comment_id = Optional.extract(resolves_comment_id)
506
512
507 if not message and not status:
513 if not message and not status:
508 raise JSONRPCError(
514 raise JSONRPCError(
509 'Both message and status parameters are missing. '
515 'Both message and status parameters are missing. '
510 'At least one is required.')
516 'At least one is required.')
511
517
512 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
518 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
513 status is not None):
519 status is not None):
514 raise JSONRPCError('Unknown comment status: `%s`' % status)
520 raise JSONRPCError('Unknown comment status: `%s`' % status)
515
521
516 if commit_id and commit_id not in pull_request.revisions:
522 if commit_id and commit_id not in pull_request.revisions:
517 raise JSONRPCError(
523 raise JSONRPCError(
518 'Invalid commit_id `%s` for this pull request.' % commit_id)
524 'Invalid commit_id `%s` for this pull request.' % commit_id)
519
525
520 allowed_to_change_status = PullRequestModel().check_user_change_status(
526 allowed_to_change_status = PullRequestModel().check_user_change_status(
521 pull_request, apiuser)
527 pull_request, apiuser)
522
528
523 # if commit_id is passed re-validated if user is allowed to change status
529 # if commit_id is passed re-validated if user is allowed to change status
524 # based on latest commit_id from the PR
530 # based on latest commit_id from the PR
525 if commit_id:
531 if commit_id:
526 commit_idx = pull_request.revisions.index(commit_id)
532 commit_idx = pull_request.revisions.index(commit_id)
527 if commit_idx != 0:
533 if commit_idx != 0:
528 allowed_to_change_status = False
534 allowed_to_change_status = False
529
535
530 if resolves_comment_id:
536 if resolves_comment_id:
531 comment = ChangesetComment.get(resolves_comment_id)
537 comment = ChangesetComment.get(resolves_comment_id)
532 if not comment:
538 if not comment:
533 raise JSONRPCError(
539 raise JSONRPCError(
534 'Invalid resolves_comment_id `%s` for this pull request.'
540 'Invalid resolves_comment_id `%s` for this pull request.'
535 % resolves_comment_id)
541 % resolves_comment_id)
536 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
542 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
537 raise JSONRPCError(
543 raise JSONRPCError(
538 'Comment `%s` is wrong type for setting status to resolved.'
544 'Comment `%s` is wrong type for setting status to resolved.'
539 % resolves_comment_id)
545 % resolves_comment_id)
540
546
541 text = message
547 text = message
542 status_label = ChangesetStatus.get_status_lbl(status)
548 status_label = ChangesetStatus.get_status_lbl(status)
543 if status and allowed_to_change_status:
549 if status and allowed_to_change_status:
544 st_message = ('Status change %(transition_icon)s %(status)s'
550 st_message = ('Status change %(transition_icon)s %(status)s'
545 % {'transition_icon': '>', 'status': status_label})
551 % {'transition_icon': '>', 'status': status_label})
546 text = message or st_message
552 text = message or st_message
547
553
548 rc_config = SettingsModel().get_all_settings()
554 rc_config = SettingsModel().get_all_settings()
549 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
555 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
550
556
551 status_change = status and allowed_to_change_status
557 status_change = status and allowed_to_change_status
552 comment = CommentsModel().create(
558 comment = CommentsModel().create(
553 text=text,
559 text=text,
554 repo=pull_request.target_repo.repo_id,
560 repo=pull_request.target_repo.repo_id,
555 user=apiuser.user_id,
561 user=apiuser.user_id,
556 pull_request=pull_request.pull_request_id,
562 pull_request=pull_request.pull_request_id,
557 f_path=None,
563 f_path=None,
558 line_no=None,
564 line_no=None,
559 status_change=(status_label if status_change else None),
565 status_change=(status_label if status_change else None),
560 status_change_type=(status if status_change else None),
566 status_change_type=(status if status_change else None),
561 closing_pr=False,
567 closing_pr=False,
562 renderer=renderer,
568 renderer=renderer,
563 comment_type=comment_type,
569 comment_type=comment_type,
564 resolves_comment_id=resolves_comment_id,
570 resolves_comment_id=resolves_comment_id,
565 auth_user=apiuser
571 auth_user=apiuser
566 )
572 )
567
573
568 if allowed_to_change_status and status:
574 if allowed_to_change_status and status:
569 old_calculated_status = pull_request.calculated_review_status()
575 old_calculated_status = pull_request.calculated_review_status()
570 ChangesetStatusModel().set_status(
576 ChangesetStatusModel().set_status(
571 pull_request.target_repo.repo_id,
577 pull_request.target_repo.repo_id,
572 status,
578 status,
573 apiuser.user_id,
579 apiuser.user_id,
574 comment,
580 comment,
575 pull_request=pull_request.pull_request_id
581 pull_request=pull_request.pull_request_id
576 )
582 )
577 Session().flush()
583 Session().flush()
578
584
579 Session().commit()
585 Session().commit()
580
586
581 PullRequestModel().trigger_pull_request_hook(
587 PullRequestModel().trigger_pull_request_hook(
582 pull_request, apiuser, 'comment',
588 pull_request, apiuser, 'comment',
583 data={'comment': comment})
589 data={'comment': comment})
584
590
585 if allowed_to_change_status and status:
591 if allowed_to_change_status and status:
586 # we now calculate the status of pull request, and based on that
592 # we now calculate the status of pull request, and based on that
587 # calculation we set the commits status
593 # calculation we set the commits status
588 calculated_status = pull_request.calculated_review_status()
594 calculated_status = pull_request.calculated_review_status()
589 if old_calculated_status != calculated_status:
595 if old_calculated_status != calculated_status:
590 PullRequestModel().trigger_pull_request_hook(
596 PullRequestModel().trigger_pull_request_hook(
591 pull_request, apiuser, 'review_status_change',
597 pull_request, apiuser, 'review_status_change',
592 data={'status': calculated_status})
598 data={'status': calculated_status})
593
599
594 data = {
600 data = {
595 'pull_request_id': pull_request.pull_request_id,
601 'pull_request_id': pull_request.pull_request_id,
596 'comment_id': comment.comment_id if comment else None,
602 'comment_id': comment.comment_id if comment else None,
597 'status': {'given': status, 'was_changed': status_change},
603 'status': {'given': status, 'was_changed': status_change},
598 }
604 }
599 return data
605 return data
600
606
601
607
602 @jsonrpc_method()
608 @jsonrpc_method()
603 def create_pull_request(
609 def create_pull_request(
604 request, apiuser, source_repo, target_repo, source_ref, target_ref,
610 request, apiuser, source_repo, target_repo, source_ref, target_ref,
605 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
611 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
606 reviewers=Optional(None)):
612 reviewers=Optional(None)):
607 """
613 """
608 Creates a new pull request.
614 Creates a new pull request.
609
615
610 Accepts refs in the following formats:
616 Accepts refs in the following formats:
611
617
612 * branch:<branch_name>:<sha>
618 * branch:<branch_name>:<sha>
613 * branch:<branch_name>
619 * branch:<branch_name>
614 * bookmark:<bookmark_name>:<sha> (Mercurial only)
620 * bookmark:<bookmark_name>:<sha> (Mercurial only)
615 * bookmark:<bookmark_name> (Mercurial only)
621 * bookmark:<bookmark_name> (Mercurial only)
616
622
617 :param apiuser: This is filled automatically from the |authtoken|.
623 :param apiuser: This is filled automatically from the |authtoken|.
618 :type apiuser: AuthUser
624 :type apiuser: AuthUser
619 :param source_repo: Set the source repository name.
625 :param source_repo: Set the source repository name.
620 :type source_repo: str
626 :type source_repo: str
621 :param target_repo: Set the target repository name.
627 :param target_repo: Set the target repository name.
622 :type target_repo: str
628 :type target_repo: str
623 :param source_ref: Set the source ref name.
629 :param source_ref: Set the source ref name.
624 :type source_ref: str
630 :type source_ref: str
625 :param target_ref: Set the target ref name.
631 :param target_ref: Set the target ref name.
626 :type target_ref: str
632 :type target_ref: str
627 :param title: Optionally Set the pull request title, it's generated otherwise
633 :param title: Optionally Set the pull request title, it's generated otherwise
628 :type title: str
634 :type title: str
629 :param description: Set the pull request description.
635 :param description: Set the pull request description.
630 :type description: Optional(str)
636 :type description: Optional(str)
631 :type description_renderer: Optional(str)
637 :type description_renderer: Optional(str)
632 :param description_renderer: Set pull request renderer for the description.
638 :param description_renderer: Set pull request renderer for the description.
633 It should be 'rst', 'markdown' or 'plain'. If not give default
639 It should be 'rst', 'markdown' or 'plain'. If not give default
634 system renderer will be used
640 system renderer will be used
635 :param reviewers: Set the new pull request reviewers list.
641 :param reviewers: Set the new pull request reviewers list.
636 Reviewer defined by review rules will be added automatically to the
642 Reviewer defined by review rules will be added automatically to the
637 defined list.
643 defined list.
638 :type reviewers: Optional(list)
644 :type reviewers: Optional(list)
639 Accepts username strings or objects of the format:
645 Accepts username strings or objects of the format:
640
646
641 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
647 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
642 """
648 """
643
649
644 source_db_repo = get_repo_or_error(source_repo)
650 source_db_repo = get_repo_or_error(source_repo)
645 target_db_repo = get_repo_or_error(target_repo)
651 target_db_repo = get_repo_or_error(target_repo)
646 if not has_superadmin_permission(apiuser):
652 if not has_superadmin_permission(apiuser):
647 _perms = ('repository.admin', 'repository.write', 'repository.read',)
653 _perms = ('repository.admin', 'repository.write', 'repository.read',)
648 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
654 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
649
655
650 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
656 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
651 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
657 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
652
658
653 source_scm = source_db_repo.scm_instance()
659 source_scm = source_db_repo.scm_instance()
654 target_scm = target_db_repo.scm_instance()
660 target_scm = target_db_repo.scm_instance()
655
661
656 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
662 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
657 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
663 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
658
664
659 ancestor = source_scm.get_common_ancestor(
665 ancestor = source_scm.get_common_ancestor(
660 source_commit.raw_id, target_commit.raw_id, target_scm)
666 source_commit.raw_id, target_commit.raw_id, target_scm)
661 if not ancestor:
667 if not ancestor:
662 raise JSONRPCError('no common ancestor found')
668 raise JSONRPCError('no common ancestor found')
663
669
664 # recalculate target ref based on ancestor
670 # recalculate target ref based on ancestor
665 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
671 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
666 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
672 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
667
673
668 commit_ranges = target_scm.compare(
674 commit_ranges = target_scm.compare(
669 target_commit.raw_id, source_commit.raw_id, source_scm,
675 target_commit.raw_id, source_commit.raw_id, source_scm,
670 merge=True, pre_load=[])
676 merge=True, pre_load=[])
671
677
672 if not commit_ranges:
678 if not commit_ranges:
673 raise JSONRPCError('no commits found')
679 raise JSONRPCError('no commits found')
674
680
675 reviewer_objects = Optional.extract(reviewers) or []
681 reviewer_objects = Optional.extract(reviewers) or []
676
682
677 # serialize and validate passed in given reviewers
683 # serialize and validate passed in given reviewers
678 if reviewer_objects:
684 if reviewer_objects:
679 schema = ReviewerListSchema()
685 schema = ReviewerListSchema()
680 try:
686 try:
681 reviewer_objects = schema.deserialize(reviewer_objects)
687 reviewer_objects = schema.deserialize(reviewer_objects)
682 except Invalid as err:
688 except Invalid as err:
683 raise JSONRPCValidationError(colander_exc=err)
689 raise JSONRPCValidationError(colander_exc=err)
684
690
685 # validate users
691 # validate users
686 for reviewer_object in reviewer_objects:
692 for reviewer_object in reviewer_objects:
687 user = get_user_or_error(reviewer_object['username'])
693 user = get_user_or_error(reviewer_object['username'])
688 reviewer_object['user_id'] = user.user_id
694 reviewer_object['user_id'] = user.user_id
689
695
690 get_default_reviewers_data, validate_default_reviewers = \
696 get_default_reviewers_data, validate_default_reviewers = \
691 PullRequestModel().get_reviewer_functions()
697 PullRequestModel().get_reviewer_functions()
692
698
693 # recalculate reviewers logic, to make sure we can validate this
699 # recalculate reviewers logic, to make sure we can validate this
694 reviewer_rules = get_default_reviewers_data(
700 reviewer_rules = get_default_reviewers_data(
695 apiuser.get_instance(), source_db_repo,
701 apiuser.get_instance(), source_db_repo,
696 source_commit, target_db_repo, target_commit)
702 source_commit, target_db_repo, target_commit)
697
703
698 # now MERGE our given with the calculated
704 # now MERGE our given with the calculated
699 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
705 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
700
706
701 try:
707 try:
702 reviewers = validate_default_reviewers(
708 reviewers = validate_default_reviewers(
703 reviewer_objects, reviewer_rules)
709 reviewer_objects, reviewer_rules)
704 except ValueError as e:
710 except ValueError as e:
705 raise JSONRPCError('Reviewers Validation: {}'.format(e))
711 raise JSONRPCError('Reviewers Validation: {}'.format(e))
706
712
707 title = Optional.extract(title)
713 title = Optional.extract(title)
708 if not title:
714 if not title:
709 title_source_ref = source_ref.split(':', 2)[1]
715 title_source_ref = source_ref.split(':', 2)[1]
710 title = PullRequestModel().generate_pullrequest_title(
716 title = PullRequestModel().generate_pullrequest_title(
711 source=source_repo,
717 source=source_repo,
712 source_ref=title_source_ref,
718 source_ref=title_source_ref,
713 target=target_repo
719 target=target_repo
714 )
720 )
715 # fetch renderer, if set fallback to plain in case of PR
721 # fetch renderer, if set fallback to plain in case of PR
716 rc_config = SettingsModel().get_all_settings()
722 rc_config = SettingsModel().get_all_settings()
717 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
723 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
718 description = Optional.extract(description)
724 description = Optional.extract(description)
719 description_renderer = Optional.extract(description_renderer) or default_system_renderer
725 description_renderer = Optional.extract(description_renderer) or default_system_renderer
720
726
721 pull_request = PullRequestModel().create(
727 pull_request = PullRequestModel().create(
722 created_by=apiuser.user_id,
728 created_by=apiuser.user_id,
723 source_repo=source_repo,
729 source_repo=source_repo,
724 source_ref=full_source_ref,
730 source_ref=full_source_ref,
725 target_repo=target_repo,
731 target_repo=target_repo,
726 target_ref=full_target_ref,
732 target_ref=full_target_ref,
727 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
733 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
728 reviewers=reviewers,
734 reviewers=reviewers,
729 title=title,
735 title=title,
730 description=description,
736 description=description,
731 description_renderer=description_renderer,
737 description_renderer=description_renderer,
732 reviewer_data=reviewer_rules,
738 reviewer_data=reviewer_rules,
733 auth_user=apiuser
739 auth_user=apiuser
734 )
740 )
735
741
736 Session().commit()
742 Session().commit()
737 data = {
743 data = {
738 'msg': 'Created new pull request `{}`'.format(title),
744 'msg': 'Created new pull request `{}`'.format(title),
739 'pull_request_id': pull_request.pull_request_id,
745 'pull_request_id': pull_request.pull_request_id,
740 }
746 }
741 return data
747 return data
742
748
743
749
744 @jsonrpc_method()
750 @jsonrpc_method()
745 def update_pull_request(
751 def update_pull_request(
746 request, apiuser, pullrequestid, repoid=Optional(None),
752 request, apiuser, pullrequestid, repoid=Optional(None),
747 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
753 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
748 reviewers=Optional(None), update_commits=Optional(None)):
754 reviewers=Optional(None), update_commits=Optional(None)):
749 """
755 """
750 Updates a pull request.
756 Updates a pull request.
751
757
752 :param apiuser: This is filled automatically from the |authtoken|.
758 :param apiuser: This is filled automatically from the |authtoken|.
753 :type apiuser: AuthUser
759 :type apiuser: AuthUser
754 :param repoid: Optional repository name or repository ID.
760 :param repoid: Optional repository name or repository ID.
755 :type repoid: str or int
761 :type repoid: str or int
756 :param pullrequestid: The pull request ID.
762 :param pullrequestid: The pull request ID.
757 :type pullrequestid: int
763 :type pullrequestid: int
758 :param title: Set the pull request title.
764 :param title: Set the pull request title.
759 :type title: str
765 :type title: str
760 :param description: Update pull request description.
766 :param description: Update pull request description.
761 :type description: Optional(str)
767 :type description: Optional(str)
762 :type description_renderer: Optional(str)
768 :type description_renderer: Optional(str)
763 :param description_renderer: Update pull request renderer for the description.
769 :param description_renderer: Update pull request renderer for the description.
764 It should be 'rst', 'markdown' or 'plain'
770 It should be 'rst', 'markdown' or 'plain'
765 :param reviewers: Update pull request reviewers list with new value.
771 :param reviewers: Update pull request reviewers list with new value.
766 :type reviewers: Optional(list)
772 :type reviewers: Optional(list)
767 Accepts username strings or objects of the format:
773 Accepts username strings or objects of the format:
768
774
769 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
775 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
770
776
771 :param update_commits: Trigger update of commits for this pull request
777 :param update_commits: Trigger update of commits for this pull request
772 :type: update_commits: Optional(bool)
778 :type: update_commits: Optional(bool)
773
779
774 Example output:
780 Example output:
775
781
776 .. code-block:: bash
782 .. code-block:: bash
777
783
778 id : <id_given_in_input>
784 id : <id_given_in_input>
779 result : {
785 result : {
780 "msg": "Updated pull request `63`",
786 "msg": "Updated pull request `63`",
781 "pull_request": <pull_request_object>,
787 "pull_request": <pull_request_object>,
782 "updated_reviewers": {
788 "updated_reviewers": {
783 "added": [
789 "added": [
784 "username"
790 "username"
785 ],
791 ],
786 "removed": []
792 "removed": []
787 },
793 },
788 "updated_commits": {
794 "updated_commits": {
789 "added": [
795 "added": [
790 "<sha1_hash>"
796 "<sha1_hash>"
791 ],
797 ],
792 "common": [
798 "common": [
793 "<sha1_hash>",
799 "<sha1_hash>",
794 "<sha1_hash>",
800 "<sha1_hash>",
795 ],
801 ],
796 "removed": []
802 "removed": []
797 }
803 }
798 }
804 }
799 error : null
805 error : null
800 """
806 """
801
807
802 pull_request = get_pull_request_or_error(pullrequestid)
808 pull_request = get_pull_request_or_error(pullrequestid)
803 if Optional.extract(repoid):
809 if Optional.extract(repoid):
804 repo = get_repo_or_error(repoid)
810 repo = get_repo_or_error(repoid)
805 else:
811 else:
806 repo = pull_request.target_repo
812 repo = pull_request.target_repo
807
813
808 if not PullRequestModel().check_user_update(
814 if not PullRequestModel().check_user_update(
809 pull_request, apiuser, api=True):
815 pull_request, apiuser, api=True):
810 raise JSONRPCError(
816 raise JSONRPCError(
811 'pull request `%s` update failed, no permission to update.' % (
817 'pull request `%s` update failed, no permission to update.' % (
812 pullrequestid,))
818 pullrequestid,))
813 if pull_request.is_closed():
819 if pull_request.is_closed():
814 raise JSONRPCError(
820 raise JSONRPCError(
815 'pull request `%s` update failed, pull request is closed' % (
821 'pull request `%s` update failed, pull request is closed' % (
816 pullrequestid,))
822 pullrequestid,))
817
823
818 reviewer_objects = Optional.extract(reviewers) or []
824 reviewer_objects = Optional.extract(reviewers) or []
819
825
820 if reviewer_objects:
826 if reviewer_objects:
821 schema = ReviewerListSchema()
827 schema = ReviewerListSchema()
822 try:
828 try:
823 reviewer_objects = schema.deserialize(reviewer_objects)
829 reviewer_objects = schema.deserialize(reviewer_objects)
824 except Invalid as err:
830 except Invalid as err:
825 raise JSONRPCValidationError(colander_exc=err)
831 raise JSONRPCValidationError(colander_exc=err)
826
832
827 # validate users
833 # validate users
828 for reviewer_object in reviewer_objects:
834 for reviewer_object in reviewer_objects:
829 user = get_user_or_error(reviewer_object['username'])
835 user = get_user_or_error(reviewer_object['username'])
830 reviewer_object['user_id'] = user.user_id
836 reviewer_object['user_id'] = user.user_id
831
837
832 get_default_reviewers_data, get_validated_reviewers = \
838 get_default_reviewers_data, get_validated_reviewers = \
833 PullRequestModel().get_reviewer_functions()
839 PullRequestModel().get_reviewer_functions()
834
840
835 # re-use stored rules
841 # re-use stored rules
836 reviewer_rules = pull_request.reviewer_data
842 reviewer_rules = pull_request.reviewer_data
837 try:
843 try:
838 reviewers = get_validated_reviewers(
844 reviewers = get_validated_reviewers(
839 reviewer_objects, reviewer_rules)
845 reviewer_objects, reviewer_rules)
840 except ValueError as e:
846 except ValueError as e:
841 raise JSONRPCError('Reviewers Validation: {}'.format(e))
847 raise JSONRPCError('Reviewers Validation: {}'.format(e))
842 else:
848 else:
843 reviewers = []
849 reviewers = []
844
850
845 title = Optional.extract(title)
851 title = Optional.extract(title)
846 description = Optional.extract(description)
852 description = Optional.extract(description)
847 description_renderer = Optional.extract(description_renderer)
853 description_renderer = Optional.extract(description_renderer)
848
854
849 if title or description:
855 if title or description:
850 PullRequestModel().edit(
856 PullRequestModel().edit(
851 pull_request,
857 pull_request,
852 title or pull_request.title,
858 title or pull_request.title,
853 description or pull_request.description,
859 description or pull_request.description,
854 description_renderer or pull_request.description_renderer,
860 description_renderer or pull_request.description_renderer,
855 apiuser)
861 apiuser)
856 Session().commit()
862 Session().commit()
857
863
858 commit_changes = {"added": [], "common": [], "removed": []}
864 commit_changes = {"added": [], "common": [], "removed": []}
859 if str2bool(Optional.extract(update_commits)):
865 if str2bool(Optional.extract(update_commits)):
860
866
861 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
867 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
862 raise JSONRPCError(
868 raise JSONRPCError(
863 'Operation forbidden because pull request is in state {}, '
869 'Operation forbidden because pull request is in state {}, '
864 'only state {} is allowed.'.format(
870 'only state {} is allowed.'.format(
865 pull_request.pull_request_state, PullRequest.STATE_CREATED))
871 pull_request.pull_request_state, PullRequest.STATE_CREATED))
866
872
867 with pull_request.set_state(PullRequest.STATE_UPDATING):
873 with pull_request.set_state(PullRequest.STATE_UPDATING):
868 if PullRequestModel().has_valid_update_type(pull_request):
874 if PullRequestModel().has_valid_update_type(pull_request):
869 update_response = PullRequestModel().update_commits(pull_request)
875 update_response = PullRequestModel().update_commits(pull_request)
870 commit_changes = update_response.changes or commit_changes
876 commit_changes = update_response.changes or commit_changes
871 Session().commit()
877 Session().commit()
872
878
873 reviewers_changes = {"added": [], "removed": []}
879 reviewers_changes = {"added": [], "removed": []}
874 if reviewers:
880 if reviewers:
875 old_calculated_status = pull_request.calculated_review_status()
881 old_calculated_status = pull_request.calculated_review_status()
876 added_reviewers, removed_reviewers = \
882 added_reviewers, removed_reviewers = \
877 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
883 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
878
884
879 reviewers_changes['added'] = sorted(
885 reviewers_changes['added'] = sorted(
880 [get_user_or_error(n).username for n in added_reviewers])
886 [get_user_or_error(n).username for n in added_reviewers])
881 reviewers_changes['removed'] = sorted(
887 reviewers_changes['removed'] = sorted(
882 [get_user_or_error(n).username for n in removed_reviewers])
888 [get_user_or_error(n).username for n in removed_reviewers])
883 Session().commit()
889 Session().commit()
884
890
885 # trigger status changed if change in reviewers changes the status
891 # trigger status changed if change in reviewers changes the status
886 calculated_status = pull_request.calculated_review_status()
892 calculated_status = pull_request.calculated_review_status()
887 if old_calculated_status != calculated_status:
893 if old_calculated_status != calculated_status:
888 PullRequestModel().trigger_pull_request_hook(
894 PullRequestModel().trigger_pull_request_hook(
889 pull_request, apiuser, 'review_status_change',
895 pull_request, apiuser, 'review_status_change',
890 data={'status': calculated_status})
896 data={'status': calculated_status})
891
897
892 data = {
898 data = {
893 'msg': 'Updated pull request `{}`'.format(
899 'msg': 'Updated pull request `{}`'.format(
894 pull_request.pull_request_id),
900 pull_request.pull_request_id),
895 'pull_request': pull_request.get_api_data(),
901 'pull_request': pull_request.get_api_data(),
896 'updated_commits': commit_changes,
902 'updated_commits': commit_changes,
897 'updated_reviewers': reviewers_changes
903 'updated_reviewers': reviewers_changes
898 }
904 }
899
905
900 return data
906 return data
901
907
902
908
903 @jsonrpc_method()
909 @jsonrpc_method()
904 def close_pull_request(
910 def close_pull_request(
905 request, apiuser, pullrequestid, repoid=Optional(None),
911 request, apiuser, pullrequestid, repoid=Optional(None),
906 userid=Optional(OAttr('apiuser')), message=Optional('')):
912 userid=Optional(OAttr('apiuser')), message=Optional('')):
907 """
913 """
908 Close the pull request specified by `pullrequestid`.
914 Close the pull request specified by `pullrequestid`.
909
915
910 :param apiuser: This is filled automatically from the |authtoken|.
916 :param apiuser: This is filled automatically from the |authtoken|.
911 :type apiuser: AuthUser
917 :type apiuser: AuthUser
912 :param repoid: Repository name or repository ID to which the pull
918 :param repoid: Repository name or repository ID to which the pull
913 request belongs.
919 request belongs.
914 :type repoid: str or int
920 :type repoid: str or int
915 :param pullrequestid: ID of the pull request to be closed.
921 :param pullrequestid: ID of the pull request to be closed.
916 :type pullrequestid: int
922 :type pullrequestid: int
917 :param userid: Close the pull request as this user.
923 :param userid: Close the pull request as this user.
918 :type userid: Optional(str or int)
924 :type userid: Optional(str or int)
919 :param message: Optional message to close the Pull Request with. If not
925 :param message: Optional message to close the Pull Request with. If not
920 specified it will be generated automatically.
926 specified it will be generated automatically.
921 :type message: Optional(str)
927 :type message: Optional(str)
922
928
923 Example output:
929 Example output:
924
930
925 .. code-block:: bash
931 .. code-block:: bash
926
932
927 "id": <id_given_in_input>,
933 "id": <id_given_in_input>,
928 "result": {
934 "result": {
929 "pull_request_id": "<int>",
935 "pull_request_id": "<int>",
930 "close_status": "<str:status_lbl>,
936 "close_status": "<str:status_lbl>,
931 "closed": "<bool>"
937 "closed": "<bool>"
932 },
938 },
933 "error": null
939 "error": null
934
940
935 """
941 """
936 _ = request.translate
942 _ = request.translate
937
943
938 pull_request = get_pull_request_or_error(pullrequestid)
944 pull_request = get_pull_request_or_error(pullrequestid)
939 if Optional.extract(repoid):
945 if Optional.extract(repoid):
940 repo = get_repo_or_error(repoid)
946 repo = get_repo_or_error(repoid)
941 else:
947 else:
942 repo = pull_request.target_repo
948 repo = pull_request.target_repo
943
949
944 if not isinstance(userid, Optional):
950 if not isinstance(userid, Optional):
945 if (has_superadmin_permission(apiuser) or
951 if (has_superadmin_permission(apiuser) or
946 HasRepoPermissionAnyApi('repository.admin')(
952 HasRepoPermissionAnyApi('repository.admin')(
947 user=apiuser, repo_name=repo.repo_name)):
953 user=apiuser, repo_name=repo.repo_name)):
948 apiuser = get_user_or_error(userid)
954 apiuser = get_user_or_error(userid)
949 else:
955 else:
950 raise JSONRPCError('userid is not the same as your user')
956 raise JSONRPCError('userid is not the same as your user')
951
957
952 if pull_request.is_closed():
958 if pull_request.is_closed():
953 raise JSONRPCError(
959 raise JSONRPCError(
954 'pull request `%s` is already closed' % (pullrequestid,))
960 'pull request `%s` is already closed' % (pullrequestid,))
955
961
956 # only owner or admin or person with write permissions
962 # only owner or admin or person with write permissions
957 allowed_to_close = PullRequestModel().check_user_update(
963 allowed_to_close = PullRequestModel().check_user_update(
958 pull_request, apiuser, api=True)
964 pull_request, apiuser, api=True)
959
965
960 if not allowed_to_close:
966 if not allowed_to_close:
961 raise JSONRPCError(
967 raise JSONRPCError(
962 'pull request `%s` close failed, no permission to close.' % (
968 'pull request `%s` close failed, no permission to close.' % (
963 pullrequestid,))
969 pullrequestid,))
964
970
965 # message we're using to close the PR, else it's automatically generated
971 # message we're using to close the PR, else it's automatically generated
966 message = Optional.extract(message)
972 message = Optional.extract(message)
967
973
968 # finally close the PR, with proper message comment
974 # finally close the PR, with proper message comment
969 comment, status = PullRequestModel().close_pull_request_with_comment(
975 comment, status = PullRequestModel().close_pull_request_with_comment(
970 pull_request, apiuser, repo, message=message, auth_user=apiuser)
976 pull_request, apiuser, repo, message=message, auth_user=apiuser)
971 status_lbl = ChangesetStatus.get_status_lbl(status)
977 status_lbl = ChangesetStatus.get_status_lbl(status)
972
978
973 Session().commit()
979 Session().commit()
974
980
975 data = {
981 data = {
976 'pull_request_id': pull_request.pull_request_id,
982 'pull_request_id': pull_request.pull_request_id,
977 'close_status': status_lbl,
983 'close_status': status_lbl,
978 'closed': True,
984 'closed': True,
979 }
985 }
980 return data
986 return data
@@ -1,1715 +1,1716 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid import compat
33 from pyramid import compat
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35
35
36 from rhodecode import events
36 from rhodecode import events
37 from rhodecode.translation import lazy_ugettext
37 from rhodecode.translation import lazy_ugettext
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 UpdateResponse = collections.namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
78
78
79 UPDATE_STATUS_MESSAGES = {
79 UPDATE_STATUS_MESSAGES = {
80 UpdateFailureReason.NONE: lazy_ugettext(
80 UpdateFailureReason.NONE: lazy_ugettext(
81 'Pull request update successful.'),
81 'Pull request update successful.'),
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
83 'Pull request update failed because of an unknown error.'),
83 'Pull request update failed because of an unknown error.'),
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
85 'No update needed because the source and target have not changed.'),
85 'No update needed because the source and target have not changed.'),
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
87 'Pull request cannot be updated because the reference type is '
87 'Pull request cannot be updated because the reference type is '
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
90 'This pull request cannot be updated because the target '
90 'This pull request cannot be updated because the target '
91 'reference is missing.'),
91 'reference is missing.'),
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
93 'This pull request cannot be updated because the source '
93 'This pull request cannot be updated because the source '
94 'reference is missing.'),
94 'reference is missing.'),
95 }
95 }
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
98
98
99 def __get_pull_request(self, pull_request):
99 def __get_pull_request(self, pull_request):
100 return self._get_instance((
100 return self._get_instance((
101 PullRequest, PullRequestVersion), pull_request)
101 PullRequest, PullRequestVersion), pull_request)
102
102
103 def _check_perms(self, perms, pull_request, user, api=False):
103 def _check_perms(self, perms, pull_request, user, api=False):
104 if not api:
104 if not api:
105 return h.HasRepoPermissionAny(*perms)(
105 return h.HasRepoPermissionAny(*perms)(
106 user=user, repo_name=pull_request.target_repo.repo_name)
106 user=user, repo_name=pull_request.target_repo.repo_name)
107 else:
107 else:
108 return h.HasRepoPermissionAnyApi(*perms)(
108 return h.HasRepoPermissionAnyApi(*perms)(
109 user=user, repo_name=pull_request.target_repo.repo_name)
109 user=user, repo_name=pull_request.target_repo.repo_name)
110
110
111 def check_user_read(self, pull_request, user, api=False):
111 def check_user_read(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_merge(self, pull_request, user, api=False):
115 def check_user_merge(self, pull_request, user, api=False):
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
117 return self._check_perms(_perms, pull_request, user, api)
117 return self._check_perms(_perms, pull_request, user, api)
118
118
119 def check_user_update(self, pull_request, user, api=False):
119 def check_user_update(self, pull_request, user, api=False):
120 owner = user.user_id == pull_request.user_id
120 owner = user.user_id == pull_request.user_id
121 return self.check_user_merge(pull_request, user, api) or owner
121 return self.check_user_merge(pull_request, user, api) or owner
122
122
123 def check_user_delete(self, pull_request, user):
123 def check_user_delete(self, pull_request, user):
124 owner = user.user_id == pull_request.user_id
124 owner = user.user_id == pull_request.user_id
125 _perms = ('repository.admin',)
125 _perms = ('repository.admin',)
126 return self._check_perms(_perms, pull_request, user) or owner
126 return self._check_perms(_perms, pull_request, user) or owner
127
127
128 def check_user_change_status(self, pull_request, user, api=False):
128 def check_user_change_status(self, pull_request, user, api=False):
129 reviewer = user.user_id in [x.user_id for x in
129 reviewer = user.user_id in [x.user_id for x in
130 pull_request.reviewers]
130 pull_request.reviewers]
131 return self.check_user_update(pull_request, user, api) or reviewer
131 return self.check_user_update(pull_request, user, api) or reviewer
132
132
133 def check_user_comment(self, pull_request, user):
133 def check_user_comment(self, pull_request, user):
134 owner = user.user_id == pull_request.user_id
134 owner = user.user_id == pull_request.user_id
135 return self.check_user_read(pull_request, user) or owner
135 return self.check_user_read(pull_request, user) or owner
136
136
137 def get(self, pull_request):
137 def get(self, pull_request):
138 return self.__get_pull_request(pull_request)
138 return self.__get_pull_request(pull_request)
139
139
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
141 opened_by=None, order_by=None,
141 opened_by=None, order_by=None,
142 order_dir='desc', only_created=True):
142 order_dir='desc', only_created=True):
143 repo = None
143 repo = None
144 if repo_name:
144 if repo_name:
145 repo = self._get_repo(repo_name)
145 repo = self._get_repo(repo_name)
146
146
147 q = PullRequest.query()
147 q = PullRequest.query()
148
148
149 # source or target
149 # source or target
150 if repo and source:
150 if repo and source:
151 q = q.filter(PullRequest.source_repo == repo)
151 q = q.filter(PullRequest.source_repo == repo)
152 elif repo:
152 elif repo:
153 q = q.filter(PullRequest.target_repo == repo)
153 q = q.filter(PullRequest.target_repo == repo)
154
154
155 # closed,opened
155 # closed,opened
156 if statuses:
156 if statuses:
157 q = q.filter(PullRequest.status.in_(statuses))
157 q = q.filter(PullRequest.status.in_(statuses))
158
158
159 # opened by filter
159 # opened by filter
160 if opened_by:
160 if opened_by:
161 q = q.filter(PullRequest.user_id.in_(opened_by))
161 q = q.filter(PullRequest.user_id.in_(opened_by))
162
162
163 # only get those that are in "created" state
163 # only get those that are in "created" state
164 if only_created:
164 if only_created:
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
166
166
167 if order_by:
167 if order_by:
168 order_map = {
168 order_map = {
169 'name_raw': PullRequest.pull_request_id,
169 'name_raw': PullRequest.pull_request_id,
170 'id': PullRequest.pull_request_id,
170 'title': PullRequest.title,
171 'title': PullRequest.title,
171 'updated_on_raw': PullRequest.updated_on,
172 'updated_on_raw': PullRequest.updated_on,
172 'target_repo': PullRequest.target_repo_id
173 'target_repo': PullRequest.target_repo_id
173 }
174 }
174 if order_dir == 'asc':
175 if order_dir == 'asc':
175 q = q.order_by(order_map[order_by].asc())
176 q = q.order_by(order_map[order_by].asc())
176 else:
177 else:
177 q = q.order_by(order_map[order_by].desc())
178 q = q.order_by(order_map[order_by].desc())
178
179
179 return q
180 return q
180
181
181 def count_all(self, repo_name, source=False, statuses=None,
182 def count_all(self, repo_name, source=False, statuses=None,
182 opened_by=None):
183 opened_by=None):
183 """
184 """
184 Count the number of pull requests for a specific repository.
185 Count the number of pull requests for a specific repository.
185
186
186 :param repo_name: target or source repo
187 :param repo_name: target or source repo
187 :param source: boolean flag to specify if repo_name refers to source
188 :param source: boolean flag to specify if repo_name refers to source
188 :param statuses: list of pull request statuses
189 :param statuses: list of pull request statuses
189 :param opened_by: author user of the pull request
190 :param opened_by: author user of the pull request
190 :returns: int number of pull requests
191 :returns: int number of pull requests
191 """
192 """
192 q = self._prepare_get_all_query(
193 q = self._prepare_get_all_query(
193 repo_name, source=source, statuses=statuses, opened_by=opened_by)
194 repo_name, source=source, statuses=statuses, opened_by=opened_by)
194
195
195 return q.count()
196 return q.count()
196
197
197 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
198 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
198 offset=0, length=None, order_by=None, order_dir='desc'):
199 offset=0, length=None, order_by=None, order_dir='desc'):
199 """
200 """
200 Get all pull requests for a specific repository.
201 Get all pull requests for a specific repository.
201
202
202 :param repo_name: target or source repo
203 :param repo_name: target or source repo
203 :param source: boolean flag to specify if repo_name refers to source
204 :param source: boolean flag to specify if repo_name refers to source
204 :param statuses: list of pull request statuses
205 :param statuses: list of pull request statuses
205 :param opened_by: author user of the pull request
206 :param opened_by: author user of the pull request
206 :param offset: pagination offset
207 :param offset: pagination offset
207 :param length: length of returned list
208 :param length: length of returned list
208 :param order_by: order of the returned list
209 :param order_by: order of the returned list
209 :param order_dir: 'asc' or 'desc' ordering direction
210 :param order_dir: 'asc' or 'desc' ordering direction
210 :returns: list of pull requests
211 :returns: list of pull requests
211 """
212 """
212 q = self._prepare_get_all_query(
213 q = self._prepare_get_all_query(
213 repo_name, source=source, statuses=statuses, opened_by=opened_by,
214 repo_name, source=source, statuses=statuses, opened_by=opened_by,
214 order_by=order_by, order_dir=order_dir)
215 order_by=order_by, order_dir=order_dir)
215
216
216 if length:
217 if length:
217 pull_requests = q.limit(length).offset(offset).all()
218 pull_requests = q.limit(length).offset(offset).all()
218 else:
219 else:
219 pull_requests = q.all()
220 pull_requests = q.all()
220
221
221 return pull_requests
222 return pull_requests
222
223
223 def count_awaiting_review(self, repo_name, source=False, statuses=None,
224 def count_awaiting_review(self, repo_name, source=False, statuses=None,
224 opened_by=None):
225 opened_by=None):
225 """
226 """
226 Count the number of pull requests for a specific repository that are
227 Count the number of pull requests for a specific repository that are
227 awaiting review.
228 awaiting review.
228
229
229 :param repo_name: target or source repo
230 :param repo_name: target or source repo
230 :param source: boolean flag to specify if repo_name refers to source
231 :param source: boolean flag to specify if repo_name refers to source
231 :param statuses: list of pull request statuses
232 :param statuses: list of pull request statuses
232 :param opened_by: author user of the pull request
233 :param opened_by: author user of the pull request
233 :returns: int number of pull requests
234 :returns: int number of pull requests
234 """
235 """
235 pull_requests = self.get_awaiting_review(
236 pull_requests = self.get_awaiting_review(
236 repo_name, source=source, statuses=statuses, opened_by=opened_by)
237 repo_name, source=source, statuses=statuses, opened_by=opened_by)
237
238
238 return len(pull_requests)
239 return len(pull_requests)
239
240
240 def get_awaiting_review(self, repo_name, source=False, statuses=None,
241 def get_awaiting_review(self, repo_name, source=False, statuses=None,
241 opened_by=None, offset=0, length=None,
242 opened_by=None, offset=0, length=None,
242 order_by=None, order_dir='desc'):
243 order_by=None, order_dir='desc'):
243 """
244 """
244 Get all pull requests for a specific repository that are awaiting
245 Get all pull requests for a specific repository that are awaiting
245 review.
246 review.
246
247
247 :param repo_name: target or source repo
248 :param repo_name: target or source repo
248 :param source: boolean flag to specify if repo_name refers to source
249 :param source: boolean flag to specify if repo_name refers to source
249 :param statuses: list of pull request statuses
250 :param statuses: list of pull request statuses
250 :param opened_by: author user of the pull request
251 :param opened_by: author user of the pull request
251 :param offset: pagination offset
252 :param offset: pagination offset
252 :param length: length of returned list
253 :param length: length of returned list
253 :param order_by: order of the returned list
254 :param order_by: order of the returned list
254 :param order_dir: 'asc' or 'desc' ordering direction
255 :param order_dir: 'asc' or 'desc' ordering direction
255 :returns: list of pull requests
256 :returns: list of pull requests
256 """
257 """
257 pull_requests = self.get_all(
258 pull_requests = self.get_all(
258 repo_name, source=source, statuses=statuses, opened_by=opened_by,
259 repo_name, source=source, statuses=statuses, opened_by=opened_by,
259 order_by=order_by, order_dir=order_dir)
260 order_by=order_by, order_dir=order_dir)
260
261
261 _filtered_pull_requests = []
262 _filtered_pull_requests = []
262 for pr in pull_requests:
263 for pr in pull_requests:
263 status = pr.calculated_review_status()
264 status = pr.calculated_review_status()
264 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
265 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
265 ChangesetStatus.STATUS_UNDER_REVIEW]:
266 ChangesetStatus.STATUS_UNDER_REVIEW]:
266 _filtered_pull_requests.append(pr)
267 _filtered_pull_requests.append(pr)
267 if length:
268 if length:
268 return _filtered_pull_requests[offset:offset+length]
269 return _filtered_pull_requests[offset:offset+length]
269 else:
270 else:
270 return _filtered_pull_requests
271 return _filtered_pull_requests
271
272
272 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
273 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
273 opened_by=None, user_id=None):
274 opened_by=None, user_id=None):
274 """
275 """
275 Count the number of pull requests for a specific repository that are
276 Count the number of pull requests for a specific repository that are
276 awaiting review from a specific user.
277 awaiting review from a specific user.
277
278
278 :param repo_name: target or source repo
279 :param repo_name: target or source repo
279 :param source: boolean flag to specify if repo_name refers to source
280 :param source: boolean flag to specify if repo_name refers to source
280 :param statuses: list of pull request statuses
281 :param statuses: list of pull request statuses
281 :param opened_by: author user of the pull request
282 :param opened_by: author user of the pull request
282 :param user_id: reviewer user of the pull request
283 :param user_id: reviewer user of the pull request
283 :returns: int number of pull requests
284 :returns: int number of pull requests
284 """
285 """
285 pull_requests = self.get_awaiting_my_review(
286 pull_requests = self.get_awaiting_my_review(
286 repo_name, source=source, statuses=statuses, opened_by=opened_by,
287 repo_name, source=source, statuses=statuses, opened_by=opened_by,
287 user_id=user_id)
288 user_id=user_id)
288
289
289 return len(pull_requests)
290 return len(pull_requests)
290
291
291 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
292 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
292 opened_by=None, user_id=None, offset=0,
293 opened_by=None, user_id=None, offset=0,
293 length=None, order_by=None, order_dir='desc'):
294 length=None, order_by=None, order_dir='desc'):
294 """
295 """
295 Get all pull requests for a specific repository that are awaiting
296 Get all pull requests for a specific repository that are awaiting
296 review from a specific user.
297 review from a specific user.
297
298
298 :param repo_name: target or source repo
299 :param repo_name: target or source repo
299 :param source: boolean flag to specify if repo_name refers to source
300 :param source: boolean flag to specify if repo_name refers to source
300 :param statuses: list of pull request statuses
301 :param statuses: list of pull request statuses
301 :param opened_by: author user of the pull request
302 :param opened_by: author user of the pull request
302 :param user_id: reviewer user of the pull request
303 :param user_id: reviewer user of the pull request
303 :param offset: pagination offset
304 :param offset: pagination offset
304 :param length: length of returned list
305 :param length: length of returned list
305 :param order_by: order of the returned list
306 :param order_by: order of the returned list
306 :param order_dir: 'asc' or 'desc' ordering direction
307 :param order_dir: 'asc' or 'desc' ordering direction
307 :returns: list of pull requests
308 :returns: list of pull requests
308 """
309 """
309 pull_requests = self.get_all(
310 pull_requests = self.get_all(
310 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 order_by=order_by, order_dir=order_dir)
312 order_by=order_by, order_dir=order_dir)
312
313
313 _my = PullRequestModel().get_not_reviewed(user_id)
314 _my = PullRequestModel().get_not_reviewed(user_id)
314 my_participation = []
315 my_participation = []
315 for pr in pull_requests:
316 for pr in pull_requests:
316 if pr in _my:
317 if pr in _my:
317 my_participation.append(pr)
318 my_participation.append(pr)
318 _filtered_pull_requests = my_participation
319 _filtered_pull_requests = my_participation
319 if length:
320 if length:
320 return _filtered_pull_requests[offset:offset+length]
321 return _filtered_pull_requests[offset:offset+length]
321 else:
322 else:
322 return _filtered_pull_requests
323 return _filtered_pull_requests
323
324
324 def get_not_reviewed(self, user_id):
325 def get_not_reviewed(self, user_id):
325 return [
326 return [
326 x.pull_request for x in PullRequestReviewers.query().filter(
327 x.pull_request for x in PullRequestReviewers.query().filter(
327 PullRequestReviewers.user_id == user_id).all()
328 PullRequestReviewers.user_id == user_id).all()
328 ]
329 ]
329
330
330 def _prepare_participating_query(self, user_id=None, statuses=None,
331 def _prepare_participating_query(self, user_id=None, statuses=None,
331 order_by=None, order_dir='desc'):
332 order_by=None, order_dir='desc'):
332 q = PullRequest.query()
333 q = PullRequest.query()
333 if user_id:
334 if user_id:
334 reviewers_subquery = Session().query(
335 reviewers_subquery = Session().query(
335 PullRequestReviewers.pull_request_id).filter(
336 PullRequestReviewers.pull_request_id).filter(
336 PullRequestReviewers.user_id == user_id).subquery()
337 PullRequestReviewers.user_id == user_id).subquery()
337 user_filter = or_(
338 user_filter = or_(
338 PullRequest.user_id == user_id,
339 PullRequest.user_id == user_id,
339 PullRequest.pull_request_id.in_(reviewers_subquery)
340 PullRequest.pull_request_id.in_(reviewers_subquery)
340 )
341 )
341 q = PullRequest.query().filter(user_filter)
342 q = PullRequest.query().filter(user_filter)
342
343
343 # closed,opened
344 # closed,opened
344 if statuses:
345 if statuses:
345 q = q.filter(PullRequest.status.in_(statuses))
346 q = q.filter(PullRequest.status.in_(statuses))
346
347
347 if order_by:
348 if order_by:
348 order_map = {
349 order_map = {
349 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
350 'title': PullRequest.title,
351 'title': PullRequest.title,
351 'updated_on_raw': PullRequest.updated_on,
352 'updated_on_raw': PullRequest.updated_on,
352 'target_repo': PullRequest.target_repo_id
353 'target_repo': PullRequest.target_repo_id
353 }
354 }
354 if order_dir == 'asc':
355 if order_dir == 'asc':
355 q = q.order_by(order_map[order_by].asc())
356 q = q.order_by(order_map[order_by].asc())
356 else:
357 else:
357 q = q.order_by(order_map[order_by].desc())
358 q = q.order_by(order_map[order_by].desc())
358
359
359 return q
360 return q
360
361
361 def count_im_participating_in(self, user_id=None, statuses=None):
362 def count_im_participating_in(self, user_id=None, statuses=None):
362 q = self._prepare_participating_query(user_id, statuses=statuses)
363 q = self._prepare_participating_query(user_id, statuses=statuses)
363 return q.count()
364 return q.count()
364
365
365 def get_im_participating_in(
366 def get_im_participating_in(
366 self, user_id=None, statuses=None, offset=0,
367 self, user_id=None, statuses=None, offset=0,
367 length=None, order_by=None, order_dir='desc'):
368 length=None, order_by=None, order_dir='desc'):
368 """
369 """
369 Get all Pull requests that i'm participating in, or i have opened
370 Get all Pull requests that i'm participating in, or i have opened
370 """
371 """
371
372
372 q = self._prepare_participating_query(
373 q = self._prepare_participating_query(
373 user_id, statuses=statuses, order_by=order_by,
374 user_id, statuses=statuses, order_by=order_by,
374 order_dir=order_dir)
375 order_dir=order_dir)
375
376
376 if length:
377 if length:
377 pull_requests = q.limit(length).offset(offset).all()
378 pull_requests = q.limit(length).offset(offset).all()
378 else:
379 else:
379 pull_requests = q.all()
380 pull_requests = q.all()
380
381
381 return pull_requests
382 return pull_requests
382
383
383 def get_versions(self, pull_request):
384 def get_versions(self, pull_request):
384 """
385 """
385 returns version of pull request sorted by ID descending
386 returns version of pull request sorted by ID descending
386 """
387 """
387 return PullRequestVersion.query()\
388 return PullRequestVersion.query()\
388 .filter(PullRequestVersion.pull_request == pull_request)\
389 .filter(PullRequestVersion.pull_request == pull_request)\
389 .order_by(PullRequestVersion.pull_request_version_id.asc())\
390 .order_by(PullRequestVersion.pull_request_version_id.asc())\
390 .all()
391 .all()
391
392
392 def get_pr_version(self, pull_request_id, version=None):
393 def get_pr_version(self, pull_request_id, version=None):
393 at_version = None
394 at_version = None
394
395
395 if version and version == 'latest':
396 if version and version == 'latest':
396 pull_request_ver = PullRequest.get(pull_request_id)
397 pull_request_ver = PullRequest.get(pull_request_id)
397 pull_request_obj = pull_request_ver
398 pull_request_obj = pull_request_ver
398 _org_pull_request_obj = pull_request_obj
399 _org_pull_request_obj = pull_request_obj
399 at_version = 'latest'
400 at_version = 'latest'
400 elif version:
401 elif version:
401 pull_request_ver = PullRequestVersion.get_or_404(version)
402 pull_request_ver = PullRequestVersion.get_or_404(version)
402 pull_request_obj = pull_request_ver
403 pull_request_obj = pull_request_ver
403 _org_pull_request_obj = pull_request_ver.pull_request
404 _org_pull_request_obj = pull_request_ver.pull_request
404 at_version = pull_request_ver.pull_request_version_id
405 at_version = pull_request_ver.pull_request_version_id
405 else:
406 else:
406 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
407 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
407 pull_request_id)
408 pull_request_id)
408
409
409 pull_request_display_obj = PullRequest.get_pr_display_object(
410 pull_request_display_obj = PullRequest.get_pr_display_object(
410 pull_request_obj, _org_pull_request_obj)
411 pull_request_obj, _org_pull_request_obj)
411
412
412 return _org_pull_request_obj, pull_request_obj, \
413 return _org_pull_request_obj, pull_request_obj, \
413 pull_request_display_obj, at_version
414 pull_request_display_obj, at_version
414
415
415 def create(self, created_by, source_repo, source_ref, target_repo,
416 def create(self, created_by, source_repo, source_ref, target_repo,
416 target_ref, revisions, reviewers, title, description=None,
417 target_ref, revisions, reviewers, title, description=None,
417 description_renderer=None,
418 description_renderer=None,
418 reviewer_data=None, translator=None, auth_user=None):
419 reviewer_data=None, translator=None, auth_user=None):
419 translator = translator or get_current_request().translate
420 translator = translator or get_current_request().translate
420
421
421 created_by_user = self._get_user(created_by)
422 created_by_user = self._get_user(created_by)
422 auth_user = auth_user or created_by_user.AuthUser()
423 auth_user = auth_user or created_by_user.AuthUser()
423 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
424 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
425
426
426 pull_request = PullRequest()
427 pull_request = PullRequest()
427 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
428 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
429 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
430 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
431 pull_request.revisions = revisions
432 pull_request.revisions = revisions
432 pull_request.title = title
433 pull_request.title = title
433 pull_request.description = description
434 pull_request.description = description
434 pull_request.description_renderer = description_renderer
435 pull_request.description_renderer = description_renderer
435 pull_request.author = created_by_user
436 pull_request.author = created_by_user
436 pull_request.reviewer_data = reviewer_data
437 pull_request.reviewer_data = reviewer_data
437 pull_request.pull_request_state = pull_request.STATE_CREATING
438 pull_request.pull_request_state = pull_request.STATE_CREATING
438 Session().add(pull_request)
439 Session().add(pull_request)
439 Session().flush()
440 Session().flush()
440
441
441 reviewer_ids = set()
442 reviewer_ids = set()
442 # members / reviewers
443 # members / reviewers
443 for reviewer_object in reviewers:
444 for reviewer_object in reviewers:
444 user_id, reasons, mandatory, rules = reviewer_object
445 user_id, reasons, mandatory, rules = reviewer_object
445 user = self._get_user(user_id)
446 user = self._get_user(user_id)
446
447
447 # skip duplicates
448 # skip duplicates
448 if user.user_id in reviewer_ids:
449 if user.user_id in reviewer_ids:
449 continue
450 continue
450
451
451 reviewer_ids.add(user.user_id)
452 reviewer_ids.add(user.user_id)
452
453
453 reviewer = PullRequestReviewers()
454 reviewer = PullRequestReviewers()
454 reviewer.user = user
455 reviewer.user = user
455 reviewer.pull_request = pull_request
456 reviewer.pull_request = pull_request
456 reviewer.reasons = reasons
457 reviewer.reasons = reasons
457 reviewer.mandatory = mandatory
458 reviewer.mandatory = mandatory
458
459
459 # NOTE(marcink): pick only first rule for now
460 # NOTE(marcink): pick only first rule for now
460 rule_id = list(rules)[0] if rules else None
461 rule_id = list(rules)[0] if rules else None
461 rule = RepoReviewRule.get(rule_id) if rule_id else None
462 rule = RepoReviewRule.get(rule_id) if rule_id else None
462 if rule:
463 if rule:
463 review_group = rule.user_group_vote_rule(user_id)
464 review_group = rule.user_group_vote_rule(user_id)
464 # we check if this particular reviewer is member of a voting group
465 # we check if this particular reviewer is member of a voting group
465 if review_group:
466 if review_group:
466 # NOTE(marcink):
467 # NOTE(marcink):
467 # can be that user is member of more but we pick the first same,
468 # can be that user is member of more but we pick the first same,
468 # same as default reviewers algo
469 # same as default reviewers algo
469 review_group = review_group[0]
470 review_group = review_group[0]
470
471
471 rule_data = {
472 rule_data = {
472 'rule_name':
473 'rule_name':
473 rule.review_rule_name,
474 rule.review_rule_name,
474 'rule_user_group_entry_id':
475 'rule_user_group_entry_id':
475 review_group.repo_review_rule_users_group_id,
476 review_group.repo_review_rule_users_group_id,
476 'rule_user_group_name':
477 'rule_user_group_name':
477 review_group.users_group.users_group_name,
478 review_group.users_group.users_group_name,
478 'rule_user_group_members':
479 'rule_user_group_members':
479 [x.user.username for x in review_group.users_group.members],
480 [x.user.username for x in review_group.users_group.members],
480 'rule_user_group_members_id':
481 'rule_user_group_members_id':
481 [x.user.user_id for x in review_group.users_group.members],
482 [x.user.user_id for x in review_group.users_group.members],
482 }
483 }
483 # e.g {'vote_rule': -1, 'mandatory': True}
484 # e.g {'vote_rule': -1, 'mandatory': True}
484 rule_data.update(review_group.rule_data())
485 rule_data.update(review_group.rule_data())
485
486
486 reviewer.rule_data = rule_data
487 reviewer.rule_data = rule_data
487
488
488 Session().add(reviewer)
489 Session().add(reviewer)
489 Session().flush()
490 Session().flush()
490
491
491 # Set approval status to "Under Review" for all commits which are
492 # Set approval status to "Under Review" for all commits which are
492 # part of this pull request.
493 # part of this pull request.
493 ChangesetStatusModel().set_status(
494 ChangesetStatusModel().set_status(
494 repo=target_repo,
495 repo=target_repo,
495 status=ChangesetStatus.STATUS_UNDER_REVIEW,
496 status=ChangesetStatus.STATUS_UNDER_REVIEW,
496 user=created_by_user,
497 user=created_by_user,
497 pull_request=pull_request
498 pull_request=pull_request
498 )
499 )
499 # we commit early at this point. This has to do with a fact
500 # we commit early at this point. This has to do with a fact
500 # that before queries do some row-locking. And because of that
501 # that before queries do some row-locking. And because of that
501 # we need to commit and finish transaction before below validate call
502 # we need to commit and finish transaction before below validate call
502 # that for large repos could be long resulting in long row locks
503 # that for large repos could be long resulting in long row locks
503 Session().commit()
504 Session().commit()
504
505
505 # prepare workspace, and run initial merge simulation. Set state during that
506 # prepare workspace, and run initial merge simulation. Set state during that
506 # operation
507 # operation
507 pull_request = PullRequest.get(pull_request.pull_request_id)
508 pull_request = PullRequest.get(pull_request.pull_request_id)
508
509
509 # set as merging, for simulation, and if finished to created so we mark
510 # set as merging, for simulation, and if finished to created so we mark
510 # simulation is working fine
511 # simulation is working fine
511 with pull_request.set_state(PullRequest.STATE_MERGING,
512 with pull_request.set_state(PullRequest.STATE_MERGING,
512 final_state=PullRequest.STATE_CREATED):
513 final_state=PullRequest.STATE_CREATED):
513 MergeCheck.validate(
514 MergeCheck.validate(
514 pull_request, auth_user=auth_user, translator=translator)
515 pull_request, auth_user=auth_user, translator=translator)
515
516
516 self.notify_reviewers(pull_request, reviewer_ids)
517 self.notify_reviewers(pull_request, reviewer_ids)
517 self.trigger_pull_request_hook(
518 self.trigger_pull_request_hook(
518 pull_request, created_by_user, 'create')
519 pull_request, created_by_user, 'create')
519
520
520 creation_data = pull_request.get_api_data(with_merge_state=False)
521 creation_data = pull_request.get_api_data(with_merge_state=False)
521 self._log_audit_action(
522 self._log_audit_action(
522 'repo.pull_request.create', {'data': creation_data},
523 'repo.pull_request.create', {'data': creation_data},
523 auth_user, pull_request)
524 auth_user, pull_request)
524
525
525 return pull_request
526 return pull_request
526
527
527 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
528 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
528 pull_request = self.__get_pull_request(pull_request)
529 pull_request = self.__get_pull_request(pull_request)
529 target_scm = pull_request.target_repo.scm_instance()
530 target_scm = pull_request.target_repo.scm_instance()
530 if action == 'create':
531 if action == 'create':
531 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
532 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
532 elif action == 'merge':
533 elif action == 'merge':
533 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
534 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
534 elif action == 'close':
535 elif action == 'close':
535 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
536 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
536 elif action == 'review_status_change':
537 elif action == 'review_status_change':
537 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
538 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
538 elif action == 'update':
539 elif action == 'update':
539 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
540 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
540 elif action == 'comment':
541 elif action == 'comment':
541 # dummy hook ! for comment. We want this function to handle all cases
542 # dummy hook ! for comment. We want this function to handle all cases
542 def trigger_hook(*args, **kwargs):
543 def trigger_hook(*args, **kwargs):
543 pass
544 pass
544 comment = data['comment']
545 comment = data['comment']
545 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
546 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
546 else:
547 else:
547 return
548 return
548
549
549 trigger_hook(
550 trigger_hook(
550 username=user.username,
551 username=user.username,
551 repo_name=pull_request.target_repo.repo_name,
552 repo_name=pull_request.target_repo.repo_name,
552 repo_alias=target_scm.alias,
553 repo_alias=target_scm.alias,
553 pull_request=pull_request,
554 pull_request=pull_request,
554 data=data)
555 data=data)
555
556
556 def _get_commit_ids(self, pull_request):
557 def _get_commit_ids(self, pull_request):
557 """
558 """
558 Return the commit ids of the merged pull request.
559 Return the commit ids of the merged pull request.
559
560
560 This method is not dealing correctly yet with the lack of autoupdates
561 This method is not dealing correctly yet with the lack of autoupdates
561 nor with the implicit target updates.
562 nor with the implicit target updates.
562 For example: if a commit in the source repo is already in the target it
563 For example: if a commit in the source repo is already in the target it
563 will be reported anyways.
564 will be reported anyways.
564 """
565 """
565 merge_rev = pull_request.merge_rev
566 merge_rev = pull_request.merge_rev
566 if merge_rev is None:
567 if merge_rev is None:
567 raise ValueError('This pull request was not merged yet')
568 raise ValueError('This pull request was not merged yet')
568
569
569 commit_ids = list(pull_request.revisions)
570 commit_ids = list(pull_request.revisions)
570 if merge_rev not in commit_ids:
571 if merge_rev not in commit_ids:
571 commit_ids.append(merge_rev)
572 commit_ids.append(merge_rev)
572
573
573 return commit_ids
574 return commit_ids
574
575
575 def merge_repo(self, pull_request, user, extras):
576 def merge_repo(self, pull_request, user, extras):
576 log.debug("Merging pull request %s", pull_request.pull_request_id)
577 log.debug("Merging pull request %s", pull_request.pull_request_id)
577 extras['user_agent'] = 'internal-merge'
578 extras['user_agent'] = 'internal-merge'
578 merge_state = self._merge_pull_request(pull_request, user, extras)
579 merge_state = self._merge_pull_request(pull_request, user, extras)
579 if merge_state.executed:
580 if merge_state.executed:
580 log.debug("Merge was successful, updating the pull request comments.")
581 log.debug("Merge was successful, updating the pull request comments.")
581 self._comment_and_close_pr(pull_request, user, merge_state)
582 self._comment_and_close_pr(pull_request, user, merge_state)
582
583
583 self._log_audit_action(
584 self._log_audit_action(
584 'repo.pull_request.merge',
585 'repo.pull_request.merge',
585 {'merge_state': merge_state.__dict__},
586 {'merge_state': merge_state.__dict__},
586 user, pull_request)
587 user, pull_request)
587
588
588 else:
589 else:
589 log.warn("Merge failed, not updating the pull request.")
590 log.warn("Merge failed, not updating the pull request.")
590 return merge_state
591 return merge_state
591
592
592 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
593 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
593 target_vcs = pull_request.target_repo.scm_instance()
594 target_vcs = pull_request.target_repo.scm_instance()
594 source_vcs = pull_request.source_repo.scm_instance()
595 source_vcs = pull_request.source_repo.scm_instance()
595
596
596 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
597 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
597 pr_id=pull_request.pull_request_id,
598 pr_id=pull_request.pull_request_id,
598 pr_title=pull_request.title,
599 pr_title=pull_request.title,
599 source_repo=source_vcs.name,
600 source_repo=source_vcs.name,
600 source_ref_name=pull_request.source_ref_parts.name,
601 source_ref_name=pull_request.source_ref_parts.name,
601 target_repo=target_vcs.name,
602 target_repo=target_vcs.name,
602 target_ref_name=pull_request.target_ref_parts.name,
603 target_ref_name=pull_request.target_ref_parts.name,
603 )
604 )
604
605
605 workspace_id = self._workspace_id(pull_request)
606 workspace_id = self._workspace_id(pull_request)
606 repo_id = pull_request.target_repo.repo_id
607 repo_id = pull_request.target_repo.repo_id
607 use_rebase = self._use_rebase_for_merging(pull_request)
608 use_rebase = self._use_rebase_for_merging(pull_request)
608 close_branch = self._close_branch_before_merging(pull_request)
609 close_branch = self._close_branch_before_merging(pull_request)
609
610
610 target_ref = self._refresh_reference(
611 target_ref = self._refresh_reference(
611 pull_request.target_ref_parts, target_vcs)
612 pull_request.target_ref_parts, target_vcs)
612
613
613 callback_daemon, extras = prepare_callback_daemon(
614 callback_daemon, extras = prepare_callback_daemon(
614 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
615 host=vcs_settings.HOOKS_HOST,
616 host=vcs_settings.HOOKS_HOST,
616 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
617 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
617
618
618 with callback_daemon:
619 with callback_daemon:
619 # TODO: johbo: Implement a clean way to run a config_override
620 # TODO: johbo: Implement a clean way to run a config_override
620 # for a single call.
621 # for a single call.
621 target_vcs.config.set(
622 target_vcs.config.set(
622 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
623 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
623
624
624 user_name = user.short_contact
625 user_name = user.short_contact
625 merge_state = target_vcs.merge(
626 merge_state = target_vcs.merge(
626 repo_id, workspace_id, target_ref, source_vcs,
627 repo_id, workspace_id, target_ref, source_vcs,
627 pull_request.source_ref_parts,
628 pull_request.source_ref_parts,
628 user_name=user_name, user_email=user.email,
629 user_name=user_name, user_email=user.email,
629 message=message, use_rebase=use_rebase,
630 message=message, use_rebase=use_rebase,
630 close_branch=close_branch)
631 close_branch=close_branch)
631 return merge_state
632 return merge_state
632
633
633 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
634 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
634 pull_request.merge_rev = merge_state.merge_ref.commit_id
635 pull_request.merge_rev = merge_state.merge_ref.commit_id
635 pull_request.updated_on = datetime.datetime.now()
636 pull_request.updated_on = datetime.datetime.now()
636 close_msg = close_msg or 'Pull request merged and closed'
637 close_msg = close_msg or 'Pull request merged and closed'
637
638
638 CommentsModel().create(
639 CommentsModel().create(
639 text=safe_unicode(close_msg),
640 text=safe_unicode(close_msg),
640 repo=pull_request.target_repo.repo_id,
641 repo=pull_request.target_repo.repo_id,
641 user=user.user_id,
642 user=user.user_id,
642 pull_request=pull_request.pull_request_id,
643 pull_request=pull_request.pull_request_id,
643 f_path=None,
644 f_path=None,
644 line_no=None,
645 line_no=None,
645 closing_pr=True
646 closing_pr=True
646 )
647 )
647
648
648 Session().add(pull_request)
649 Session().add(pull_request)
649 Session().flush()
650 Session().flush()
650 # TODO: paris: replace invalidation with less radical solution
651 # TODO: paris: replace invalidation with less radical solution
651 ScmModel().mark_for_invalidation(
652 ScmModel().mark_for_invalidation(
652 pull_request.target_repo.repo_name)
653 pull_request.target_repo.repo_name)
653 self.trigger_pull_request_hook(pull_request, user, 'merge')
654 self.trigger_pull_request_hook(pull_request, user, 'merge')
654
655
655 def has_valid_update_type(self, pull_request):
656 def has_valid_update_type(self, pull_request):
656 source_ref_type = pull_request.source_ref_parts.type
657 source_ref_type = pull_request.source_ref_parts.type
657 return source_ref_type in self.REF_TYPES
658 return source_ref_type in self.REF_TYPES
658
659
659 def update_commits(self, pull_request):
660 def update_commits(self, pull_request):
660 """
661 """
661 Get the updated list of commits for the pull request
662 Get the updated list of commits for the pull request
662 and return the new pull request version and the list
663 and return the new pull request version and the list
663 of commits processed by this update action
664 of commits processed by this update action
664 """
665 """
665 pull_request = self.__get_pull_request(pull_request)
666 pull_request = self.__get_pull_request(pull_request)
666 source_ref_type = pull_request.source_ref_parts.type
667 source_ref_type = pull_request.source_ref_parts.type
667 source_ref_name = pull_request.source_ref_parts.name
668 source_ref_name = pull_request.source_ref_parts.name
668 source_ref_id = pull_request.source_ref_parts.commit_id
669 source_ref_id = pull_request.source_ref_parts.commit_id
669
670
670 target_ref_type = pull_request.target_ref_parts.type
671 target_ref_type = pull_request.target_ref_parts.type
671 target_ref_name = pull_request.target_ref_parts.name
672 target_ref_name = pull_request.target_ref_parts.name
672 target_ref_id = pull_request.target_ref_parts.commit_id
673 target_ref_id = pull_request.target_ref_parts.commit_id
673
674
674 if not self.has_valid_update_type(pull_request):
675 if not self.has_valid_update_type(pull_request):
675 log.debug("Skipping update of pull request %s due to ref type: %s",
676 log.debug("Skipping update of pull request %s due to ref type: %s",
676 pull_request, source_ref_type)
677 pull_request, source_ref_type)
677 return UpdateResponse(
678 return UpdateResponse(
678 executed=False,
679 executed=False,
679 reason=UpdateFailureReason.WRONG_REF_TYPE,
680 reason=UpdateFailureReason.WRONG_REF_TYPE,
680 old=pull_request, new=None, changes=None,
681 old=pull_request, new=None, changes=None,
681 source_changed=False, target_changed=False)
682 source_changed=False, target_changed=False)
682
683
683 # source repo
684 # source repo
684 source_repo = pull_request.source_repo.scm_instance()
685 source_repo = pull_request.source_repo.scm_instance()
685 try:
686 try:
686 source_commit = source_repo.get_commit(commit_id=source_ref_name)
687 source_commit = source_repo.get_commit(commit_id=source_ref_name)
687 except CommitDoesNotExistError:
688 except CommitDoesNotExistError:
688 return UpdateResponse(
689 return UpdateResponse(
689 executed=False,
690 executed=False,
690 reason=UpdateFailureReason.MISSING_SOURCE_REF,
691 reason=UpdateFailureReason.MISSING_SOURCE_REF,
691 old=pull_request, new=None, changes=None,
692 old=pull_request, new=None, changes=None,
692 source_changed=False, target_changed=False)
693 source_changed=False, target_changed=False)
693
694
694 source_changed = source_ref_id != source_commit.raw_id
695 source_changed = source_ref_id != source_commit.raw_id
695
696
696 # target repo
697 # target repo
697 target_repo = pull_request.target_repo.scm_instance()
698 target_repo = pull_request.target_repo.scm_instance()
698 try:
699 try:
699 target_commit = target_repo.get_commit(commit_id=target_ref_name)
700 target_commit = target_repo.get_commit(commit_id=target_ref_name)
700 except CommitDoesNotExistError:
701 except CommitDoesNotExistError:
701 return UpdateResponse(
702 return UpdateResponse(
702 executed=False,
703 executed=False,
703 reason=UpdateFailureReason.MISSING_TARGET_REF,
704 reason=UpdateFailureReason.MISSING_TARGET_REF,
704 old=pull_request, new=None, changes=None,
705 old=pull_request, new=None, changes=None,
705 source_changed=False, target_changed=False)
706 source_changed=False, target_changed=False)
706 target_changed = target_ref_id != target_commit.raw_id
707 target_changed = target_ref_id != target_commit.raw_id
707
708
708 if not (source_changed or target_changed):
709 if not (source_changed or target_changed):
709 log.debug("Nothing changed in pull request %s", pull_request)
710 log.debug("Nothing changed in pull request %s", pull_request)
710 return UpdateResponse(
711 return UpdateResponse(
711 executed=False,
712 executed=False,
712 reason=UpdateFailureReason.NO_CHANGE,
713 reason=UpdateFailureReason.NO_CHANGE,
713 old=pull_request, new=None, changes=None,
714 old=pull_request, new=None, changes=None,
714 source_changed=target_changed, target_changed=source_changed)
715 source_changed=target_changed, target_changed=source_changed)
715
716
716 change_in_found = 'target repo' if target_changed else 'source repo'
717 change_in_found = 'target repo' if target_changed else 'source repo'
717 log.debug('Updating pull request because of change in %s detected',
718 log.debug('Updating pull request because of change in %s detected',
718 change_in_found)
719 change_in_found)
719
720
720 # Finally there is a need for an update, in case of source change
721 # Finally there is a need for an update, in case of source change
721 # we create a new version, else just an update
722 # we create a new version, else just an update
722 if source_changed:
723 if source_changed:
723 pull_request_version = self._create_version_from_snapshot(pull_request)
724 pull_request_version = self._create_version_from_snapshot(pull_request)
724 self._link_comments_to_version(pull_request_version)
725 self._link_comments_to_version(pull_request_version)
725 else:
726 else:
726 try:
727 try:
727 ver = pull_request.versions[-1]
728 ver = pull_request.versions[-1]
728 except IndexError:
729 except IndexError:
729 ver = None
730 ver = None
730
731
731 pull_request.pull_request_version_id = \
732 pull_request.pull_request_version_id = \
732 ver.pull_request_version_id if ver else None
733 ver.pull_request_version_id if ver else None
733 pull_request_version = pull_request
734 pull_request_version = pull_request
734
735
735 try:
736 try:
736 if target_ref_type in self.REF_TYPES:
737 if target_ref_type in self.REF_TYPES:
737 target_commit = target_repo.get_commit(target_ref_name)
738 target_commit = target_repo.get_commit(target_ref_name)
738 else:
739 else:
739 target_commit = target_repo.get_commit(target_ref_id)
740 target_commit = target_repo.get_commit(target_ref_id)
740 except CommitDoesNotExistError:
741 except CommitDoesNotExistError:
741 return UpdateResponse(
742 return UpdateResponse(
742 executed=False,
743 executed=False,
743 reason=UpdateFailureReason.MISSING_TARGET_REF,
744 reason=UpdateFailureReason.MISSING_TARGET_REF,
744 old=pull_request, new=None, changes=None,
745 old=pull_request, new=None, changes=None,
745 source_changed=source_changed, target_changed=target_changed)
746 source_changed=source_changed, target_changed=target_changed)
746
747
747 # re-compute commit ids
748 # re-compute commit ids
748 old_commit_ids = pull_request.revisions
749 old_commit_ids = pull_request.revisions
749 pre_load = ["author", "branch", "date", "message"]
750 pre_load = ["author", "branch", "date", "message"]
750 commit_ranges = target_repo.compare(
751 commit_ranges = target_repo.compare(
751 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
752 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
752 pre_load=pre_load)
753 pre_load=pre_load)
753
754
754 ancestor = target_repo.get_common_ancestor(
755 ancestor = target_repo.get_common_ancestor(
755 target_commit.raw_id, source_commit.raw_id, source_repo)
756 target_commit.raw_id, source_commit.raw_id, source_repo)
756
757
757 pull_request.source_ref = '%s:%s:%s' % (
758 pull_request.source_ref = '%s:%s:%s' % (
758 source_ref_type, source_ref_name, source_commit.raw_id)
759 source_ref_type, source_ref_name, source_commit.raw_id)
759 pull_request.target_ref = '%s:%s:%s' % (
760 pull_request.target_ref = '%s:%s:%s' % (
760 target_ref_type, target_ref_name, ancestor)
761 target_ref_type, target_ref_name, ancestor)
761
762
762 pull_request.revisions = [
763 pull_request.revisions = [
763 commit.raw_id for commit in reversed(commit_ranges)]
764 commit.raw_id for commit in reversed(commit_ranges)]
764 pull_request.updated_on = datetime.datetime.now()
765 pull_request.updated_on = datetime.datetime.now()
765 Session().add(pull_request)
766 Session().add(pull_request)
766 new_commit_ids = pull_request.revisions
767 new_commit_ids = pull_request.revisions
767
768
768 old_diff_data, new_diff_data = self._generate_update_diffs(
769 old_diff_data, new_diff_data = self._generate_update_diffs(
769 pull_request, pull_request_version)
770 pull_request, pull_request_version)
770
771
771 # calculate commit and file changes
772 # calculate commit and file changes
772 changes = self._calculate_commit_id_changes(
773 changes = self._calculate_commit_id_changes(
773 old_commit_ids, new_commit_ids)
774 old_commit_ids, new_commit_ids)
774 file_changes = self._calculate_file_changes(
775 file_changes = self._calculate_file_changes(
775 old_diff_data, new_diff_data)
776 old_diff_data, new_diff_data)
776
777
777 # set comments as outdated if DIFFS changed
778 # set comments as outdated if DIFFS changed
778 CommentsModel().outdate_comments(
779 CommentsModel().outdate_comments(
779 pull_request, old_diff_data=old_diff_data,
780 pull_request, old_diff_data=old_diff_data,
780 new_diff_data=new_diff_data)
781 new_diff_data=new_diff_data)
781
782
782 commit_changes = (changes.added or changes.removed)
783 commit_changes = (changes.added or changes.removed)
783 file_node_changes = (
784 file_node_changes = (
784 file_changes.added or file_changes.modified or file_changes.removed)
785 file_changes.added or file_changes.modified or file_changes.removed)
785 pr_has_changes = commit_changes or file_node_changes
786 pr_has_changes = commit_changes or file_node_changes
786
787
787 # Add an automatic comment to the pull request, in case
788 # Add an automatic comment to the pull request, in case
788 # anything has changed
789 # anything has changed
789 if pr_has_changes:
790 if pr_has_changes:
790 update_comment = CommentsModel().create(
791 update_comment = CommentsModel().create(
791 text=self._render_update_message(changes, file_changes),
792 text=self._render_update_message(changes, file_changes),
792 repo=pull_request.target_repo,
793 repo=pull_request.target_repo,
793 user=pull_request.author,
794 user=pull_request.author,
794 pull_request=pull_request,
795 pull_request=pull_request,
795 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
796 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
796
797
797 # Update status to "Under Review" for added commits
798 # Update status to "Under Review" for added commits
798 for commit_id in changes.added:
799 for commit_id in changes.added:
799 ChangesetStatusModel().set_status(
800 ChangesetStatusModel().set_status(
800 repo=pull_request.source_repo,
801 repo=pull_request.source_repo,
801 status=ChangesetStatus.STATUS_UNDER_REVIEW,
802 status=ChangesetStatus.STATUS_UNDER_REVIEW,
802 comment=update_comment,
803 comment=update_comment,
803 user=pull_request.author,
804 user=pull_request.author,
804 pull_request=pull_request,
805 pull_request=pull_request,
805 revision=commit_id)
806 revision=commit_id)
806
807
807 log.debug(
808 log.debug(
808 'Updated pull request %s, added_ids: %s, common_ids: %s, '
809 'Updated pull request %s, added_ids: %s, common_ids: %s, '
809 'removed_ids: %s', pull_request.pull_request_id,
810 'removed_ids: %s', pull_request.pull_request_id,
810 changes.added, changes.common, changes.removed)
811 changes.added, changes.common, changes.removed)
811 log.debug(
812 log.debug(
812 'Updated pull request with the following file changes: %s',
813 'Updated pull request with the following file changes: %s',
813 file_changes)
814 file_changes)
814
815
815 log.info(
816 log.info(
816 "Updated pull request %s from commit %s to commit %s, "
817 "Updated pull request %s from commit %s to commit %s, "
817 "stored new version %s of this pull request.",
818 "stored new version %s of this pull request.",
818 pull_request.pull_request_id, source_ref_id,
819 pull_request.pull_request_id, source_ref_id,
819 pull_request.source_ref_parts.commit_id,
820 pull_request.source_ref_parts.commit_id,
820 pull_request_version.pull_request_version_id)
821 pull_request_version.pull_request_version_id)
821 Session().commit()
822 Session().commit()
822 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
823 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
823
824
824 return UpdateResponse(
825 return UpdateResponse(
825 executed=True, reason=UpdateFailureReason.NONE,
826 executed=True, reason=UpdateFailureReason.NONE,
826 old=pull_request, new=pull_request_version, changes=changes,
827 old=pull_request, new=pull_request_version, changes=changes,
827 source_changed=source_changed, target_changed=target_changed)
828 source_changed=source_changed, target_changed=target_changed)
828
829
829 def _create_version_from_snapshot(self, pull_request):
830 def _create_version_from_snapshot(self, pull_request):
830 version = PullRequestVersion()
831 version = PullRequestVersion()
831 version.title = pull_request.title
832 version.title = pull_request.title
832 version.description = pull_request.description
833 version.description = pull_request.description
833 version.status = pull_request.status
834 version.status = pull_request.status
834 version.pull_request_state = pull_request.pull_request_state
835 version.pull_request_state = pull_request.pull_request_state
835 version.created_on = datetime.datetime.now()
836 version.created_on = datetime.datetime.now()
836 version.updated_on = pull_request.updated_on
837 version.updated_on = pull_request.updated_on
837 version.user_id = pull_request.user_id
838 version.user_id = pull_request.user_id
838 version.source_repo = pull_request.source_repo
839 version.source_repo = pull_request.source_repo
839 version.source_ref = pull_request.source_ref
840 version.source_ref = pull_request.source_ref
840 version.target_repo = pull_request.target_repo
841 version.target_repo = pull_request.target_repo
841 version.target_ref = pull_request.target_ref
842 version.target_ref = pull_request.target_ref
842
843
843 version._last_merge_source_rev = pull_request._last_merge_source_rev
844 version._last_merge_source_rev = pull_request._last_merge_source_rev
844 version._last_merge_target_rev = pull_request._last_merge_target_rev
845 version._last_merge_target_rev = pull_request._last_merge_target_rev
845 version.last_merge_status = pull_request.last_merge_status
846 version.last_merge_status = pull_request.last_merge_status
846 version.shadow_merge_ref = pull_request.shadow_merge_ref
847 version.shadow_merge_ref = pull_request.shadow_merge_ref
847 version.merge_rev = pull_request.merge_rev
848 version.merge_rev = pull_request.merge_rev
848 version.reviewer_data = pull_request.reviewer_data
849 version.reviewer_data = pull_request.reviewer_data
849
850
850 version.revisions = pull_request.revisions
851 version.revisions = pull_request.revisions
851 version.pull_request = pull_request
852 version.pull_request = pull_request
852 Session().add(version)
853 Session().add(version)
853 Session().flush()
854 Session().flush()
854
855
855 return version
856 return version
856
857
857 def _generate_update_diffs(self, pull_request, pull_request_version):
858 def _generate_update_diffs(self, pull_request, pull_request_version):
858
859
859 diff_context = (
860 diff_context = (
860 self.DIFF_CONTEXT +
861 self.DIFF_CONTEXT +
861 CommentsModel.needed_extra_diff_context())
862 CommentsModel.needed_extra_diff_context())
862 hide_whitespace_changes = False
863 hide_whitespace_changes = False
863 source_repo = pull_request_version.source_repo
864 source_repo = pull_request_version.source_repo
864 source_ref_id = pull_request_version.source_ref_parts.commit_id
865 source_ref_id = pull_request_version.source_ref_parts.commit_id
865 target_ref_id = pull_request_version.target_ref_parts.commit_id
866 target_ref_id = pull_request_version.target_ref_parts.commit_id
866 old_diff = self._get_diff_from_pr_or_version(
867 old_diff = self._get_diff_from_pr_or_version(
867 source_repo, source_ref_id, target_ref_id,
868 source_repo, source_ref_id, target_ref_id,
868 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
869 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
869
870
870 source_repo = pull_request.source_repo
871 source_repo = pull_request.source_repo
871 source_ref_id = pull_request.source_ref_parts.commit_id
872 source_ref_id = pull_request.source_ref_parts.commit_id
872 target_ref_id = pull_request.target_ref_parts.commit_id
873 target_ref_id = pull_request.target_ref_parts.commit_id
873
874
874 new_diff = self._get_diff_from_pr_or_version(
875 new_diff = self._get_diff_from_pr_or_version(
875 source_repo, source_ref_id, target_ref_id,
876 source_repo, source_ref_id, target_ref_id,
876 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
877 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
877
878
878 old_diff_data = diffs.DiffProcessor(old_diff)
879 old_diff_data = diffs.DiffProcessor(old_diff)
879 old_diff_data.prepare()
880 old_diff_data.prepare()
880 new_diff_data = diffs.DiffProcessor(new_diff)
881 new_diff_data = diffs.DiffProcessor(new_diff)
881 new_diff_data.prepare()
882 new_diff_data.prepare()
882
883
883 return old_diff_data, new_diff_data
884 return old_diff_data, new_diff_data
884
885
885 def _link_comments_to_version(self, pull_request_version):
886 def _link_comments_to_version(self, pull_request_version):
886 """
887 """
887 Link all unlinked comments of this pull request to the given version.
888 Link all unlinked comments of this pull request to the given version.
888
889
889 :param pull_request_version: The `PullRequestVersion` to which
890 :param pull_request_version: The `PullRequestVersion` to which
890 the comments shall be linked.
891 the comments shall be linked.
891
892
892 """
893 """
893 pull_request = pull_request_version.pull_request
894 pull_request = pull_request_version.pull_request
894 comments = ChangesetComment.query()\
895 comments = ChangesetComment.query()\
895 .filter(
896 .filter(
896 # TODO: johbo: Should we query for the repo at all here?
897 # TODO: johbo: Should we query for the repo at all here?
897 # Pending decision on how comments of PRs are to be related
898 # Pending decision on how comments of PRs are to be related
898 # to either the source repo, the target repo or no repo at all.
899 # to either the source repo, the target repo or no repo at all.
899 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
900 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
900 ChangesetComment.pull_request == pull_request,
901 ChangesetComment.pull_request == pull_request,
901 ChangesetComment.pull_request_version == None)\
902 ChangesetComment.pull_request_version == None)\
902 .order_by(ChangesetComment.comment_id.asc())
903 .order_by(ChangesetComment.comment_id.asc())
903
904
904 # TODO: johbo: Find out why this breaks if it is done in a bulk
905 # TODO: johbo: Find out why this breaks if it is done in a bulk
905 # operation.
906 # operation.
906 for comment in comments:
907 for comment in comments:
907 comment.pull_request_version_id = (
908 comment.pull_request_version_id = (
908 pull_request_version.pull_request_version_id)
909 pull_request_version.pull_request_version_id)
909 Session().add(comment)
910 Session().add(comment)
910
911
911 def _calculate_commit_id_changes(self, old_ids, new_ids):
912 def _calculate_commit_id_changes(self, old_ids, new_ids):
912 added = [x for x in new_ids if x not in old_ids]
913 added = [x for x in new_ids if x not in old_ids]
913 common = [x for x in new_ids if x in old_ids]
914 common = [x for x in new_ids if x in old_ids]
914 removed = [x for x in old_ids if x not in new_ids]
915 removed = [x for x in old_ids if x not in new_ids]
915 total = new_ids
916 total = new_ids
916 return ChangeTuple(added, common, removed, total)
917 return ChangeTuple(added, common, removed, total)
917
918
918 def _calculate_file_changes(self, old_diff_data, new_diff_data):
919 def _calculate_file_changes(self, old_diff_data, new_diff_data):
919
920
920 old_files = OrderedDict()
921 old_files = OrderedDict()
921 for diff_data in old_diff_data.parsed_diff:
922 for diff_data in old_diff_data.parsed_diff:
922 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
923 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
923
924
924 added_files = []
925 added_files = []
925 modified_files = []
926 modified_files = []
926 removed_files = []
927 removed_files = []
927 for diff_data in new_diff_data.parsed_diff:
928 for diff_data in new_diff_data.parsed_diff:
928 new_filename = diff_data['filename']
929 new_filename = diff_data['filename']
929 new_hash = md5_safe(diff_data['raw_diff'])
930 new_hash = md5_safe(diff_data['raw_diff'])
930
931
931 old_hash = old_files.get(new_filename)
932 old_hash = old_files.get(new_filename)
932 if not old_hash:
933 if not old_hash:
933 # file is not present in old diff, means it's added
934 # file is not present in old diff, means it's added
934 added_files.append(new_filename)
935 added_files.append(new_filename)
935 else:
936 else:
936 if new_hash != old_hash:
937 if new_hash != old_hash:
937 modified_files.append(new_filename)
938 modified_files.append(new_filename)
938 # now remove a file from old, since we have seen it already
939 # now remove a file from old, since we have seen it already
939 del old_files[new_filename]
940 del old_files[new_filename]
940
941
941 # removed files is when there are present in old, but not in NEW,
942 # removed files is when there are present in old, but not in NEW,
942 # since we remove old files that are present in new diff, left-overs
943 # since we remove old files that are present in new diff, left-overs
943 # if any should be the removed files
944 # if any should be the removed files
944 removed_files.extend(old_files.keys())
945 removed_files.extend(old_files.keys())
945
946
946 return FileChangeTuple(added_files, modified_files, removed_files)
947 return FileChangeTuple(added_files, modified_files, removed_files)
947
948
948 def _render_update_message(self, changes, file_changes):
949 def _render_update_message(self, changes, file_changes):
949 """
950 """
950 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
951 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
951 so it's always looking the same disregarding on which default
952 so it's always looking the same disregarding on which default
952 renderer system is using.
953 renderer system is using.
953
954
954 :param changes: changes named tuple
955 :param changes: changes named tuple
955 :param file_changes: file changes named tuple
956 :param file_changes: file changes named tuple
956
957
957 """
958 """
958 new_status = ChangesetStatus.get_status_lbl(
959 new_status = ChangesetStatus.get_status_lbl(
959 ChangesetStatus.STATUS_UNDER_REVIEW)
960 ChangesetStatus.STATUS_UNDER_REVIEW)
960
961
961 changed_files = (
962 changed_files = (
962 file_changes.added + file_changes.modified + file_changes.removed)
963 file_changes.added + file_changes.modified + file_changes.removed)
963
964
964 params = {
965 params = {
965 'under_review_label': new_status,
966 'under_review_label': new_status,
966 'added_commits': changes.added,
967 'added_commits': changes.added,
967 'removed_commits': changes.removed,
968 'removed_commits': changes.removed,
968 'changed_files': changed_files,
969 'changed_files': changed_files,
969 'added_files': file_changes.added,
970 'added_files': file_changes.added,
970 'modified_files': file_changes.modified,
971 'modified_files': file_changes.modified,
971 'removed_files': file_changes.removed,
972 'removed_files': file_changes.removed,
972 }
973 }
973 renderer = RstTemplateRenderer()
974 renderer = RstTemplateRenderer()
974 return renderer.render('pull_request_update.mako', **params)
975 return renderer.render('pull_request_update.mako', **params)
975
976
976 def edit(self, pull_request, title, description, description_renderer, user):
977 def edit(self, pull_request, title, description, description_renderer, user):
977 pull_request = self.__get_pull_request(pull_request)
978 pull_request = self.__get_pull_request(pull_request)
978 old_data = pull_request.get_api_data(with_merge_state=False)
979 old_data = pull_request.get_api_data(with_merge_state=False)
979 if pull_request.is_closed():
980 if pull_request.is_closed():
980 raise ValueError('This pull request is closed')
981 raise ValueError('This pull request is closed')
981 if title:
982 if title:
982 pull_request.title = title
983 pull_request.title = title
983 pull_request.description = description
984 pull_request.description = description
984 pull_request.updated_on = datetime.datetime.now()
985 pull_request.updated_on = datetime.datetime.now()
985 pull_request.description_renderer = description_renderer
986 pull_request.description_renderer = description_renderer
986 Session().add(pull_request)
987 Session().add(pull_request)
987 self._log_audit_action(
988 self._log_audit_action(
988 'repo.pull_request.edit', {'old_data': old_data},
989 'repo.pull_request.edit', {'old_data': old_data},
989 user, pull_request)
990 user, pull_request)
990
991
991 def update_reviewers(self, pull_request, reviewer_data, user):
992 def update_reviewers(self, pull_request, reviewer_data, user):
992 """
993 """
993 Update the reviewers in the pull request
994 Update the reviewers in the pull request
994
995
995 :param pull_request: the pr to update
996 :param pull_request: the pr to update
996 :param reviewer_data: list of tuples
997 :param reviewer_data: list of tuples
997 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
998 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
998 """
999 """
999 pull_request = self.__get_pull_request(pull_request)
1000 pull_request = self.__get_pull_request(pull_request)
1000 if pull_request.is_closed():
1001 if pull_request.is_closed():
1001 raise ValueError('This pull request is closed')
1002 raise ValueError('This pull request is closed')
1002
1003
1003 reviewers = {}
1004 reviewers = {}
1004 for user_id, reasons, mandatory, rules in reviewer_data:
1005 for user_id, reasons, mandatory, rules in reviewer_data:
1005 if isinstance(user_id, (int, compat.string_types)):
1006 if isinstance(user_id, (int, compat.string_types)):
1006 user_id = self._get_user(user_id).user_id
1007 user_id = self._get_user(user_id).user_id
1007 reviewers[user_id] = {
1008 reviewers[user_id] = {
1008 'reasons': reasons, 'mandatory': mandatory}
1009 'reasons': reasons, 'mandatory': mandatory}
1009
1010
1010 reviewers_ids = set(reviewers.keys())
1011 reviewers_ids = set(reviewers.keys())
1011 current_reviewers = PullRequestReviewers.query()\
1012 current_reviewers = PullRequestReviewers.query()\
1012 .filter(PullRequestReviewers.pull_request ==
1013 .filter(PullRequestReviewers.pull_request ==
1013 pull_request).all()
1014 pull_request).all()
1014 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1015 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1015
1016
1016 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1017 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1017 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1018 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1018
1019
1019 log.debug("Adding %s reviewers", ids_to_add)
1020 log.debug("Adding %s reviewers", ids_to_add)
1020 log.debug("Removing %s reviewers", ids_to_remove)
1021 log.debug("Removing %s reviewers", ids_to_remove)
1021 changed = False
1022 changed = False
1022 for uid in ids_to_add:
1023 for uid in ids_to_add:
1023 changed = True
1024 changed = True
1024 _usr = self._get_user(uid)
1025 _usr = self._get_user(uid)
1025 reviewer = PullRequestReviewers()
1026 reviewer = PullRequestReviewers()
1026 reviewer.user = _usr
1027 reviewer.user = _usr
1027 reviewer.pull_request = pull_request
1028 reviewer.pull_request = pull_request
1028 reviewer.reasons = reviewers[uid]['reasons']
1029 reviewer.reasons = reviewers[uid]['reasons']
1029 # NOTE(marcink): mandatory shouldn't be changed now
1030 # NOTE(marcink): mandatory shouldn't be changed now
1030 # reviewer.mandatory = reviewers[uid]['reasons']
1031 # reviewer.mandatory = reviewers[uid]['reasons']
1031 Session().add(reviewer)
1032 Session().add(reviewer)
1032 self._log_audit_action(
1033 self._log_audit_action(
1033 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1034 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1034 user, pull_request)
1035 user, pull_request)
1035
1036
1036 for uid in ids_to_remove:
1037 for uid in ids_to_remove:
1037 changed = True
1038 changed = True
1038 reviewers = PullRequestReviewers.query()\
1039 reviewers = PullRequestReviewers.query()\
1039 .filter(PullRequestReviewers.user_id == uid,
1040 .filter(PullRequestReviewers.user_id == uid,
1040 PullRequestReviewers.pull_request == pull_request)\
1041 PullRequestReviewers.pull_request == pull_request)\
1041 .all()
1042 .all()
1042 # use .all() in case we accidentally added the same person twice
1043 # use .all() in case we accidentally added the same person twice
1043 # this CAN happen due to the lack of DB checks
1044 # this CAN happen due to the lack of DB checks
1044 for obj in reviewers:
1045 for obj in reviewers:
1045 old_data = obj.get_dict()
1046 old_data = obj.get_dict()
1046 Session().delete(obj)
1047 Session().delete(obj)
1047 self._log_audit_action(
1048 self._log_audit_action(
1048 'repo.pull_request.reviewer.delete',
1049 'repo.pull_request.reviewer.delete',
1049 {'old_data': old_data}, user, pull_request)
1050 {'old_data': old_data}, user, pull_request)
1050
1051
1051 if changed:
1052 if changed:
1052 pull_request.updated_on = datetime.datetime.now()
1053 pull_request.updated_on = datetime.datetime.now()
1053 Session().add(pull_request)
1054 Session().add(pull_request)
1054
1055
1055 self.notify_reviewers(pull_request, ids_to_add)
1056 self.notify_reviewers(pull_request, ids_to_add)
1056 return ids_to_add, ids_to_remove
1057 return ids_to_add, ids_to_remove
1057
1058
1058 def get_url(self, pull_request, request=None, permalink=False):
1059 def get_url(self, pull_request, request=None, permalink=False):
1059 if not request:
1060 if not request:
1060 request = get_current_request()
1061 request = get_current_request()
1061
1062
1062 if permalink:
1063 if permalink:
1063 return request.route_url(
1064 return request.route_url(
1064 'pull_requests_global',
1065 'pull_requests_global',
1065 pull_request_id=pull_request.pull_request_id,)
1066 pull_request_id=pull_request.pull_request_id,)
1066 else:
1067 else:
1067 return request.route_url('pullrequest_show',
1068 return request.route_url('pullrequest_show',
1068 repo_name=safe_str(pull_request.target_repo.repo_name),
1069 repo_name=safe_str(pull_request.target_repo.repo_name),
1069 pull_request_id=pull_request.pull_request_id,)
1070 pull_request_id=pull_request.pull_request_id,)
1070
1071
1071 def get_shadow_clone_url(self, pull_request, request=None):
1072 def get_shadow_clone_url(self, pull_request, request=None):
1072 """
1073 """
1073 Returns qualified url pointing to the shadow repository. If this pull
1074 Returns qualified url pointing to the shadow repository. If this pull
1074 request is closed there is no shadow repository and ``None`` will be
1075 request is closed there is no shadow repository and ``None`` will be
1075 returned.
1076 returned.
1076 """
1077 """
1077 if pull_request.is_closed():
1078 if pull_request.is_closed():
1078 return None
1079 return None
1079 else:
1080 else:
1080 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1081 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1081 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1082 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1082
1083
1083 def notify_reviewers(self, pull_request, reviewers_ids):
1084 def notify_reviewers(self, pull_request, reviewers_ids):
1084 # notification to reviewers
1085 # notification to reviewers
1085 if not reviewers_ids:
1086 if not reviewers_ids:
1086 return
1087 return
1087
1088
1088 pull_request_obj = pull_request
1089 pull_request_obj = pull_request
1089 # get the current participants of this pull request
1090 # get the current participants of this pull request
1090 recipients = reviewers_ids
1091 recipients = reviewers_ids
1091 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1092 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1092
1093
1093 pr_source_repo = pull_request_obj.source_repo
1094 pr_source_repo = pull_request_obj.source_repo
1094 pr_target_repo = pull_request_obj.target_repo
1095 pr_target_repo = pull_request_obj.target_repo
1095
1096
1096 pr_url = h.route_url('pullrequest_show',
1097 pr_url = h.route_url('pullrequest_show',
1097 repo_name=pr_target_repo.repo_name,
1098 repo_name=pr_target_repo.repo_name,
1098 pull_request_id=pull_request_obj.pull_request_id,)
1099 pull_request_id=pull_request_obj.pull_request_id,)
1099
1100
1100 # set some variables for email notification
1101 # set some variables for email notification
1101 pr_target_repo_url = h.route_url(
1102 pr_target_repo_url = h.route_url(
1102 'repo_summary', repo_name=pr_target_repo.repo_name)
1103 'repo_summary', repo_name=pr_target_repo.repo_name)
1103
1104
1104 pr_source_repo_url = h.route_url(
1105 pr_source_repo_url = h.route_url(
1105 'repo_summary', repo_name=pr_source_repo.repo_name)
1106 'repo_summary', repo_name=pr_source_repo.repo_name)
1106
1107
1107 # pull request specifics
1108 # pull request specifics
1108 pull_request_commits = [
1109 pull_request_commits = [
1109 (x.raw_id, x.message)
1110 (x.raw_id, x.message)
1110 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1111 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1111
1112
1112 kwargs = {
1113 kwargs = {
1113 'user': pull_request.author,
1114 'user': pull_request.author,
1114 'pull_request': pull_request_obj,
1115 'pull_request': pull_request_obj,
1115 'pull_request_commits': pull_request_commits,
1116 'pull_request_commits': pull_request_commits,
1116
1117
1117 'pull_request_target_repo': pr_target_repo,
1118 'pull_request_target_repo': pr_target_repo,
1118 'pull_request_target_repo_url': pr_target_repo_url,
1119 'pull_request_target_repo_url': pr_target_repo_url,
1119
1120
1120 'pull_request_source_repo': pr_source_repo,
1121 'pull_request_source_repo': pr_source_repo,
1121 'pull_request_source_repo_url': pr_source_repo_url,
1122 'pull_request_source_repo_url': pr_source_repo_url,
1122
1123
1123 'pull_request_url': pr_url,
1124 'pull_request_url': pr_url,
1124 }
1125 }
1125
1126
1126 # pre-generate the subject for notification itself
1127 # pre-generate the subject for notification itself
1127 (subject,
1128 (subject,
1128 _h, _e, # we don't care about those
1129 _h, _e, # we don't care about those
1129 body_plaintext) = EmailNotificationModel().render_email(
1130 body_plaintext) = EmailNotificationModel().render_email(
1130 notification_type, **kwargs)
1131 notification_type, **kwargs)
1131
1132
1132 # create notification objects, and emails
1133 # create notification objects, and emails
1133 NotificationModel().create(
1134 NotificationModel().create(
1134 created_by=pull_request.author,
1135 created_by=pull_request.author,
1135 notification_subject=subject,
1136 notification_subject=subject,
1136 notification_body=body_plaintext,
1137 notification_body=body_plaintext,
1137 notification_type=notification_type,
1138 notification_type=notification_type,
1138 recipients=recipients,
1139 recipients=recipients,
1139 email_kwargs=kwargs,
1140 email_kwargs=kwargs,
1140 )
1141 )
1141
1142
1142 def delete(self, pull_request, user):
1143 def delete(self, pull_request, user):
1143 pull_request = self.__get_pull_request(pull_request)
1144 pull_request = self.__get_pull_request(pull_request)
1144 old_data = pull_request.get_api_data(with_merge_state=False)
1145 old_data = pull_request.get_api_data(with_merge_state=False)
1145 self._cleanup_merge_workspace(pull_request)
1146 self._cleanup_merge_workspace(pull_request)
1146 self._log_audit_action(
1147 self._log_audit_action(
1147 'repo.pull_request.delete', {'old_data': old_data},
1148 'repo.pull_request.delete', {'old_data': old_data},
1148 user, pull_request)
1149 user, pull_request)
1149 Session().delete(pull_request)
1150 Session().delete(pull_request)
1150
1151
1151 def close_pull_request(self, pull_request, user):
1152 def close_pull_request(self, pull_request, user):
1152 pull_request = self.__get_pull_request(pull_request)
1153 pull_request = self.__get_pull_request(pull_request)
1153 self._cleanup_merge_workspace(pull_request)
1154 self._cleanup_merge_workspace(pull_request)
1154 pull_request.status = PullRequest.STATUS_CLOSED
1155 pull_request.status = PullRequest.STATUS_CLOSED
1155 pull_request.updated_on = datetime.datetime.now()
1156 pull_request.updated_on = datetime.datetime.now()
1156 Session().add(pull_request)
1157 Session().add(pull_request)
1157 self.trigger_pull_request_hook(
1158 self.trigger_pull_request_hook(
1158 pull_request, pull_request.author, 'close')
1159 pull_request, pull_request.author, 'close')
1159
1160
1160 pr_data = pull_request.get_api_data(with_merge_state=False)
1161 pr_data = pull_request.get_api_data(with_merge_state=False)
1161 self._log_audit_action(
1162 self._log_audit_action(
1162 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1163 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1163
1164
1164 def close_pull_request_with_comment(
1165 def close_pull_request_with_comment(
1165 self, pull_request, user, repo, message=None, auth_user=None):
1166 self, pull_request, user, repo, message=None, auth_user=None):
1166
1167
1167 pull_request_review_status = pull_request.calculated_review_status()
1168 pull_request_review_status = pull_request.calculated_review_status()
1168
1169
1169 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1170 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1170 # approved only if we have voting consent
1171 # approved only if we have voting consent
1171 status = ChangesetStatus.STATUS_APPROVED
1172 status = ChangesetStatus.STATUS_APPROVED
1172 else:
1173 else:
1173 status = ChangesetStatus.STATUS_REJECTED
1174 status = ChangesetStatus.STATUS_REJECTED
1174 status_lbl = ChangesetStatus.get_status_lbl(status)
1175 status_lbl = ChangesetStatus.get_status_lbl(status)
1175
1176
1176 default_message = (
1177 default_message = (
1177 'Closing with status change {transition_icon} {status}.'
1178 'Closing with status change {transition_icon} {status}.'
1178 ).format(transition_icon='>', status=status_lbl)
1179 ).format(transition_icon='>', status=status_lbl)
1179 text = message or default_message
1180 text = message or default_message
1180
1181
1181 # create a comment, and link it to new status
1182 # create a comment, and link it to new status
1182 comment = CommentsModel().create(
1183 comment = CommentsModel().create(
1183 text=text,
1184 text=text,
1184 repo=repo.repo_id,
1185 repo=repo.repo_id,
1185 user=user.user_id,
1186 user=user.user_id,
1186 pull_request=pull_request.pull_request_id,
1187 pull_request=pull_request.pull_request_id,
1187 status_change=status_lbl,
1188 status_change=status_lbl,
1188 status_change_type=status,
1189 status_change_type=status,
1189 closing_pr=True,
1190 closing_pr=True,
1190 auth_user=auth_user,
1191 auth_user=auth_user,
1191 )
1192 )
1192
1193
1193 # calculate old status before we change it
1194 # calculate old status before we change it
1194 old_calculated_status = pull_request.calculated_review_status()
1195 old_calculated_status = pull_request.calculated_review_status()
1195 ChangesetStatusModel().set_status(
1196 ChangesetStatusModel().set_status(
1196 repo.repo_id,
1197 repo.repo_id,
1197 status,
1198 status,
1198 user.user_id,
1199 user.user_id,
1199 comment=comment,
1200 comment=comment,
1200 pull_request=pull_request.pull_request_id
1201 pull_request=pull_request.pull_request_id
1201 )
1202 )
1202
1203
1203 Session().flush()
1204 Session().flush()
1204 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1205 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1205 # we now calculate the status of pull request again, and based on that
1206 # we now calculate the status of pull request again, and based on that
1206 # calculation trigger status change. This might happen in cases
1207 # calculation trigger status change. This might happen in cases
1207 # that non-reviewer admin closes a pr, which means his vote doesn't
1208 # that non-reviewer admin closes a pr, which means his vote doesn't
1208 # change the status, while if he's a reviewer this might change it.
1209 # change the status, while if he's a reviewer this might change it.
1209 calculated_status = pull_request.calculated_review_status()
1210 calculated_status = pull_request.calculated_review_status()
1210 if old_calculated_status != calculated_status:
1211 if old_calculated_status != calculated_status:
1211 self.trigger_pull_request_hook(
1212 self.trigger_pull_request_hook(
1212 pull_request, user, 'review_status_change',
1213 pull_request, user, 'review_status_change',
1213 data={'status': calculated_status})
1214 data={'status': calculated_status})
1214
1215
1215 # finally close the PR
1216 # finally close the PR
1216 PullRequestModel().close_pull_request(
1217 PullRequestModel().close_pull_request(
1217 pull_request.pull_request_id, user)
1218 pull_request.pull_request_id, user)
1218
1219
1219 return comment, status
1220 return comment, status
1220
1221
1221 def merge_status(self, pull_request, translator=None,
1222 def merge_status(self, pull_request, translator=None,
1222 force_shadow_repo_refresh=False):
1223 force_shadow_repo_refresh=False):
1223 _ = translator or get_current_request().translate
1224 _ = translator or get_current_request().translate
1224
1225
1225 if not self._is_merge_enabled(pull_request):
1226 if not self._is_merge_enabled(pull_request):
1226 return False, _('Server-side pull request merging is disabled.')
1227 return False, _('Server-side pull request merging is disabled.')
1227 if pull_request.is_closed():
1228 if pull_request.is_closed():
1228 return False, _('This pull request is closed.')
1229 return False, _('This pull request is closed.')
1229 merge_possible, msg = self._check_repo_requirements(
1230 merge_possible, msg = self._check_repo_requirements(
1230 target=pull_request.target_repo, source=pull_request.source_repo,
1231 target=pull_request.target_repo, source=pull_request.source_repo,
1231 translator=_)
1232 translator=_)
1232 if not merge_possible:
1233 if not merge_possible:
1233 return merge_possible, msg
1234 return merge_possible, msg
1234
1235
1235 try:
1236 try:
1236 resp = self._try_merge(
1237 resp = self._try_merge(
1237 pull_request,
1238 pull_request,
1238 force_shadow_repo_refresh=force_shadow_repo_refresh)
1239 force_shadow_repo_refresh=force_shadow_repo_refresh)
1239 log.debug("Merge response: %s", resp)
1240 log.debug("Merge response: %s", resp)
1240 status = resp.possible, resp.merge_status_message
1241 status = resp.possible, resp.merge_status_message
1241 except NotImplementedError:
1242 except NotImplementedError:
1242 status = False, _('Pull request merging is not supported.')
1243 status = False, _('Pull request merging is not supported.')
1243
1244
1244 return status
1245 return status
1245
1246
1246 def _check_repo_requirements(self, target, source, translator):
1247 def _check_repo_requirements(self, target, source, translator):
1247 """
1248 """
1248 Check if `target` and `source` have compatible requirements.
1249 Check if `target` and `source` have compatible requirements.
1249
1250
1250 Currently this is just checking for largefiles.
1251 Currently this is just checking for largefiles.
1251 """
1252 """
1252 _ = translator
1253 _ = translator
1253 target_has_largefiles = self._has_largefiles(target)
1254 target_has_largefiles = self._has_largefiles(target)
1254 source_has_largefiles = self._has_largefiles(source)
1255 source_has_largefiles = self._has_largefiles(source)
1255 merge_possible = True
1256 merge_possible = True
1256 message = u''
1257 message = u''
1257
1258
1258 if target_has_largefiles != source_has_largefiles:
1259 if target_has_largefiles != source_has_largefiles:
1259 merge_possible = False
1260 merge_possible = False
1260 if source_has_largefiles:
1261 if source_has_largefiles:
1261 message = _(
1262 message = _(
1262 'Target repository large files support is disabled.')
1263 'Target repository large files support is disabled.')
1263 else:
1264 else:
1264 message = _(
1265 message = _(
1265 'Source repository large files support is disabled.')
1266 'Source repository large files support is disabled.')
1266
1267
1267 return merge_possible, message
1268 return merge_possible, message
1268
1269
1269 def _has_largefiles(self, repo):
1270 def _has_largefiles(self, repo):
1270 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1271 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1271 'extensions', 'largefiles')
1272 'extensions', 'largefiles')
1272 return largefiles_ui and largefiles_ui[0].active
1273 return largefiles_ui and largefiles_ui[0].active
1273
1274
1274 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1275 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1275 """
1276 """
1276 Try to merge the pull request and return the merge status.
1277 Try to merge the pull request and return the merge status.
1277 """
1278 """
1278 log.debug(
1279 log.debug(
1279 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1280 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1280 pull_request.pull_request_id, force_shadow_repo_refresh)
1281 pull_request.pull_request_id, force_shadow_repo_refresh)
1281 target_vcs = pull_request.target_repo.scm_instance()
1282 target_vcs = pull_request.target_repo.scm_instance()
1282 # Refresh the target reference.
1283 # Refresh the target reference.
1283 try:
1284 try:
1284 target_ref = self._refresh_reference(
1285 target_ref = self._refresh_reference(
1285 pull_request.target_ref_parts, target_vcs)
1286 pull_request.target_ref_parts, target_vcs)
1286 except CommitDoesNotExistError:
1287 except CommitDoesNotExistError:
1287 merge_state = MergeResponse(
1288 merge_state = MergeResponse(
1288 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1289 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1289 metadata={'target_ref': pull_request.target_ref_parts})
1290 metadata={'target_ref': pull_request.target_ref_parts})
1290 return merge_state
1291 return merge_state
1291
1292
1292 target_locked = pull_request.target_repo.locked
1293 target_locked = pull_request.target_repo.locked
1293 if target_locked and target_locked[0]:
1294 if target_locked and target_locked[0]:
1294 locked_by = 'user:{}'.format(target_locked[0])
1295 locked_by = 'user:{}'.format(target_locked[0])
1295 log.debug("The target repository is locked by %s.", locked_by)
1296 log.debug("The target repository is locked by %s.", locked_by)
1296 merge_state = MergeResponse(
1297 merge_state = MergeResponse(
1297 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1298 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1298 metadata={'locked_by': locked_by})
1299 metadata={'locked_by': locked_by})
1299 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1300 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1300 pull_request, target_ref):
1301 pull_request, target_ref):
1301 log.debug("Refreshing the merge status of the repository.")
1302 log.debug("Refreshing the merge status of the repository.")
1302 merge_state = self._refresh_merge_state(
1303 merge_state = self._refresh_merge_state(
1303 pull_request, target_vcs, target_ref)
1304 pull_request, target_vcs, target_ref)
1304 else:
1305 else:
1305 possible = pull_request.\
1306 possible = pull_request.\
1306 last_merge_status == MergeFailureReason.NONE
1307 last_merge_status == MergeFailureReason.NONE
1307 merge_state = MergeResponse(
1308 merge_state = MergeResponse(
1308 possible, False, None, pull_request.last_merge_status)
1309 possible, False, None, pull_request.last_merge_status)
1309
1310
1310 return merge_state
1311 return merge_state
1311
1312
1312 def _refresh_reference(self, reference, vcs_repository):
1313 def _refresh_reference(self, reference, vcs_repository):
1313 if reference.type in self.UPDATABLE_REF_TYPES:
1314 if reference.type in self.UPDATABLE_REF_TYPES:
1314 name_or_id = reference.name
1315 name_or_id = reference.name
1315 else:
1316 else:
1316 name_or_id = reference.commit_id
1317 name_or_id = reference.commit_id
1317 refreshed_commit = vcs_repository.get_commit(name_or_id)
1318 refreshed_commit = vcs_repository.get_commit(name_or_id)
1318 refreshed_reference = Reference(
1319 refreshed_reference = Reference(
1319 reference.type, reference.name, refreshed_commit.raw_id)
1320 reference.type, reference.name, refreshed_commit.raw_id)
1320 return refreshed_reference
1321 return refreshed_reference
1321
1322
1322 def _needs_merge_state_refresh(self, pull_request, target_reference):
1323 def _needs_merge_state_refresh(self, pull_request, target_reference):
1323 return not(
1324 return not(
1324 pull_request.revisions and
1325 pull_request.revisions and
1325 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1326 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1326 target_reference.commit_id == pull_request._last_merge_target_rev)
1327 target_reference.commit_id == pull_request._last_merge_target_rev)
1327
1328
1328 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1329 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1329 workspace_id = self._workspace_id(pull_request)
1330 workspace_id = self._workspace_id(pull_request)
1330 source_vcs = pull_request.source_repo.scm_instance()
1331 source_vcs = pull_request.source_repo.scm_instance()
1331 repo_id = pull_request.target_repo.repo_id
1332 repo_id = pull_request.target_repo.repo_id
1332 use_rebase = self._use_rebase_for_merging(pull_request)
1333 use_rebase = self._use_rebase_for_merging(pull_request)
1333 close_branch = self._close_branch_before_merging(pull_request)
1334 close_branch = self._close_branch_before_merging(pull_request)
1334 merge_state = target_vcs.merge(
1335 merge_state = target_vcs.merge(
1335 repo_id, workspace_id,
1336 repo_id, workspace_id,
1336 target_reference, source_vcs, pull_request.source_ref_parts,
1337 target_reference, source_vcs, pull_request.source_ref_parts,
1337 dry_run=True, use_rebase=use_rebase,
1338 dry_run=True, use_rebase=use_rebase,
1338 close_branch=close_branch)
1339 close_branch=close_branch)
1339
1340
1340 # Do not store the response if there was an unknown error.
1341 # Do not store the response if there was an unknown error.
1341 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1342 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1342 pull_request._last_merge_source_rev = \
1343 pull_request._last_merge_source_rev = \
1343 pull_request.source_ref_parts.commit_id
1344 pull_request.source_ref_parts.commit_id
1344 pull_request._last_merge_target_rev = target_reference.commit_id
1345 pull_request._last_merge_target_rev = target_reference.commit_id
1345 pull_request.last_merge_status = merge_state.failure_reason
1346 pull_request.last_merge_status = merge_state.failure_reason
1346 pull_request.shadow_merge_ref = merge_state.merge_ref
1347 pull_request.shadow_merge_ref = merge_state.merge_ref
1347 Session().add(pull_request)
1348 Session().add(pull_request)
1348 Session().commit()
1349 Session().commit()
1349
1350
1350 return merge_state
1351 return merge_state
1351
1352
1352 def _workspace_id(self, pull_request):
1353 def _workspace_id(self, pull_request):
1353 workspace_id = 'pr-%s' % pull_request.pull_request_id
1354 workspace_id = 'pr-%s' % pull_request.pull_request_id
1354 return workspace_id
1355 return workspace_id
1355
1356
1356 def generate_repo_data(self, repo, commit_id=None, branch=None,
1357 def generate_repo_data(self, repo, commit_id=None, branch=None,
1357 bookmark=None, translator=None):
1358 bookmark=None, translator=None):
1358 from rhodecode.model.repo import RepoModel
1359 from rhodecode.model.repo import RepoModel
1359
1360
1360 all_refs, selected_ref = \
1361 all_refs, selected_ref = \
1361 self._get_repo_pullrequest_sources(
1362 self._get_repo_pullrequest_sources(
1362 repo.scm_instance(), commit_id=commit_id,
1363 repo.scm_instance(), commit_id=commit_id,
1363 branch=branch, bookmark=bookmark, translator=translator)
1364 branch=branch, bookmark=bookmark, translator=translator)
1364
1365
1365 refs_select2 = []
1366 refs_select2 = []
1366 for element in all_refs:
1367 for element in all_refs:
1367 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1368 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1368 refs_select2.append({'text': element[1], 'children': children})
1369 refs_select2.append({'text': element[1], 'children': children})
1369
1370
1370 return {
1371 return {
1371 'user': {
1372 'user': {
1372 'user_id': repo.user.user_id,
1373 'user_id': repo.user.user_id,
1373 'username': repo.user.username,
1374 'username': repo.user.username,
1374 'firstname': repo.user.first_name,
1375 'firstname': repo.user.first_name,
1375 'lastname': repo.user.last_name,
1376 'lastname': repo.user.last_name,
1376 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1377 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1377 },
1378 },
1378 'name': repo.repo_name,
1379 'name': repo.repo_name,
1379 'link': RepoModel().get_url(repo),
1380 'link': RepoModel().get_url(repo),
1380 'description': h.chop_at_smart(repo.description_safe, '\n'),
1381 'description': h.chop_at_smart(repo.description_safe, '\n'),
1381 'refs': {
1382 'refs': {
1382 'all_refs': all_refs,
1383 'all_refs': all_refs,
1383 'selected_ref': selected_ref,
1384 'selected_ref': selected_ref,
1384 'select2_refs': refs_select2
1385 'select2_refs': refs_select2
1385 }
1386 }
1386 }
1387 }
1387
1388
1388 def generate_pullrequest_title(self, source, source_ref, target):
1389 def generate_pullrequest_title(self, source, source_ref, target):
1389 return u'{source}#{at_ref} to {target}'.format(
1390 return u'{source}#{at_ref} to {target}'.format(
1390 source=source,
1391 source=source,
1391 at_ref=source_ref,
1392 at_ref=source_ref,
1392 target=target,
1393 target=target,
1393 )
1394 )
1394
1395
1395 def _cleanup_merge_workspace(self, pull_request):
1396 def _cleanup_merge_workspace(self, pull_request):
1396 # Merging related cleanup
1397 # Merging related cleanup
1397 repo_id = pull_request.target_repo.repo_id
1398 repo_id = pull_request.target_repo.repo_id
1398 target_scm = pull_request.target_repo.scm_instance()
1399 target_scm = pull_request.target_repo.scm_instance()
1399 workspace_id = self._workspace_id(pull_request)
1400 workspace_id = self._workspace_id(pull_request)
1400
1401
1401 try:
1402 try:
1402 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1403 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1403 except NotImplementedError:
1404 except NotImplementedError:
1404 pass
1405 pass
1405
1406
1406 def _get_repo_pullrequest_sources(
1407 def _get_repo_pullrequest_sources(
1407 self, repo, commit_id=None, branch=None, bookmark=None,
1408 self, repo, commit_id=None, branch=None, bookmark=None,
1408 translator=None):
1409 translator=None):
1409 """
1410 """
1410 Return a structure with repo's interesting commits, suitable for
1411 Return a structure with repo's interesting commits, suitable for
1411 the selectors in pullrequest controller
1412 the selectors in pullrequest controller
1412
1413
1413 :param commit_id: a commit that must be in the list somehow
1414 :param commit_id: a commit that must be in the list somehow
1414 and selected by default
1415 and selected by default
1415 :param branch: a branch that must be in the list and selected
1416 :param branch: a branch that must be in the list and selected
1416 by default - even if closed
1417 by default - even if closed
1417 :param bookmark: a bookmark that must be in the list and selected
1418 :param bookmark: a bookmark that must be in the list and selected
1418 """
1419 """
1419 _ = translator or get_current_request().translate
1420 _ = translator or get_current_request().translate
1420
1421
1421 commit_id = safe_str(commit_id) if commit_id else None
1422 commit_id = safe_str(commit_id) if commit_id else None
1422 branch = safe_str(branch) if branch else None
1423 branch = safe_str(branch) if branch else None
1423 bookmark = safe_str(bookmark) if bookmark else None
1424 bookmark = safe_str(bookmark) if bookmark else None
1424
1425
1425 selected = None
1426 selected = None
1426
1427
1427 # order matters: first source that has commit_id in it will be selected
1428 # order matters: first source that has commit_id in it will be selected
1428 sources = []
1429 sources = []
1429 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1430 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1430 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1431 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1431
1432
1432 if commit_id:
1433 if commit_id:
1433 ref_commit = (h.short_id(commit_id), commit_id)
1434 ref_commit = (h.short_id(commit_id), commit_id)
1434 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1435 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1435
1436
1436 sources.append(
1437 sources.append(
1437 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1438 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1438 )
1439 )
1439
1440
1440 groups = []
1441 groups = []
1441 for group_key, ref_list, group_name, match in sources:
1442 for group_key, ref_list, group_name, match in sources:
1442 group_refs = []
1443 group_refs = []
1443 for ref_name, ref_id in ref_list:
1444 for ref_name, ref_id in ref_list:
1444 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1445 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1445 group_refs.append((ref_key, ref_name))
1446 group_refs.append((ref_key, ref_name))
1446
1447
1447 if not selected:
1448 if not selected:
1448 if set([commit_id, match]) & set([ref_id, ref_name]):
1449 if set([commit_id, match]) & set([ref_id, ref_name]):
1449 selected = ref_key
1450 selected = ref_key
1450
1451
1451 if group_refs:
1452 if group_refs:
1452 groups.append((group_refs, group_name))
1453 groups.append((group_refs, group_name))
1453
1454
1454 if not selected:
1455 if not selected:
1455 ref = commit_id or branch or bookmark
1456 ref = commit_id or branch or bookmark
1456 if ref:
1457 if ref:
1457 raise CommitDoesNotExistError(
1458 raise CommitDoesNotExistError(
1458 'No commit refs could be found matching: %s' % ref)
1459 'No commit refs could be found matching: %s' % ref)
1459 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1460 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1460 selected = 'branch:%s:%s' % (
1461 selected = 'branch:%s:%s' % (
1461 repo.DEFAULT_BRANCH_NAME,
1462 repo.DEFAULT_BRANCH_NAME,
1462 repo.branches[repo.DEFAULT_BRANCH_NAME]
1463 repo.branches[repo.DEFAULT_BRANCH_NAME]
1463 )
1464 )
1464 elif repo.commit_ids:
1465 elif repo.commit_ids:
1465 # make the user select in this case
1466 # make the user select in this case
1466 selected = None
1467 selected = None
1467 else:
1468 else:
1468 raise EmptyRepositoryError()
1469 raise EmptyRepositoryError()
1469 return groups, selected
1470 return groups, selected
1470
1471
1471 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1472 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1472 hide_whitespace_changes, diff_context):
1473 hide_whitespace_changes, diff_context):
1473
1474
1474 return self._get_diff_from_pr_or_version(
1475 return self._get_diff_from_pr_or_version(
1475 source_repo, source_ref_id, target_ref_id,
1476 source_repo, source_ref_id, target_ref_id,
1476 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1477 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1477
1478
1478 def _get_diff_from_pr_or_version(
1479 def _get_diff_from_pr_or_version(
1479 self, source_repo, source_ref_id, target_ref_id,
1480 self, source_repo, source_ref_id, target_ref_id,
1480 hide_whitespace_changes, diff_context):
1481 hide_whitespace_changes, diff_context):
1481
1482
1482 target_commit = source_repo.get_commit(
1483 target_commit = source_repo.get_commit(
1483 commit_id=safe_str(target_ref_id))
1484 commit_id=safe_str(target_ref_id))
1484 source_commit = source_repo.get_commit(
1485 source_commit = source_repo.get_commit(
1485 commit_id=safe_str(source_ref_id))
1486 commit_id=safe_str(source_ref_id))
1486 if isinstance(source_repo, Repository):
1487 if isinstance(source_repo, Repository):
1487 vcs_repo = source_repo.scm_instance()
1488 vcs_repo = source_repo.scm_instance()
1488 else:
1489 else:
1489 vcs_repo = source_repo
1490 vcs_repo = source_repo
1490
1491
1491 # TODO: johbo: In the context of an update, we cannot reach
1492 # TODO: johbo: In the context of an update, we cannot reach
1492 # the old commit anymore with our normal mechanisms. It needs
1493 # the old commit anymore with our normal mechanisms. It needs
1493 # some sort of special support in the vcs layer to avoid this
1494 # some sort of special support in the vcs layer to avoid this
1494 # workaround.
1495 # workaround.
1495 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1496 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1496 vcs_repo.alias == 'git'):
1497 vcs_repo.alias == 'git'):
1497 source_commit.raw_id = safe_str(source_ref_id)
1498 source_commit.raw_id = safe_str(source_ref_id)
1498
1499
1499 log.debug('calculating diff between '
1500 log.debug('calculating diff between '
1500 'source_ref:%s and target_ref:%s for repo `%s`',
1501 'source_ref:%s and target_ref:%s for repo `%s`',
1501 target_ref_id, source_ref_id,
1502 target_ref_id, source_ref_id,
1502 safe_unicode(vcs_repo.path))
1503 safe_unicode(vcs_repo.path))
1503
1504
1504 vcs_diff = vcs_repo.get_diff(
1505 vcs_diff = vcs_repo.get_diff(
1505 commit1=target_commit, commit2=source_commit,
1506 commit1=target_commit, commit2=source_commit,
1506 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1507 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1507 return vcs_diff
1508 return vcs_diff
1508
1509
1509 def _is_merge_enabled(self, pull_request):
1510 def _is_merge_enabled(self, pull_request):
1510 return self._get_general_setting(
1511 return self._get_general_setting(
1511 pull_request, 'rhodecode_pr_merge_enabled')
1512 pull_request, 'rhodecode_pr_merge_enabled')
1512
1513
1513 def _use_rebase_for_merging(self, pull_request):
1514 def _use_rebase_for_merging(self, pull_request):
1514 repo_type = pull_request.target_repo.repo_type
1515 repo_type = pull_request.target_repo.repo_type
1515 if repo_type == 'hg':
1516 if repo_type == 'hg':
1516 return self._get_general_setting(
1517 return self._get_general_setting(
1517 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1518 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1518 elif repo_type == 'git':
1519 elif repo_type == 'git':
1519 return self._get_general_setting(
1520 return self._get_general_setting(
1520 pull_request, 'rhodecode_git_use_rebase_for_merging')
1521 pull_request, 'rhodecode_git_use_rebase_for_merging')
1521
1522
1522 return False
1523 return False
1523
1524
1524 def _close_branch_before_merging(self, pull_request):
1525 def _close_branch_before_merging(self, pull_request):
1525 repo_type = pull_request.target_repo.repo_type
1526 repo_type = pull_request.target_repo.repo_type
1526 if repo_type == 'hg':
1527 if repo_type == 'hg':
1527 return self._get_general_setting(
1528 return self._get_general_setting(
1528 pull_request, 'rhodecode_hg_close_branch_before_merging')
1529 pull_request, 'rhodecode_hg_close_branch_before_merging')
1529 elif repo_type == 'git':
1530 elif repo_type == 'git':
1530 return self._get_general_setting(
1531 return self._get_general_setting(
1531 pull_request, 'rhodecode_git_close_branch_before_merging')
1532 pull_request, 'rhodecode_git_close_branch_before_merging')
1532
1533
1533 return False
1534 return False
1534
1535
1535 def _get_general_setting(self, pull_request, settings_key, default=False):
1536 def _get_general_setting(self, pull_request, settings_key, default=False):
1536 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1537 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1537 settings = settings_model.get_general_settings()
1538 settings = settings_model.get_general_settings()
1538 return settings.get(settings_key, default)
1539 return settings.get(settings_key, default)
1539
1540
1540 def _log_audit_action(self, action, action_data, user, pull_request):
1541 def _log_audit_action(self, action, action_data, user, pull_request):
1541 audit_logger.store(
1542 audit_logger.store(
1542 action=action,
1543 action=action,
1543 action_data=action_data,
1544 action_data=action_data,
1544 user=user,
1545 user=user,
1545 repo=pull_request.target_repo)
1546 repo=pull_request.target_repo)
1546
1547
1547 def get_reviewer_functions(self):
1548 def get_reviewer_functions(self):
1548 """
1549 """
1549 Fetches functions for validation and fetching default reviewers.
1550 Fetches functions for validation and fetching default reviewers.
1550 If available we use the EE package, else we fallback to CE
1551 If available we use the EE package, else we fallback to CE
1551 package functions
1552 package functions
1552 """
1553 """
1553 try:
1554 try:
1554 from rc_reviewers.utils import get_default_reviewers_data
1555 from rc_reviewers.utils import get_default_reviewers_data
1555 from rc_reviewers.utils import validate_default_reviewers
1556 from rc_reviewers.utils import validate_default_reviewers
1556 except ImportError:
1557 except ImportError:
1557 from rhodecode.apps.repository.utils import get_default_reviewers_data
1558 from rhodecode.apps.repository.utils import get_default_reviewers_data
1558 from rhodecode.apps.repository.utils import validate_default_reviewers
1559 from rhodecode.apps.repository.utils import validate_default_reviewers
1559
1560
1560 return get_default_reviewers_data, validate_default_reviewers
1561 return get_default_reviewers_data, validate_default_reviewers
1561
1562
1562
1563
1563 class MergeCheck(object):
1564 class MergeCheck(object):
1564 """
1565 """
1565 Perform Merge Checks and returns a check object which stores information
1566 Perform Merge Checks and returns a check object which stores information
1566 about merge errors, and merge conditions
1567 about merge errors, and merge conditions
1567 """
1568 """
1568 TODO_CHECK = 'todo'
1569 TODO_CHECK = 'todo'
1569 PERM_CHECK = 'perm'
1570 PERM_CHECK = 'perm'
1570 REVIEW_CHECK = 'review'
1571 REVIEW_CHECK = 'review'
1571 MERGE_CHECK = 'merge'
1572 MERGE_CHECK = 'merge'
1572
1573
1573 def __init__(self):
1574 def __init__(self):
1574 self.review_status = None
1575 self.review_status = None
1575 self.merge_possible = None
1576 self.merge_possible = None
1576 self.merge_msg = ''
1577 self.merge_msg = ''
1577 self.failed = None
1578 self.failed = None
1578 self.errors = []
1579 self.errors = []
1579 self.error_details = OrderedDict()
1580 self.error_details = OrderedDict()
1580
1581
1581 def push_error(self, error_type, message, error_key, details):
1582 def push_error(self, error_type, message, error_key, details):
1582 self.failed = True
1583 self.failed = True
1583 self.errors.append([error_type, message])
1584 self.errors.append([error_type, message])
1584 self.error_details[error_key] = dict(
1585 self.error_details[error_key] = dict(
1585 details=details,
1586 details=details,
1586 error_type=error_type,
1587 error_type=error_type,
1587 message=message
1588 message=message
1588 )
1589 )
1589
1590
1590 @classmethod
1591 @classmethod
1591 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1592 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1592 force_shadow_repo_refresh=False):
1593 force_shadow_repo_refresh=False):
1593 _ = translator
1594 _ = translator
1594 merge_check = cls()
1595 merge_check = cls()
1595
1596
1596 # permissions to merge
1597 # permissions to merge
1597 user_allowed_to_merge = PullRequestModel().check_user_merge(
1598 user_allowed_to_merge = PullRequestModel().check_user_merge(
1598 pull_request, auth_user)
1599 pull_request, auth_user)
1599 if not user_allowed_to_merge:
1600 if not user_allowed_to_merge:
1600 log.debug("MergeCheck: cannot merge, approval is pending.")
1601 log.debug("MergeCheck: cannot merge, approval is pending.")
1601
1602
1602 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1603 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1603 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1604 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1604 if fail_early:
1605 if fail_early:
1605 return merge_check
1606 return merge_check
1606
1607
1607 # permission to merge into the target branch
1608 # permission to merge into the target branch
1608 target_commit_id = pull_request.target_ref_parts.commit_id
1609 target_commit_id = pull_request.target_ref_parts.commit_id
1609 if pull_request.target_ref_parts.type == 'branch':
1610 if pull_request.target_ref_parts.type == 'branch':
1610 branch_name = pull_request.target_ref_parts.name
1611 branch_name = pull_request.target_ref_parts.name
1611 else:
1612 else:
1612 # for mercurial we can always figure out the branch from the commit
1613 # for mercurial we can always figure out the branch from the commit
1613 # in case of bookmark
1614 # in case of bookmark
1614 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1615 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1615 branch_name = target_commit.branch
1616 branch_name = target_commit.branch
1616
1617
1617 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1618 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1618 pull_request.target_repo.repo_name, branch_name)
1619 pull_request.target_repo.repo_name, branch_name)
1619 if branch_perm and branch_perm == 'branch.none':
1620 if branch_perm and branch_perm == 'branch.none':
1620 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1621 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1621 branch_name, rule)
1622 branch_name, rule)
1622 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1623 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1623 if fail_early:
1624 if fail_early:
1624 return merge_check
1625 return merge_check
1625
1626
1626 # review status, must be always present
1627 # review status, must be always present
1627 review_status = pull_request.calculated_review_status()
1628 review_status = pull_request.calculated_review_status()
1628 merge_check.review_status = review_status
1629 merge_check.review_status = review_status
1629
1630
1630 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1631 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1631 if not status_approved:
1632 if not status_approved:
1632 log.debug("MergeCheck: cannot merge, approval is pending.")
1633 log.debug("MergeCheck: cannot merge, approval is pending.")
1633
1634
1634 msg = _('Pull request reviewer approval is pending.')
1635 msg = _('Pull request reviewer approval is pending.')
1635
1636
1636 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1637 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1637
1638
1638 if fail_early:
1639 if fail_early:
1639 return merge_check
1640 return merge_check
1640
1641
1641 # left over TODOs
1642 # left over TODOs
1642 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1643 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1643 if todos:
1644 if todos:
1644 log.debug("MergeCheck: cannot merge, {} "
1645 log.debug("MergeCheck: cannot merge, {} "
1645 "unresolved TODOs left.".format(len(todos)))
1646 "unresolved TODOs left.".format(len(todos)))
1646
1647
1647 if len(todos) == 1:
1648 if len(todos) == 1:
1648 msg = _('Cannot merge, {} TODO still not resolved.').format(
1649 msg = _('Cannot merge, {} TODO still not resolved.').format(
1649 len(todos))
1650 len(todos))
1650 else:
1651 else:
1651 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1652 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1652 len(todos))
1653 len(todos))
1653
1654
1654 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1655 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1655
1656
1656 if fail_early:
1657 if fail_early:
1657 return merge_check
1658 return merge_check
1658
1659
1659 # merge possible, here is the filesystem simulation + shadow repo
1660 # merge possible, here is the filesystem simulation + shadow repo
1660 merge_status, msg = PullRequestModel().merge_status(
1661 merge_status, msg = PullRequestModel().merge_status(
1661 pull_request, translator=translator,
1662 pull_request, translator=translator,
1662 force_shadow_repo_refresh=force_shadow_repo_refresh)
1663 force_shadow_repo_refresh=force_shadow_repo_refresh)
1663 merge_check.merge_possible = merge_status
1664 merge_check.merge_possible = merge_status
1664 merge_check.merge_msg = msg
1665 merge_check.merge_msg = msg
1665 if not merge_status:
1666 if not merge_status:
1666 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1667 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1667 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1668 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1668
1669
1669 if fail_early:
1670 if fail_early:
1670 return merge_check
1671 return merge_check
1671
1672
1672 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1673 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1673 return merge_check
1674 return merge_check
1674
1675
1675 @classmethod
1676 @classmethod
1676 def get_merge_conditions(cls, pull_request, translator):
1677 def get_merge_conditions(cls, pull_request, translator):
1677 _ = translator
1678 _ = translator
1678 merge_details = {}
1679 merge_details = {}
1679
1680
1680 model = PullRequestModel()
1681 model = PullRequestModel()
1681 use_rebase = model._use_rebase_for_merging(pull_request)
1682 use_rebase = model._use_rebase_for_merging(pull_request)
1682
1683
1683 if use_rebase:
1684 if use_rebase:
1684 merge_details['merge_strategy'] = dict(
1685 merge_details['merge_strategy'] = dict(
1685 details={},
1686 details={},
1686 message=_('Merge strategy: rebase')
1687 message=_('Merge strategy: rebase')
1687 )
1688 )
1688 else:
1689 else:
1689 merge_details['merge_strategy'] = dict(
1690 merge_details['merge_strategy'] = dict(
1690 details={},
1691 details={},
1691 message=_('Merge strategy: explicit merge commit')
1692 message=_('Merge strategy: explicit merge commit')
1692 )
1693 )
1693
1694
1694 close_branch = model._close_branch_before_merging(pull_request)
1695 close_branch = model._close_branch_before_merging(pull_request)
1695 if close_branch:
1696 if close_branch:
1696 repo_type = pull_request.target_repo.repo_type
1697 repo_type = pull_request.target_repo.repo_type
1697 close_msg = ''
1698 close_msg = ''
1698 if repo_type == 'hg':
1699 if repo_type == 'hg':
1699 close_msg = _('Source branch will be closed after merge.')
1700 close_msg = _('Source branch will be closed after merge.')
1700 elif repo_type == 'git':
1701 elif repo_type == 'git':
1701 close_msg = _('Source branch will be deleted after merge.')
1702 close_msg = _('Source branch will be deleted after merge.')
1702
1703
1703 merge_details['close_branch'] = dict(
1704 merge_details['close_branch'] = dict(
1704 details={},
1705 details={},
1705 message=close_msg
1706 message=close_msg
1706 )
1707 )
1707
1708
1708 return merge_details
1709 return merge_details
1709
1710
1710
1711
1711 ChangeTuple = collections.namedtuple(
1712 ChangeTuple = collections.namedtuple(
1712 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1713 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1713
1714
1714 FileChangeTuple = collections.namedtuple(
1715 FileChangeTuple = collections.namedtuple(
1715 'FileChangeTuple', ['added', 'modified', 'removed'])
1716 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now