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